| name | docker |
| description | Use when writing or optimizing a Dockerfile or docker-compose.yml for local dev, debugging containers that behave differently from local, or reducing image size for Python and Node.js apps. Not for Kubernetes resources or Helm — use containerization. |
Docker Patterns
Production-ready Docker patterns for Python and Node.js services.
When to Activate
- Writing or optimizing a Dockerfile (multi-stage, layer caching)
- Configuring Docker Compose services, networking, or volumes
- Adding health checks or startup dependencies
- Passing secrets and environment variables safely
- Debugging a container that won't start or behaves differently in Docker vs local
- Reducing image size
Dockerfile — Python (FastAPI / uv)
# syntax=docker/dockerfile:1
# --- Build stage ---
FROM python:3.12-slim AS builder
WORKDIR /app
# Install uv
RUN pip install uv --no-cache-dir
# Copy dependency files first — cached unless they change
COPY pyproject.toml uv.lock ./
# Install deps into a prefix (not system), no dev deps
RUN uv sync --frozen --no-dev --prefix /install
# --- Runtime stage ---
FROM python:3.12-slim AS runtime
WORKDIR /app
# Copy installed packages from builder
COPY --from=builder /install /usr/local
# Copy application code last (changes most often)
COPY src/ ./src/
# Non-root user — security best practice
RUN adduser --disabled-password --gecos "" appuser
USER appuser
EXPOSE 8000
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]
Dockerfile — Node.js (Next.js)
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --frozen-lockfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:20-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
# Only copy what's needed to run
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]
Enable standalone output in next.config.js:
output: "standalone"
.dockerignore
# Always include these
.git
.gitignore
**/.env
**/.env.*
**/node_modules
**/__pycache__
**/*.pyc
**/*.pyo
.venv
dist
build
.next
coverage
*.log
.DS_Store
Layer Caching Rules
Order files from least-changed to most-changed:
# ✅ GOOD — deps cached unless pyproject.toml changes
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen
COPY src/ ./src/ # code changes don't bust dep layer
# ❌ BAD — any code change busts the dep install layer
COPY . .
RUN uv sync --frozen
Cache mounts (BuildKit) — don't write pip/uv cache to image:
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev
Docker Compose
services:
api:
build:
context: .
target: runtime
ports:
- "5003:8000"
environment:
DATABASE_URL: postgresql+asyncpg://user:pass@db:5432/app
REDIS_URL: redis://redis:6379
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
restart: unless-stopped
networks:
- backend
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: app
volumes:
- pg_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d app"]
interval: 5s
timeout: 3s
retries: 5
start_period: 10s
networks:
- backend
redis:
image: redis:7-alpine
command: redis-server --appendonly yes
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 3
networks:
- backend
volumes:
pg_data:
redis_data:
networks:
backend:
driver: bridge
Health Checks
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
FastAPI health endpoint:
@app.get("/health")
async def health():
return {"status": "ok"}
Environment Variables and Secrets
services:
api:
environment:
SECRET_KEY: dev-secret
environment:
SECRET_KEY: ${SECRET_KEY}
env_file:
- .env
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txt
Access secret in container at /run/secrets/db_password.
Networking
networks:
frontend:
backend:
services:
nginx:
networks: [frontend]
api:
networks: [frontend, backend]
db:
networks: [backend]
Service DNS: containers on the same network reach each other by service name.
# From api container, reach db:
postgresql://db:5432/mydb # "db" resolves to the db container IP
redis://redis:6379
Common Commands
docker build -t myapp .
docker build --target builder -t myapp:builder .
docker build --no-cache -t myapp .
docker compose up -d
docker compose up --build
docker compose down -v
docker compose logs -f api
docker compose exec api bash
docker compose ps
docker inspect <container>
docker stats
docker system df
docker system prune -f
docker volume prune -f
docker image prune -a -f
Debugging
docker run -it --entrypoint bash myapp
docker exec <container> env
docker cp <container>:/app/logs ./local-logs
docker run --network host myapp
docker compose run --rm api python manage.py migrate
Image Size Reduction
| Technique | Savings |
|---|
Use slim/alpine base (python:3.12-slim) | 60–80% vs full image |
| Multi-stage build — don't ship build tools | varies |
--no-install-recommends on apt-get | 20–40% |
Remove apt cache: rm -rf /var/lib/apt/lists/* | small |
--no-cache-dir on pip | small |
.dockerignore to skip test/docs | small |
# Minimal apt install pattern
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
&& rm -rf /var/lib/apt/lists/*
Red Flags
COPY . . before installing dependencies — copying all source code first busts the dependency layer on every code change; always COPY pyproject.toml uv.lock ./ → install → COPY src/ ./ so deps are cached unless manifests change
- Running the container as root — the default user is root inside a container; a process escape gives the attacker full host access; always add
RUN adduser --disabled-password appuser && USER appuser in the runtime stage
- No health check on services others
depends_on — depends_on without condition: service_healthy starts the dependent service immediately, before the dependency is actually ready; always define a healthcheck and use service_healthy
- Secrets in
environment: as plaintext — environment variables are visible in docker inspect, CI logs, and image layers if baked in; use Docker secrets, a secrets manager, or pass via host env refs (SECRET_KEY: ${SECRET_KEY})
- No
.dockerignore — without it, COPY . . sends the entire repo (.git, node_modules, __pycache__, .env) into the build context, bloating image size and potentially leaking secrets
- Single-stage build shipping build tools to production — compilers, dev headers, and test dependencies included in the runtime image increase attack surface and image size; use multi-stage builds so the runtime stage starts fresh from a slim base
- Anonymous volumes for data that must persist —
volumes: ["/var/lib/postgresql/data"] (without a named volume) is recreated on docker compose down; use named volumes (pg_data:/var/lib/postgresql/data) to persist data across restarts
Checklist