Skip to main content
Technology & EngineeringDeployment Patterns479 lines

docker-deployment

Comprehensive guide to using Docker for production deployments, covering multi-stage builds, .dockerignore optimization, layer caching strategies, health checks, Docker Compose for local development, container registries, and security scanning best practices.

Quick Summary33 lines
Multi-stage builds produce minimal production images by separating build dependencies from runtime.

## Key Points

- name: Build and push
- name: Scan image
- name: Upload scan results
1. **No `.dockerignore`**: Bloated build context, leaked secrets.
2. **Single-stage builds**: Huge images with build tools in production.
3. **`latest` only tagging**: No traceability, impossible rollbacks.
4. **Running as root**: Security vulnerability amplifier.
5. **No health checks**: Orchestrators can't detect failures.
6. **Copying everything before installing dependencies**: Busted layer cache.
7. **Using `docker compose` in production without orchestration**: No restart policies, no scaling, no health management.
- [ ] Multi-stage build with minimal runtime image
- [ ] `.dockerignore` excludes dev files, `.git`, `node_modules`

## Quick Example

```dockerfile
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
  CMD curl -f http://localhost:3000/health || exit 1
```

```bash
# Local development
docker compose up

# Production-like local
docker compose -f docker-compose.yml -f docker-compose.prod.yml up
```
skilldb get deployment-patterns-skills/docker-deploymentFull skill: 479 lines
Paste into your CLAUDE.md or agent config

Docker for Deployment

Multi-Stage Builds

Multi-stage builds produce minimal production images by separating build dependencies from runtime.

Node.js Application

# Stage 1: Dependencies
FROM node:20-slim AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

# Stage 2: Build
FROM node:20-slim AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
RUN npm prune --production

# Stage 3: Runtime
FROM node:20-slim AS runtime
WORKDIR /app
RUN addgroup --system appgroup && adduser --system --ingroup appgroup appuser

COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY --from=build /app/package.json ./

USER appuser
EXPOSE 3000
ENV NODE_ENV=production
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
  CMD node -e "require('http').get('http://localhost:3000/health', (r) => { process.exit(r.statusCode === 200 ? 0 : 1) })"

CMD ["node", "dist/server.js"]

Python Application

FROM python:3.12-slim AS base
WORKDIR /app

FROM base AS deps
COPY requirements.txt ./
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

FROM base AS runtime
COPY --from=deps /install /usr/local
COPY . .

RUN adduser --system --no-create-home appuser
USER appuser

EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s CMD curl -f http://localhost:8000/health || exit 1
CMD ["gunicorn", "app:create_app()", "--bind", "0.0.0.0:8000", "--workers", "4"]

Go Application (Distroless)

FROM golang:1.22 AS build
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /server

FROM gcr.io/distroless/static-debian12
COPY --from=build /server /server
EXPOSE 8080
USER nonroot:nonroot
ENTRYPOINT ["/server"]

Anti-pattern: Using the build image as the runtime image. A typical Node.js build image is 1.5 GB; a production image should be 150-300 MB.

.dockerignore Optimization

A proper .dockerignore speeds up builds and prevents leaking secrets:

# Version control
.git
.gitignore

# Dependencies (rebuilt in container)
node_modules
vendor
__pycache__

# Build artifacts
dist
build
.next
.nuxt

# Development files
.env
.env.*
!.env.example
*.md
LICENSE
.editorconfig
.prettierrc
.eslintrc*

# IDE
.vscode
.idea
*.swp
*.swo

# Docker
Dockerfile*
docker-compose*
.dockerignore

# Tests
__tests__
*.test.*
*.spec.*
coverage
.nyc_output

# CI/CD
.github
.gitlab-ci.yml
.circleci

Anti-pattern: Not having a .dockerignore. Without it, COPY . . sends everything to the Docker daemon, including node_modules (hundreds of MB) and .git (potentially GB).

Layer Caching Strategies

