Docker Deep Dive: Beyond docker run (2026)
You know docker run and docker-compose up. Now let's unlock the real power of Docker for development and production.
Multi-Stage Builds: Smaller, Faster, Safer Images
# ❌ Single-stage: Everything in one image (HUGE)
FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Result: ~1.5GB image with devDependencies, source code, build tools
# ✅ Multi-stage: Production image only has what it needs
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production=false
COPY . .
RUN npm run build
# Stage 2: Production runtime
FROM node:20-alpine AS runner
WORKDIR /app
# Security: Run as non-root user
RUN addgroup -g 1001 appgroup && \
adduser -u 1001 -G appgroup -D appuser
USER appuser
# Only copy what's needed from builder stage
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./package.json
EXPOSE 3000
CMD ["node", "dist/index.js"]
# Result: ~80MB image. No dev deps, no source code, no root user.
Docker Compose: Full-Stack Development Setup
# docker-compose.yml — Your entire stack in one file
version: '3.8'
services:
# === App ===
app:
build:
context: .
dockerfile: Dockerfile
target: development # Use dev stage from multi-stage build
ports:
- "3000:3000"
environment:
- NODE_ENV=development
- DATABASE_URL=postgresql://app:secret@db:5432/myapp
- REDIS_URL=redis://redis:6379
volumes:
- .:/app # Live reload: host changes appear in container
- /app/node_modules # Prevent host node_modules overwriting container's
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
restart: unless-stopped
# === Database ===
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: myapp
volumes:
- pgdata:/var/lib/postgresql/data # Persist data across restarts
- ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro # Init script
ports:
- "5432:5432" # Expose for local DB tools (pgAdmin, DBeaver)
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 5s
timeout: 3s
retries: 10
# === Redis ===
redis:
image: redis:7-alpine
command: redis-server --appendonly yes --maxmemory 128mb --maxmemory-policy allkeys-lru
volumes:
- redisdata:/data
ports:
- "6379:6379"
# === Adminer (DB management UI) ===
adminer:
image: adminer:latest
ports:
- "8080:8080"
depends_on: [db]
volumes:
pgdata: # Named volume (Docker manages storage)
redisdata:
networks:
default:
name: myapp-network
# Daily workflow with compose:
docker compose up -d # Start everything in background
docker compose logs -f app # Follow app logs
docker compose exec app sh # Shell into app container
docker compose exec db psql -U app # Connect to Postgres directly
docker compose down # Stop everything
docker compose down -v # Stop + delete volumes (!destroys data!)
docker compose up -d --build # Rebuild images then start
Advanced Dockerfile Techniques
# Layer caching optimization
FROM node:20-alpine AS base
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci # Cache layer: only invalidates when pkg files change
# Separate dependency layers (cache optimization trick)
FROM base AS deps
COPY package.json package-lock.json ./
RUN npm ci
FROM base AS dev-deps
COPY package.json package-lock.json ./
RUN npm ci
FROM base AS builder
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# Build arguments (flexible builds)
ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}
RUN if [ "$NODE_ENV" = "production" ]; then npm run build:prod; else echo "Dev mode"; fi
# Non-root user (security best practice)
FROM node:20-alpine AS production
RUN apk add --no-cache tini # Proper init process (signal handling)
ENTRYPOINT ["tini", "--"]
USER node # Use built-in node user
WORKDIR /app
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/index.js"]
# Health check (for orchestration)
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
Networking Deep Dive
# Docker network types:
# 1. Bridge (default) — Containers on same network can communicate
docker network create my-net
docker run --network my-net nginx
docker run --network my-net alpine wget nginx # Can reach nginx by name!
# 2. Host networking — Container shares host's network stack
docker run --network host nginx
# Nginx listens on host's port 80 directly (no port mapping needed!)
# 3. None — No network (isolated)
docker run --network none alpine
# Debugging network issues:
docker network inspect bridge # See network details including IP addresses
docker exec <container> cat /etc/hosts # Check DNS resolution
docker exec <container> ip addr # Check interfaces
docker exec <container> ping db # Test connectivity between containers
# Port publishing vs exposing:
EXPOSE 3000 # Documents the port (metadata only, doesn't publish!)
-p 3000:3000 # Actually publishes to host (localhost:3000 → container:3000)
-p 0.0.0.0:3000:3000 # Bind to all interfaces (accessible from other machines!)
-p 127.0.0.1:3000:3000 # Bind to localhost only (safer!)
# DNS round-robin (load balancing without extra tools):
docker run -d --name app1 --network my-net -e PORT=3001 myapp
docker run -d --name app2 --network my-net -e PORT=3002 myapp
# Both containers register as "myapp" in DNS
# Requests to "myapp" are load-balanced across them!
Performance & Optimization
# Image size analysis:
docker history myimage:v1 # See each layer's size
docker build --no-cache -t myimage:v2 . # Force rebuild (no layer cache)
# Slim images checklist:
# ✅ Use alpine/slim base images
# ✅ Multi-stage builds (discard build tools)
# ✅ .dockerignore (exclude unnecessary files)
# ⚠️ .dockerignore example:
node_modules
npm-debug.log
.git
.env
coverage
.next
dist
Dockerfile
docker-compose.yml
README.md
# Build cache optimization:
# Order Dockerfile instructions from LEAST to MOST frequently changing:
# 1. Base image (rarely changes)
# 2. System dependencies (rarely changes)
# 3. package.json/lock files (changes when deps change)
# 4. Source code (changes often)
# Speed up builds with BuildKit:
DOCKER_BUILDKIT=1 docker build -t myapp .
# Parallel multi-platform builds:
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest .
# Resource limits (prevent containers from eating your machine):
docker run -m 512m --cpus="1.5" myapp # Max 512MB RAM, 1.5 CPU cores
Production Tips
# Logging (structured JSON logs):
docker run --log-driver=json-file --log-opt max-size=10m --log-opt max-file=3 myapp
# Restart policies:
docker run --restart=unless-stopped myapp # Always restart unless manually stopped
docker run --restart=on-failure:5 myapp # Restart on failure, max 5 times
# Read-only filesystem (security!):
docker run --read-only --tmpfs /tmp myapp # Can't write anywhere except /tmp
# Drop capabilities (principle of least privilege):
docker run --cap-drop ALL --cap-add NET_BIND_SERVICE myapp
# Secrets management (never hardcode!):
echo "my_secret_password" | docker secret create db_password -
# In compose:
# secrets:
# db_password:
# external: true
# Then use in service as: /run/secrets/db_password
# Cleanup routine (run weekly):
docker system prune -af --volumes # Remove EVERYTHING unused
# Free space: can reclaim GBs of old images, stopped containers, unused volumes
What's your favorite Docker tip? What Docker topic do you want to learn more about?
Follow @armorbreak for more practical developer guides.
Top comments (0)