Docker Explained — Containers for Beginners

Photo: Adem Percem / Pexels
"It works on my machine" is the most expensive phrase in software. A developer runs code perfectly on their laptop, pushes it to staging, and suddenly nothing works — different Node.js version, missing environment variable, wrong OS library. Docker solves this by packaging your application and everything it needs into a container that runs identically everywhere.
This guide explains Docker from the ground up: what containers are, how to write a Dockerfile, how to run and manage containers, and how to wire multiple services together with Docker Compose.
Containers vs Virtualization
The classic way to guarantee consistent environments was virtual machines (VMs). A VM emulates an entire computer — CPU, memory, storage, network — with its own operating system. This works, but VMs are heavy: each one carries a full OS that can consume gigabytes of memory and take minutes to boot.
Containers take a different approach. They share the host machine's OS kernel and isolate only the application and its dependencies. This makes them:
- Much lighter (megabytes vs gigabytes)
- Much faster to start (milliseconds vs minutes)
- More portable (same behavior on any Linux host)
| Feature | Virtual Machine | Docker Container |
|---|---|---|
| OS | Full guest OS per VM | Shares host kernel |
| Startup time | 30 seconds – 2 minutes | Milliseconds |
| Size | Gigabytes | Megabytes |
| Isolation | Full hardware isolation | Process-level isolation |
| Portability | VM image is provider-specific | Runs on any Docker host |
| Performance overhead | High (hypervisor layer) | Near-native |
| Use case | Full OS isolation, legacy apps | Microservices, CI/CD, dev environments |
Docker is not the only container runtime (Podman, containerd, and LXC are alternatives), but it is by far the most widely used, especially in development workflows.
Key Concepts
Before running any commands, it helps to understand Docker's vocabulary:
Image — A read-only template that contains your application code, runtime, libraries, and configuration. Images are built from a Dockerfile and stored in registries. Think of an image as a blueprint.
Container — A running instance of an image. You can run many containers from the same image simultaneously, each isolated from the others. Think of a container as a running process built from the blueprint.
Dockerfile — A text file with instructions for building an image. Each instruction creates a layer in the image.
Registry — A storage and distribution system for images. Docker Hub is the default public registry. Companies run private registries (AWS ECR, GitHub Container Registry, etc.).
Volume — A mechanism for persisting data outside the container lifecycle. When a container stops or is deleted, its filesystem disappears — volumes survive.
Network — Docker creates isolated networks for containers. Containers on the same network can reach each other by service name; external access requires port mapping.
Dockerfile Anatomy
A Dockerfile is a recipe for building an image. Here is a real-world example for a Node.js API:
# 1. Base image — start from official Node.js LTS
FROM node:20-alpine
# 2. Set working directory inside the container
WORKDIR /app
# 3. Copy package files first (layer caching optimization)
COPY package*.json./
# 4. Install dependencies
RUN npm ci --only=production
# 5. Copy the rest of the application source
COPY..
# 6. Document the port the app listens on (informational)
EXPOSE 3000
# 7. Default command to run when the container starts
CMD ["node", "src/index.js"]
Why copy package.json before the source code? Docker builds images in layers. Each instruction creates a cached layer. If you copy everything at once, any code change invalidates the dependency install layer. By copying package.json first and running npm ci, Docker reuses the cached node_modules layer unless dependencies change — a huge build time speedup.
Common Dockerfile Instructions
| Instruction | Purpose |
|---|---|
FROM |
Set the base image |
WORKDIR |
Set the working directory for subsequent instructions |
COPY |
Copy files from host into the image |
RUN |
Execute a command during build (installs, compiles) |
ENV |
Set environment variables |
EXPOSE |
Document which port the container listens on |
CMD |
Default command to run when container starts (overridable) |
ENTRYPOINT |
Fixed command — arguments passed to it, not replaced |
Building and Running Containers
Build an Image
# Build from the Dockerfile in the current directory
docker build -t myapp:latest.
# Build with a specific tag
docker build -t myapp:1.0.0.
# Build with cache disabled
docker build --no-cache -t myapp:latest.
The -t flag names and optionally tags the image (name:tag). If you omit the tag, Docker uses latest.
Run a Container
# Run the container and expose port 3000
docker run -p 3000:3000 myapp:latest
# Run in detached mode (background)
docker run -d -p 3000:3000 --name my-api myapp:latest
# Run with environment variables
docker run -d -p 3000:3000 \
-e DATABASE_URL=postgres://localhost/mydb \
-e NODE_ENV=production \
--name my-api \
myapp:latest
The -p host:container flag maps a port on your machine to a port inside the container.
Manage Running Containers
# List running containers
docker ps
# List all containers (including stopped)
docker ps -a
# View logs
docker logs my-api
docker logs -f my-api # follow (tail -f equivalent)
# Execute a command inside a running container
docker exec -it my-api sh # open a shell
# Stop and remove a container
docker stop my-api
docker rm my-api
# Stop and remove in one step
docker rm -f my-api
Docker Compose for Multi-Container Apps
Real applications rarely run as a single container. A typical web app has a backend API, a database, a cache, and maybe a background worker. Docker Compose lets you define and run all of these together with a single file.
Here is a docker-compose.yml for a Node.js API with PostgreSQL and Redis:
version: "3.9"
services:
api:
build:.
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=postgres://postgres:secret@db:5432/myapp
- REDIS_URL=redis://cache:6379
depends_on:
- db
- cache
volumes:
-./logs:/app/logs
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: secret
POSTGRES_DB: myapp
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
cache:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
postgres_data:
Compose Commands
# Start all services (build if needed)
docker compose up
# Start in detached mode
docker compose up -d
# Rebuild images before starting
docker compose up --build
# Stop all services
docker compose down
# Stop and remove volumes (delete database data)
docker compose down -v
# View logs for a specific service
docker compose logs -f api
Notice that the API's DATABASE_URL uses db as the hostname — Docker Compose automatically creates a network where services can reach each other by their service name.
This is the pattern used in web development automation workflows and is foundational to modern CI/CD deployment pipelines.
Docker Hub and Private Registries
Docker Hub is the default public registry. Official images for Node.js, PostgreSQL, Redis, Nginx, and thousands of other tools are hosted there.
# Pull an image from Docker Hub
docker pull node:20-alpine
# Login to Docker Hub
docker login
# Tag your image for Docker Hub
docker tag myapp:latest yourusername/myapp:latest
# Push to Docker Hub
docker push yourusername/myapp:latest
For production, most teams use private registries:
- AWS ECR (Elastic Container Registry)
- GitHub Container Registry (ghcr.io)
- Google Artifact Registry
- Self-hosted (Harbor, Nexus)
Private registries keep proprietary code off public servers and integrate with cloud IAM for access control.
When to Use Docker vs When Not To
Docker solves real problems, but it adds complexity. Use it when that trade-off makes sense.
Use Docker when:
- You need consistent environments across dev, staging, and production
- You're deploying microservices or multiple interdependent services
- Your team uses different operating systems (Windows, Mac, Linux)
- You want to run services locally without installing them system-wide (databases, queues)
- You're building CI/CD pipelines
Skip Docker when:
- You're building a simple static website
- Your team is small and all uses the same OS
- The deployment target is a managed platform (Vercel, Netlify, Railway) that handles containerization for you
- You're prototyping something that will be thrown away
- The ops overhead outweighs the consistency benefit for your team size
FAQ
Q: Do I need to know Linux to use Docker?
A: Basic familiarity helps — most Docker images are Linux-based, so commands like ls, cat, and sh are useful. But you don't need to be a Linux expert to write Dockerfiles and run containers.
Q: What's the difference between CMD and ENTRYPOINT?
A: CMD sets the default command and can be overridden when running the container. ENTRYPOINT sets a fixed executable; anything passed to docker run is treated as arguments to it. Use ENTRYPOINT for containers that always run the same program.
Q: How do I persist database data when a container restarts?
A: Use a named volume (as shown in the Compose example above). Data stored in named volumes lives outside the container and survives restarts, stops, and even docker rm.
Q: Why does Docker say "port already in use"?
A: Another process on your host machine is using that port. Find it with lsof -i :3000 (Mac/Linux) or netstat -ano | findstr 3000 (Windows), then stop it or map to a different host port.
Q: What is a multi-stage build? A: A Dockerfile technique where you use one image to build your app and a smaller image to run it. This keeps production images small by excluding build tools. Common for compiled languages (Go, Rust) and frontend builds.
Q: Is Docker the same as Kubernetes? A: No. Docker runs containers on a single machine. Kubernetes orchestrates containers across many machines — handling scaling, load balancing, self-healing, and rolling updates. Docker (or containerd) is the runtime Kubernetes uses under the hood.
Sources
Follow new articles
Add SciHub101 to your feed reader to see new science and technology stories.
About the Author
SciHub101 is an independent science and technology publication founded and edited by Buğra Sözeri, a software engineer and app developer based in Türkiye. AI tools assist with drafting and research. Source documentation in the existing archive is uneven, and articles are being reviewed and corrected. The Editorial Team byline identifies editorial responsibility; it does not certify specialist or independent human review. Where we compare products, anything built by our own team is labelled as such — see the editorial guidelines and About page for how corrections and disclosures work.
Related Articles

Build Tools Compared 2026 — Vite, Webpack, Turbopack, Bun
Vite, webpack, Turbopack, Bun, Rspack and Rolldown compared for 2026: how each works, where each wins, and what the benchmarks do and do not show.

What Is uv? The Fast Python Package Manager
uv is a Rust-based Python package manager from Astral, 10-100x faster than pip. Here's what it does, when to use it, and how it compares to poetry.

Best Web Development Tools 2026 — Editors & Frameworks
Essential web development tools 2026: VS Code, Git, npm, React, Vite, Vercel, PostgreSQL. Complete free stack for building and deploying modern websites.

Deploy Website for Beginners — Go Live in 5 Minutes
Deploy your website for free in 5 minutes: Vercel, Netlify, or GitHub Pages. Custom domain, SSL, environment variables, and rollback guide for beginners.

What Is HTMX? Hypermedia for Modern Web
HTMX explained: how this 14KB library lets you build dynamic web apps with HTML attributes instead of a JavaScript framework, plus how it actually works.

Bun vs Node.js vs Deno — 2026 Comparison
Bun is fastest, Node is bulletproof, Deno is the most secure. A 2026 comparison of the three major JavaScript runtimes with benchmarks and trade-offs.