Docker Deep Dive: Beyond docker run (2026)
You know docker run -p 3000:3000. Here's everything else that makes Docker powerful in production.
Dockerfile Best Practices
# ❌ BAD: Bloated image, slow builds, security risks
FROM node:20
RUN apt-get update && apt-get install -y vim curl git
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]
# ✅ GOOD: Optimized for size, speed, and security
FROM node:20-alpine AS base
# Build stage — install deps only (layer caching!)
FROM base AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production && npm cache clean --force
# Production stage — minimal final image
FROM base AS production
WORKDIR /app
# Create non-root user (security best practice)
RUN addgroup --system --gid 1001 appgroup && \
adduser --system --uid 1001 appuser
# Copy from build stage
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Set ownership
RUN chown -R appuser:appgroup /app
USER appuser
EXPOSE 3000
# Health check (Docker can monitor your app!)
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
CMD ["node", "server.js"]
Key Dockerfile Rules
1. Layer caching is your friend
→ COPY package.json FIRST (before source code)
→ npm install runs only when package.json changes
→ Source code changes don't trigger reinstall!
2. Use .dockerignore (like .gitignore for Docker)
node_modules # Rebuild fresh each time
.git # Don't copy repo history
.env # Secrets!
*.md # Documentation not needed at runtime
.DS_Store # macOS junk
coverage/ # Test output
dist/ # If you're building inside Docker
3. Alpine images are tiny (~8MB vs ~180MB for full Node)
Trade-off: Some tools missing (glibc compatibility issues)
For most Node apps: alpine works perfectly fine
4. Multi-stage builds = smaller production images
Build stage has all dev tools
Production stage only gets what's needed
Result: Image goes from 500MB+ to ~80MB
5. Never run as root
USER appuser after setup
Prevents container escape attacks
Required by many Kubernetes clusters anyway
Docker Compose for Local Development
# docker-compose.yml
version: '3.9'
services:
app:
build:
context: .
target: development # Use a different stage for dev
ports:
- "3000:3000"
- "9229:9229" # Node debugger port
environment:
- NODE_ENV=development
- DATABASE_URL=postgres://dev:dev@db:5432/myapp
- REDIS_URL=redis://redis:6379
volumes:
- .:/app # Live reload! Code changes reflect immediately
- /app/node_modules # Prevent host node_modules overwriting container's
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped # Auto-restart on crash
networks:
- backend
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: dev
POSTGRES_PASSWORD: dev
POSTGRES_DB: myapp
volumes:
- pgdata:/var/lib/postgresql/data
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U dev"]
interval: 5s
timeout: 3s
retries: 5
networks:
- backend
redis:
image: redis:7-alpine
command: redis-server --appendonly yes
volumes:
- redisdata:/data
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
networks:
- backend
volumes:
pgdata: # Named volume (persists across containers)
redisdata:
networks:
backend:
driver: bridge # Isolated network for services
Essential Compose Commands
# Start everything
docker compose up -d # Detached mode (background)
docker compose up # Foreground (see logs live)
# Development workflow
docker compose up --build # Rebuild before starting
docker compose logs -f app # Follow logs for one service
docker compose logs -f --tail=50 # Last 50 lines, then follow
docker compose exec app sh # Shell into running container
docker compose exec app npm run migrate # Run commands inside container
# Debugging
docker compose ps # Show all services status
docker compose top # Show processes in each container
docker compose stats # Resource usage (CPU, memory, network)
# Cleanup
docker compose down # Stop and remove containers + networks
docker compose down -v # Also remove volumes (resets data!)
docker compose down --rmi all # Also remove images (full reset)
# One-off tasks
docker compose run --rm app npm run test # Run tests then exit
docker compose run --rm db psql -U dev -d myapp # Connect to DB directly
Docker Networks Explained
# Default networks:
# bridge → Default, containers on same host can communicate
# host → Container uses host's network stack (no isolation)
# none → No networking (rarely used)
# Custom networks (recommended):
docker network create myapp-net
docker run --network myapp-net ...
# DNS resolution works automatically!
# Container names become hostnames:
# app container can reach db via: postgres://db:5432/myapp
# Network types:
bridge # Most common, isolated per network
host # Direct host access (good for performance monitoring)
overlay # Multi-host (Docker Swarm/Kubernetes)
macvlan # Container gets its own MAC address (appears as physical device)
ipvlan # Like macvlan but shares host MAC
Volumes vs Bind Mounts
# Bind mount: Host path → Container path
# Good for: Development (live code sync), config files
docker run -v $(pwd)/src:/app/src nginx
# Named volume: Managed by Docker, stored in /var/lib/docker/volumes/
# Good for: Data persistence, databases, caches
docker run -v mydata:/data/app nginx
# Read-only bind mount (security!)
docker run -v $(pwd)/config:/etc/app/config:ro nginx
# tmpfs: In-memory only (gone when container stops)
# Good for: Temporary files, secrets, sensitive processing
docker run --tmpfs /tmp:rw,noexec,nosuid,size=100m nginx
# Volume lifecycle:
docker volume create mydata # Create
docker volume inspect mydata # See details (where it lives)
docker volume ls # List all
docker volume prune # Remove unused volumes (frees disk space!)
Useful Docker Commands You Might Not Know
# Container management
docker ps -a # All containers (including stopped)
docker stats # Real-time resource usage (like top)
docker top <container> # Processes inside container
docker update --memory=512m <container> # Update resource limits live!
docker rename old-name new-name # Rename a container
# Inspecting
docker inspect <container> | jq '.[0].NetworkSettings.IPAddress' # Get IP
docker inspect <container> --format='{{.State.Status}}' # Quick status
docker logs <container> --since 1h # Logs from last hour
docker logs <container> --tail 100 -f # Tail last 100 lines
# Copying files
docker cp file.txt container:/path/inside/ # Host → Container
docker cp container:/path/file.txt ./local/ # Container → Host
# Cleaning up (run regularly!)
docker system prune # Remove stopped containers, unused networks, dangling images
docker system prune -a # ALSO remove unused images (not just dangling)
docker volume prune # Remove unused volumes
docker builder prune # Remove build cache (can be GBs!)
# Image management
docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}" # Pretty table
docker history <image> # See each layer and its size
docker save myimage | gzip > image.tar.gz # Export image for transfer
docker load < image.tar.gz # Import exported image
# Debugging a failing container
docker run --rm -it --entrypoint sh myimage # Override entrypoint to get shell
docker commit <container> myimage:debug # Save container state as new image
Docker in CI/CD
# GitHub Actions example
name: Build & Push Docker Image
on:
push:
branches: [main]
jobs:
docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
${{ secrets.DOCKERHUB_USERNAME }}/myapp:${{ github.sha }}
${{ secrets.DOCKERHUB_USERNAME }}/myapp:latest
cache-from: type=gha # GitHub Actions cache
cache-to: type=gha,mode=max # Speed up future builds
platforms: linux/amd64,linux/arm64 # Multi-architecture support!
Common Docker Problems & Solutions
# Problem: Container exits immediately
# Solution: Check the process! Containers die when their main process exits.
docker logs <container> # Always first step
# Fix: Make sure your CMD/ENTRYPOINT runs a long-running process
# Problem: Permission denied errors
# Solution: UID mismatch between host and container
# In Dockerfile: RUN adduser --uid 1001 appuser; USER appuser
# Or: docker run -u $(id -u):$(id -g) ...
# Problem: Disk space full
# Solution: Regular cleanup
docker system df # See what's using space
docker system prune -a --volumes # Nuclear option (frees most space)
# Problem: Networking issues (can't reach other containers)
# Solution: Check they're on the same network
docker network inspect <network-name>
# Ensure both services use the same network in compose
# Problem: Stale layers making rebuilds slow
# Solution: Clear build cache
docker builder prune
# Then rebuild with --no-cache once
# Problem: Timezone wrong inside container
# Solution: Pass TZ environment variable
docker run -e TZ=Asia/Shanghai ...
# Or in Dockerfile: ENV TZ=Asia/Shanghai
What's your favorite Docker tip?
Follow @armorbreak for more practical developer guides.
Top comments (0)