Docker Deep Dive: Beyond docker run (2026)
You know docker run and docker build. But production Docker is so much more. Here's what you need to know for real-world container workflows.
Dockerfile Best Practices
# ✅ Production-ready Dockerfile pattern:
# 1. Use specific version tags (not "latest"!)
FROM node:22-alpine3.20 AS base
# 2. Set working directory early
WORKDIR /app
# 3. Install dependencies FIRST (layer caching!)
# This layer only changes when package.json changes
COPY package.json package-lock.json ./
RUN npm ci --only=production && npm cache clean --force
# 4. Copy source code (changes frequently, placed after deps)
COPY . .
# 5. Non-root user for security!
RUN addgroup -g 1001 appgroup && \
adduser -u 1001 -G appgroup -D appuser && \
chown -R appuser:appgroup /app
USER appuser
# 6. Expose port
EXPOSE 3000
# 7. Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
# 8. Use ENTRYPOINT + CMD for flexibility
ENTRYPOINT ["node", "server.js"]
# Multi-stage builds — smaller, more secure images:
# Stage 1: Build
FROM node:22-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Production image (ONLY contains built output!)
FROM node:22-alpine AS runner
WORKDIR /app
RUN addgroup -g 1001 appgroup && \
adduser -u 1001 -G appgroup -D appuser
# Copy ONLY what's needed from builder stage
COPY --from=builder --chown=appuser:appgroup /app/node_modules ./node_modules
COPY --from=builder --chown=appuser:appgroup /app/dist ./dist
COPY --from=builder --chown=appuser:appgroup /app/package.json .
USER appuser
EXPOSE 3000
HEALTHCHECK CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
CMD ["node", "dist/server.js"]
# Result: ~150MB instead of ~800MB+ with full dev toolchain!
Essential Docker Commands Beyond the Basics
# === Image Management ===
docker images # List images
docker images --format "{{.Repository}}:{{.Tag}} {{.Size}}" # Compact view
docker history myimage:latest # See all layers of an image
docker inspect myimage:latest # Full JSON details of an image
docker image prune # Remove unused images
docker image prune -a # Remove ALL unused images (free significant space!)
# === Container Lifecycle Deep Dive ===
docker ps -a # All containers including stopped
docker stats # Live resource usage (CPU, MEM, NET, I/O)
docker top <container> # Processes running inside container
docker port <container> # Port mappings
docker diff <container> # File changes vs original image
docker update --memory="512m" --cpus=1 <container> # Update resource limits live!
# === Inspecting & Debugging ===
docker logs -f --tail=100 <container> # Follow last 100 lines
docker exec -it <container> sh # Interactive shell inside running container
docker exec -it <container> node -e "console.log('hello')" # Run command in container
docker cp <container>:/app/config ./local-config # Copy files OUT of container
docker cp ./local-config <container>:/app/config # Copy files INTO container
# === Network Deep Dive ===
docker network ls # List networks
docker network inspect bridge # Inspect default network
docker network create app-net --driver bridge # Custom network
docker run --network=app-net nginx # Container on custom network
# Containers on same custom network can communicate by name!
# === Volume Management ===
docker volume ls # List volumes
docker volume create data-vol # Create named volume
docker volume inspect data-vol # Volume details (where it's stored)
docker run -v data-vol:/data alpine # Mount named volume
docker run -v $(pwd)/config:/config:ro alpine # Read-only bind mount
docker run --tmpfs /tmp alpine # In-memory tmpfs (fast, ephemeral)
Docker Compose for Development
# docker-compose.dev.yml — Developer-friendly setup
version: "3.8"
services:
app:
build:
context: .
target: development # Use a different Dockerfile stage for dev
volumes:
- .:/app # Hot-reload: local changes reflect immediately
- /app/node_modules # Prevent host overwriting container's node_modules
- npm-cache:/root/.npm # Persist npm cache across rebuilds
environment:
NODE_ENV: development
DEBUG: app:*
ports:
- "3000:3000"
- "9229:9229" # Node.js debugger port
command: sh -c "npm install && npm run dev" # Auto-install on start
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
ports:
- "5432:5432" # Expose locally for GUI tools (pgAdmin, DBeaver)
environment:
POSTGRES_USER: dev
POSTGRES_PASSWORD: dev123
POSTGRES_DB: myapp_dev
volumes:
- pg-data:/var/lib/postgresql/data
- ./init-dev.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U dev"]
interval: 5s
timeout: 3s
retries: 5
redis:
image: redis:7-alpine
ports:
- "6379:6379"
command: redis-server --appendonly yes
adminer: # Web-based DB admin UI (lighter than pgAdmin!)
image: adminer:latest
ports:
- "8080:8080"
volumes:
pg-data:
npm-cache:
Performance Optimization
# 1. Slim down images:
# Use Alpine-based images (~5MB base) instead of Debian/Ubuntu (~100MB+)
# Multi-stage builds remove all build tools from final image
# 2. Layer caching strategy:
# Order Dockerfile instructions from LEAST frequent change to MOST frequent:
# 1. Base image + OS packages (rarely changes)
# 2. System dependencies (rarely changes)
# 3. package.json + npm ci (changes when deps change)
# 4. Source code copy (changes every commit)
# 5. Build step (changes with source)
# 3. .dockerignore (like .gitgitignore but for docker context):
node_modules
dist
.git
.env*
*.log
Dockerfile
docker-compose*.yml
README.md
.nyc_output
coverage
.dockerignore
# 4. Resource limits:
docker run --memory="512m" --cpus="1.0" myimage # Limit memory + CPU
# In compose.yml:
deploy:
resources:
limits:
memory: 512M
cpus: '1.0'
# 5. Clean up apt caches in same RUN instruction:
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 && rm -rf /var/lib/apt/lists/*
# Each RUN creates a layer! Clean in the same layer to keep image small.
Troubleshooting Common Issues
# Container won't start?
docker logs <container> # Always check this first!
docker inspect <container> # Check config, state, health
docker events --since 5m # Recent Docker daemon events
# Can't access localhost port?
docker ps # Is it running?
docker port <container> # Is port mapped?
ss -tlnp | grep :3000 # Is something else using the port?
docker run --network=host ... # Debug: use host networking to bypass port issues
# Out of disk space?
docker system df # Overview of Docker disk usage
docker system df -v # Detailed breakdown
docker system prune -a # Remove ALL unused images, containers, networks, volumes
docker volume prune # Remove unused volumes
# Permission denied errors?
# Files created in container are owned by root by default
# Fix: Use non-root user in Dockerfile (see best practices above)
# Or: docker run -u $(id -u):$(id -g) ...
# DNS issues inside containers?
docker run --dns=8.8.8.8 myimage # Override DNS
# Or configure in daemon.json
# Slow build times?
# 1. Use .dockerexclude to reduce context size
# 2. Leverage layer caching (order instructions properly!)
# 3. Use BuildKit (enabled by default in modern Docker):
DOCKER_BUILDKIT=1 docker build .
What's your most useful Docker tip? What Docker problem took you forever to solve?
Follow @armorbreak for more practical developer guides.
Top comments (0)