The Problem Docker Solves
“It works on my machine!” — every developer has heard this at least once. The root cause is the difference between development and production environments (OS, library versions, configuration). Docker solves this by bundling applications and their runtime environments into isolated packages called containers.
Unlike virtual machines (VMs), containers share the OS kernel, making them much lighter.
| Factor | Virtual Machine (VM) | Container (Docker) |
|---|---|---|
| Startup time | Tens of seconds to minutes | Within seconds |
| Memory | GB scale | MB scale |
| Isolation level | Full OS isolation | Process-level isolation |
| Image size | Several GB | Tens to hundreds of MB |
| Portability | Hypervisor dependent | Runs identically anywhere |
Three Core Concepts
To understand Docker, you only need to know three things: images, containers, and registries.
- Image: A read-only template containing everything needed to run an app (code, runtime, libraries). Think of it as a blueprint.
- Container: A running instance created from an image. It’s the actual building made from the blueprint. You can run multiple containers simultaneously.
- Registry: A repository for storing and sharing images. Docker Hub is the most common example.
Essential Commands
Image Management
These commands download images from Docker Hub and manage them locally.
# Download an image (defaults to 'latest' if tag is omitted)
docker pull nginx:alpine
# List local images
docker images
# REPOSITORY TAG IMAGE ID SIZE
# nginx alpine a2573b4e3f72 43MB
# Delete an image
docker rmi nginx:alpine
# View image details (layers, environment variables, etc.)
docker inspect nginx:alpine
The alpine tag refers to lightweight Alpine Linux-based images. Even for the same app, node:22-alpine (~50MB) is much smaller than node:22 (~350MB). Use alpine tags whenever possible in production.
Running and Managing Containers
These commands run images as containers and manage their state.
# Run in background (-d), assign a name, map ports (host:container)
docker run -d --name my-nginx -p 8080:80 nginx:alpine
# → Accessible at http://localhost:8080
# List running containers
docker ps
# CONTAINER ID IMAGE STATUS PORTS NAMES
# a1b2c3d4e5f6 nginx:alpine Up 5s 0.0.0.0:8080->80/tcp my-nginx
# List all containers (including stopped ones)
docker ps -a
# Stop / restart / remove
docker stop my-nginx
docker start my-nginx
docker rm my-nginx # Can only remove when stopped
docker rm -f my-nginx # Force remove (even while running)
In -p 8080:80, the first number is the host port and the second is the container port. This is easy to mix up, so remember the order: host:container.
Debugging Commands
Used for inspecting container internals and viewing logs.
# Stream logs in real time
docker logs -f my-nginx
# View only the last 50 lines
docker logs --tail 50 my-nginx
# Access a shell inside the container
docker exec -it my-nginx sh
# → Run commands like ls, cat inside the container
# → Type exit to leave
# Monitor container resource usage
docker stats my-nginx
# CPU % MEM USAGE NET I/O BLOCK I/O
# 0.00% 2.3MiB 1kB/0B 0B/0B
Writing a Dockerfile
A Dockerfile is the recipe for building an image. Here’s an explanation of each instruction using a Node.js app as an example.
# 1. Specify base image (prefer alpine)
FROM node:22-alpine
# 2. Set working directory
WORKDIR /app
# 3. Copy dependency files first → key to cache optimization
COPY package.json package-lock.json ./
# 4. Install dependencies (exclude devDependencies)
RUN npm ci --omit=dev
# 5. Copy source code (must come after step 3 to keep cache valid)
COPY . .
# 6. Document the port the container will use
EXPOSE 3000
# 7. Run command
CMD ["node", "server.js"]
The order of steps 3-5 is important. If package.json hasn’t changed, the npm ci layer is cached, significantly speeding up the build. If only the source code changes, only step 5 onward is re-executed.
Build and run:
# Build image (-t: assign tag, .: current directory)
docker build -t my-app:1.0 .
# Run the built image
docker run -d --name my-app -p 3000:3000 my-app:1.0
Use a .dockerignore file to exclude unnecessary files from the build context.
node_modules
.git
.env
dist
*.md
Docker Compose
A tool for defining and running multiple containers at once. Use it when managing related services together, like a web app + database.
# docker-compose.yml (or compose.yml)
services:
web:
build: . # Build using the Dockerfile in the current directory
ports:
- "3000:3000" # Host:container port mapping
environment:
- NODE_ENV=production
- DATABASE_URL=postgres://user:secret@db:5432/mydb
depends_on:
- db # Specify dependency so db starts first
restart: unless-stopped # Auto-restart on abnormal exit
db:
image: postgres:16-alpine # Use official image
volumes:
- db-data:/var/lib/postgresql/data # Persist data
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=secret
- POSTGRES_DB=mydb
volumes:
db-data: # Named volume (persists even if container is removed)
Compose commands:
# Build + run all services in background
docker compose up -d --build
# Check service status
docker compose ps
# View logs (specific service only)
docker compose logs -f web
# Stop all services + remove containers
docker compose down
# Also remove volumes (includes DB data — use with caution!)
docker compose down -v
depends_on only guarantees startup order. It does not wait until the DB is “ready.” Implement DB connection retry logic in your app, or configure a healthcheck.
Practical Tips
| Scenario | Command | Description |
|---|---|---|
| Disk cleanup | docker system prune -a | Remove all unused images/containers/networks |
| Build cache check | docker builder prune | Remove build cache only |
| Environment variable file | docker run --env-file .env | Inject .env file variables into the container |
| Volume mount | docker run -v $(pwd):/app | Mount host directory to container (for development) |
| Multi-stage build | FROM ... AS builder | Exclude build tools from final image to reduce size |
In production, use specific version tags (e.g., node:22.14-alpine) instead of latest. Since latest can change at any time, the same Dockerfile may produce different results.