Docker caches each layer. Order instructions from least to most frequently changing:

# GOOD: Dependencies change less often than source code
FROM node:20-slim
WORKDIR /app

# Layer 1: Rarely changes
COPY package.json package-lock.json ./
RUN npm ci

# Layer 2: Changes frequently
COPY . .
RUN npm run build

Cache Mounts (BuildKit)

# syntax=docker/dockerfile:1
FROM node:20-slim
WORKDIR /app

COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci

COPY . .
RUN --mount=type=cache,target=/app/.next/cache npm run build

CI/CD Caching

# GitHub Actions with Docker layer caching
- name: Build and push
  uses: docker/build-push-action@v5
  with:
    context: .
    push: true
    tags: ghcr.io/org/app:${{ github.sha }}
    cache-from: type=gha
    cache-to: type=gha,mode=max

Anti-pattern: Copying package.json and source code in the same COPY instruction. This invalidates the dependency cache on every source change.

Health Checks

In Dockerfile

HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
  CMD curl -f http://localhost:3000/health || exit 1

Health Check Endpoint

// Express health check
app.get('/health', async (req, res) => {
  try {
    // Check database connection
    await db.query('SELECT 1');

    // Check Redis connection
    await redis.ping();

    res.status(200).json({
      status: 'healthy',
      uptime: process.uptime(),
      timestamp: new Date().toISOString(),
    });
  } catch (error) {
    res.status(503).json({
      status: 'unhealthy',
      error: error.message,
    });
  }
});

Docker Compose Health Checks

services:
  web:
    build: .
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 30s
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5

Anti-pattern: Health checks that only return 200 without actually verifying dependencies. A "healthy" container with a dead database connection is worse than unhealthy.

Docker Compose for Local Development

# docker-compose.yml
services:
  web:
    build:
      context: .
      target: deps  # Use the deps stage for development
    command: npm run dev
    ports:
      - "3000:3000"
    volumes:
      - .:/app
      - /app/node_modules  # Anonymous volume prevents overwrite
    environment:
      - DATABASE_URL=postgresql://postgres:postgres@db:5432/myapp
      - REDIS_URL=redis://redis:6379
      - NODE_ENV=development
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy

  db:
    image: postgres:16
    ports:
      - "5432:5432"
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 5

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5

  mailhog:
    image: mailhog/mailhog
    ports:
      - "1025:1025"  # SMTP
      - "8025:8025"  # Web UI

volumes:
  pgdata:

Production Compose Override

# docker-compose.prod.yml
services:
  web:
    build:
      context: .
      target: runtime
    command: node dist/server.js
    volumes: []  # No source mounting in production
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: '1.0'
# Local development
docker compose up

# Production-like local
docker compose -f docker-compose.yml -f docker-compose.prod.yml up

Container Registries

GitHub Container Registry (GHCR)

# Login
echo $GITHUB_TOKEN | docker login ghcr.io -u USERNAME --password-stdin

# Build and push
docker build -t ghcr.io/org/app:latest -t ghcr.io/org/app:$(git rev-parse --short HEAD) .
docker push ghcr.io/org/app --all-tags

AWS ECR

# Login
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 123456789.dkr.ecr.us-east-1.amazonaws.com

# Build and push
docker build -t 123456789.dkr.ecr.us-east-1.amazonaws.com/app:latest .
docker push 123456789.dkr.ecr.us-east-1.amazonaws.com/app:latest

Docker Hub

docker login -u username
docker build -t username/app:latest .
docker push username/app:latest

Tagging Strategy

# Always tag with git SHA for traceability
IMAGE="ghcr.io/org/app"
SHA=$(git rev-parse --short HEAD)
TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "dev")

docker build \
  -t $IMAGE:latest \
  -t $IMAGE:$SHA \
  -t $IMAGE:$TAG \
  --label "org.opencontainers.image.revision=$SHA" \
  .

