DEV Community

Alex Chen
Alex Chen

Posted on

Docker Deep Dive: Beyond docker run (2026)

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.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
# 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)