Docker for Developers: The Only Guide You Actually Need (2026)
Stop memorizing Docker commands. Understand how it works once, and you'll never be confused again.
Why Docker Matters
"It works on my machine" → No longer a valid excuse.
Docker ensures your app runs the same way:
→ On your laptop
→ On your teammate's Mac
→ On the Linux server
→ In CI/CD pipeline
One command to rule them all: docker compose up
Docker vs VMs (The Key Insight)
Virtual Machine:
┌─────────────────────────────────┐
│ Host OS │
│ ┌─────────────────────────┐ │
│ │ Guest OS │ │ ← Full OS (GBs of RAM)
│ │ ┌───────────────┐ │ │
│ │ │ Your App │ │ │
│ │ └───────────────┘ │ │
│ └─────────────────────────┘ │
└─────────────────────────────────┘
Container:
┌─────────────────────────────────┐
│ Host OS (Linux) │
│ ┌─────────────────────────┐ │
│ │ Docker Engine │ │
│ │ ┌───────────────┐ │ │
│ │ │ Your App │ │ │ ← Just your app + deps (MBs)
│ │ └───────────────┘ │ │
│ └─────────────────────────┘ │
└─────────────────────────────────┘
Containers share the host OS kernel. That's why they're lightweight.
Essential Concepts (5 Minutes)
Image vs Container
# Image = Blueprint (like a class)
# Container = Running instance (like an object)
docker run node:20-alpine # Pull image + create container
docker images # List all images
docker ps # List running containers
docker ps -a # List ALL containers (including stopped)
Dockerfile = Your Build Recipe
# Every Dockerfile starts with a base image
FROM node:20-alpine
# Set working directory
WORKDIR /app
# Copy package files first (layer caching!)
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy source code
COPY . .
# Expose port
EXPOSE 3000
# Command to run
CMD ["node", "server.js"]
docker-compose.yml = Multi-Service Orchestration
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=postgres://user:pass@db:5432/myapp
depends_on:
- db
restart: unless-stopped
db:
image: postgres:16-alpine
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: myapp
restart: unless-stopped
volumes:
pgdata:
The Commands I Use Every Day
# === Building & Running ===
docker compose up -d # Start services in background
docker compose down # Stop & remove containers
docker compose logs -f --tail 50 # Follow logs (last 50 lines)
docker compose restart app # Restart one service
# === Debugging ===
docker exec -it app sh # Shell into running container
docker exec -it app npm test # Run command inside container
docker compose run --rm app npm run migrate # Run one-off command
# === Cleanup ===
docker system df # See disk usage
docker system prune -a # Remove unused images, containers, volumes
docker volume prune # Remove unused volumes
# === Inspecting ===
docker inspect <container> # Full JSON config of container
docker stats # Live resource usage (CPU, MEM, NET)
docker logs <container> -f # Follow specific container logs
Real-World Examples
1. Node.js API with PostgreSQL
# Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
FROM node:20-alpine AS runner
WORKDIR /app
RUN addgroup -g 1001 app && adduser -u 1001 -G app -s /bin/sh -D app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
USER app
EXPOSE 3000
CMD ["node", "dist/server.js"]
# docker-compose.yml
version: '3.8'
services:
api:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://app:secret@db:5432/appdb
REDIS_URL: redis://redis:6379
depends_on:
- db
- redis
db:
image: postgres:16-alpine
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: appdb
ports:
- "5432:5432"
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
postgres_data:
2. Static Site with Nginx
version: '3.8'
services:
web:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./public:/usr/share/nginx/html:ro
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
restart: unless-stopped
3. Development Environment (Hot Reload)
version: '3.8'
services:
dev:
image: node:20-alpine
working_dir: /app
command: sh -c "npm install && npm run dev"
ports:
- "3000:3000"
- "9229:9229" # Node.js debugger
volumes:
- .:/app # Live reload source changes!
- node_modules:/app/node_modules # Persist installed modules
environment:
- NODE_ENV=development
volumes:
node_modules:
Performance Tips
1. Optimize Your Dockerfile
# ❌ BAD — Rebuilds everything on any code change
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]
# ✅ GOOD — Leverages layer caching
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci # Only re-runs if package.json changes
COPY . .
CMD ["node", "server.js"]
# ✅ BETTER — Multi-stage build (smaller final image)
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
CMD ["node", "server.js"]
2. Use .dockerignore
# .dockerignore — like .gitignore for Docker
node_modules
npm-debug.log
.git
.env
coverage
.next
build
.DS_Store
*.md
!README.md
This can cut build time from minutes to seconds.
3. Smaller Base Images
| Image | Size | When to use |
|---|---|---|
node:20 |
~1 GB | Development |
node:20-alpine |
~180 MB | Production (most cases) |
node:20-slim |
~200 MB | Need glibc compatibility |
Common Problems & Solutions
"Permission denied" in mounted volumes
# Add this to your service
user: "${UID:-1000}:${GID:-1000}"
Then run: UID=$(id -u) GID=$(id -g) docker compose up -d
Container eats all memory
# Add memory limits
services:
app:
deploy:
resources:
limits:
memory: 512M
reservations:
memory: 256M
Port already in use
# Find what's using the port
lsof -i :3000
# Kill it or change your mapping
ports:
- "3001:3000" # Map host 3001 → container 3000
Images getting too large
# Check what's taking space
docker history your-image:latest
# Clean up in Dockerfile
RUN apt-get clean && rm -rf /var/lib/apt/lists/*
Docker for Side Projects (My Setup)
I run 4 projects on one VPS using Docker Compose:
# docker-compose.yml (production)
version: '3.8'
services:
agentvote:
build: ./agentvote
restart: unless-stopped
ports:
- "3000:3000"
environment:
- NODE_ENV=production
signal:
build: ./crypto-signal
restart: unless-stopped
ports:
- "3001:3001"
volumes:
- signal_data:/app/data
blog:
image: nginx:alpine
restart: unless-stopped
volumes:
- ./blog/public:/usr/share/nginx/html:ro
ports:
- "8080:80"
formatter:
build: ./text-formatter
restart: unless-stopped
ports:
- "3099:3099"
volumes:
signal_data:
# Deploy everything
docker compose up -d --build
# Update one service
docker compose up -d --build agentvote
# Check everything's running
docker compose ps
Quick Reference Card
# Lifecycle
docker compose up -d # Start
docker compose down # Stop
docker compose restart # Restart
# Debugging
docker exec -it <svc> sh # Shell in
docker compose logs -f # Logs
docker compose ps # Status
# Maintenance
docker system prune -a # Cleanup
docker compose pull # Update images
docker compose up -d --build # Rebuild & restart
What's your Docker setup look like? Still avoiding it?
Follow @armorbreak for more practical DevOps guides.
Top comments (0)