Docker for Backend Developers: Production Patterns
Docker is not just for packaging applications. In production, it is the foundation for consistent deployments, reproducible environments, and horizontal scaling. But the Dockerfile patterns that work in development often fail in production.
This guide covers the Docker patterns that backend developers need for production deployments. For multi-stage build specifics, see the Docker multi-stage builds guide.
Problem
Production Docker deployments fail because of:
- Images that are 1-2 GB when they should be 100 MB
- Containers that run as root
- Build caches that are never used, causing slow CI
- No health checks, so orchestrators cannot detect failures
- Secrets baked into images
Production Dockerfile for Python
# Stage 1: Build dependencies
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
# Stage 2: Production image
FROM python:3.12-slim
# Security: non-root user
RUN useradd --create-home appuser
WORKDIR /home/appuser/app
# Copy only installed packages
COPY --from=builder /install /usr/local
COPY . .
# Security: switch to non-root
USER appuser
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Production Dockerfile for Node.js
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM node:20-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /home/appuser/app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
USER appuser
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
CMD ["node", "server.js"]
Docker Compose for Development
services:
api:
build:
context: .
target: builder
volumes:
- .:/app
- /app/node_modules
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgresql://user:pass@db:5432/app
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: app
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user"]
interval: 5s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
volumes:
pgdata:
Optimizing Build Cache
Layer ordering determines cache efficiency. Put things that change least at the top:
# GOOD: Dependencies cached separately from source code
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
# BAD: Any source change invalidates the dependency cache
COPY . .
RUN pip install -r requirements.txt
Secrets Management
Never put secrets in the image:
# BAD: Secret baked into the image
ENV DATABASE_URL=postgresql://user:password@host/db
# GOOD: Passed at runtime
# (no ENV for secrets in Dockerfile)
# Pass secrets at runtime
docker run -e DATABASE_URL="postgresql://..." myapp
# Or use Docker secrets
echo "postgresql://..." | docker secret create db_url -
Container Logging
Write logs to stdout/stderr, not files:
import logging
import sys
logging.basicConfig(
stream=sys.stdout,
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
)
Docker captures stdout/stderr automatically. Log aggregation tools (CloudWatch, Datadog, ELK) collect from the Docker log driver.
Resource Limits
Always set memory and CPU limits:
services:
api:
deploy:
resources:
limits:
memory: 512M
cpus: "1.0"
reservations:
memory: 256M
cpus: "0.5"
Without limits, one container can consume all host resources and crash others.
Common Mistakes
Mistake 1: Running as root. If a vulnerability allows code execution, the attacker has root access. Always use a non-root user.
Mistake 2: Using :latest tag. In production, always pin versions. python:3.12-slim not python:latest.
Mistake 3: No .dockerignore. Without it, COPY . . includes node_modules, .git, and test files in the image.
Mistake 4: No health check. Orchestrators cannot restart unhealthy containers without health checks.
Takeaways
Production Docker patterns start with multi-stage builds for small images, non-root users for security, and health checks for reliability. Cache dependencies separately from source code. Never bake secrets into images. Set resource limits. These patterns apply whether you deploy to Kubernetes, ECS, or a single server.