Anti-pattern: Only using latest tag. You lose the ability to trace which commit is deployed and can't roll back to a specific version.

Security Scanning

Trivy

# Scan a local image
trivy image myapp:latest

# Scan and fail on critical vulnerabilities
trivy image --severity CRITICAL --exit-code 1 myapp:latest

# Scan Dockerfile for misconfigurations
trivy config Dockerfile

Docker Scout

# Analyze image
docker scout cves myapp:latest

# Compare to previous version
docker scout compare myapp:latest --to myapp:previous

CI Integration

# GitHub Actions
- name: Scan image
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: ghcr.io/org/app:${{ github.sha }}
    format: 'sarif'
    output: 'trivy-results.sarif'
    severity: 'CRITICAL,HIGH'

- name: Upload scan results
  uses: github/codeql-action/upload-sarif@v3
  with:
    sarif_file: 'trivy-results.sarif'

Security Best Practices

# Run as non-root
RUN addgroup --system app && adduser --system --ingroup app app
USER app

# Don't install unnecessary packages
RUN apt-get update && apt-get install -y --no-install-recommends \
    ca-certificates \
    && rm -rf /var/lib/apt/lists/*

# Use specific image digests for reproducibility
FROM node:20-slim@sha256:abc123...

# Don't expose secrets in build args
# BAD: ARG DATABASE_URL
# GOOD: Use runtime environment variables

Anti-pattern: Running containers as root. If the application is compromised, the attacker has root access inside the container.

Common Anti-Patterns Summary

  1. No .dockerignore: Bloated build context, leaked secrets.
  2. Single-stage builds: Huge images with build tools in production.
  3. latest only tagging: No traceability, impossible rollbacks.
  4. Running as root: Security vulnerability amplifier.
  5. No health checks: Orchestrators can't detect failures.
  6. Copying everything before installing dependencies: Busted layer cache.
  7. Using docker compose in production without orchestration: No restart policies, no scaling, no health management.

Build Checklist

  • Multi-stage build with minimal runtime image
  • .dockerignore excludes dev files, .git, node_modules
  • Layer ordering optimized for cache hits
  • Non-root user configured
  • Health check defined
  • Security scan integrated in CI
  • Image tagged with git SHA and semantic version
  • No secrets in build args or image layers

Install this skill directly: skilldb add deployment-patterns-skills

Get CLI access →

Related Skills

database-deployment

Comprehensive guide to database deployment for web applications, covering managed database services (PlanetScale, Neon, Supabase, Turso), migration strategies, connection pooling, backup and restore procedures, data seeding, and schema management best practices for production environments.

Deployment Patterns539L

fly-io-deployment

Complete guide to deploying applications on Fly.io, covering flyctl CLI usage, Dockerfile-based deployments, fly.toml configuration, persistent volumes, horizontal and vertical scaling, multi-region deployments, managed Postgres and Redis, private networking, and auto-scaling strategies.

Deployment Patterns412L

github-actions-cd

Comprehensive guide to implementing continuous deployment with GitHub Actions, covering deploy workflows, environment protection rules, secrets management, matrix builds, dependency caching, artifact management, and deploying to multiple targets including Vercel, Fly.io, AWS, and container registries.

Deployment Patterns469L

monitoring-post-deploy

Comprehensive guide to post-deployment monitoring for web applications, covering uptime checks, error tracking with Sentry, application performance monitoring, log aggregation, alerting strategies, public status pages, and incident response procedures for production systems.

Deployment Patterns572L

netlify-deployment

Complete guide to deploying web applications on Netlify, covering build settings, deploy previews, serverless and edge functions, forms, identity, redirects and rewrites, split testing, and environment variable management for production workflows.

Deployment Patterns399L

railway-deployment

Complete guide to deploying applications on Railway, covering project setup, environment variable management, services and databases (Postgres, Redis, MySQL), persistent volumes, monorepo support, private networking between services, and scheduled cron jobs.

Deployment Patterns434L