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.
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 linesDocker 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
- No
.dockerignore: Bloated build context, leaked secrets. - Single-stage builds: Huge images with build tools in production.
latestonly tagging: No traceability, impossible rollbacks.- Running as root: Security vulnerability amplifier.
- No health checks: Orchestrators can't detect failures.
- Copying everything before installing dependencies: Busted layer cache.
- Using
docker composein production without orchestration: No restart policies, no scaling, no health management.
Build Checklist
- Multi-stage build with minimal runtime image
-
.dockerignoreexcludes 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
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.
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.
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.
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.
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.
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.