Docker Best Practices
When to Use
Activate this skill when:
- •Creating a new Dockerfile for a Python backend or React frontend
- •Optimizing existing Docker images for smaller size or faster builds
- •Setting up Docker Compose for local development
- •Configuring multi-stage builds to separate build and runtime dependencies
- •Hardening container security (non-root user, minimal base images)
- •Running security scans on Docker images with Trivy
- •Designing an image tagging strategy for CI/CD pipelines
- •Troubleshooting Docker build failures or runtime issues
Do NOT use this skill for:
- •Deployment orchestration or CI/CD pipelines (use
deployment-pipeline) - •Kubernetes configuration or Helm charts
- •Cloud infrastructure provisioning (Terraform, CloudFormation)
- •Application code patterns (use
python-backend-expertorreact-frontend-expert)
Instructions
Multi-Stage Build Strategy
Multi-stage builds keep final images small by separating build-time and runtime dependencies.
Principle: Build in a full image, run in a minimal image. Only copy what is needed for runtime.
┌──────────────────────────────────┐ │ Stage 1: Builder │ │ Full SDK, build tools, deps │ │ Compile, install, build │ ├──────────────────────────────────┤ │ Stage 2: Runtime │ │ Minimal base image │ │ COPY --from=builder artifacts │ │ Non-root user, health check │ └──────────────────────────────────┘
Python Backend Dockerfile
See references/python-dockerfile-template for the complete template.
Key decisions for Python:
# Stage 1: Build dependencies
FROM python:3.12-slim AS builder
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential libpq-dev \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
# Stage 2: Runtime
FROM python:3.12-slim AS runtime
# Install only runtime system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq5 curl \
&& rm -rf /var/lib/apt/lists/*
# Create non-root user
RUN groupadd -r appuser && useradd -r -g appuser -d /app -s /sbin/nologin appuser
WORKDIR /app
COPY --from=builder /install /usr/local
COPY src/ ./src/
COPY alembic/ ./alembic/
COPY alembic.ini .
RUN chown -R appuser:appuser /app
USER appuser
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]
Why these choices:
- •
python:3.12-sliminstead ofalpine-- avoids musl compatibility issues with binary wheels - •
--no-cache-dir-- prevents pip cache from bloating the image - •
--prefix=/install-- isolates installed packages for clean COPY - •
libpq5at runtime,libpq-devonly at build -- minimizes runtime dependencies - •
curlin runtime -- needed for HEALTHCHECK command
React Frontend Dockerfile
See references/react-dockerfile-template for the complete template.
Key decisions for React:
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --ignore-scripts
COPY . .
RUN npm run build
# Stage 2: Serve with Nginx
FROM nginx:alpine AS runtime
COPY --from=builder /app/build /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
RUN addgroup -g 1001 -S appgroup && \
adduser -S appuser -u 1001 -G appgroup && \
chown -R appuser:appgroup /var/cache/nginx /var/log/nginx /etc/nginx/conf.d && \
touch /var/run/nginx.pid && chown appuser:appgroup /var/run/nginx.pid
USER appuser
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD wget -q --spider http://localhost:8080/ || exit 1
CMD ["nginx", "-g", "daemon off;"]
Why these choices:
- •
node:20-alpinefor build -- smallest Node image, only needed at build time - •
nginx:alpinefor serving -- ~7MB base, production-grade static file server - •
npm ci-- deterministic installs from lockfile, faster thannpm install - •
--ignore-scripts-- security measure, prevents running arbitrary scripts during install - •Final image has NO Node.js runtime -- only static files + Nginx
Base Image Selection Guide
| Use Case | Base Image | Size | Notes |
|---|---|---|---|
| Python backend | python:3.12-slim | ~150MB | Best compatibility with binary wheels |
| Python backend (minimal) | python:3.12-alpine | ~50MB | May need musl workarounds for some packages |
| React build stage | node:20-alpine | ~130MB | Only used during build |
| React runtime | nginx:alpine | ~7MB | Production static file serving |
| Utility/scripts | alpine:3.19 | ~5MB | For helper containers |
Rules:
- •Never use
latesttag -- always pin major.minor version - •Prefer
-slimvariants for Python (avoids musl issues) - •Prefer
-alpinevariants for Node.js and Nginx (smaller images) - •Update base images monthly for security patches
Layer Optimization
Docker caches layers. Order instructions from least-changing to most-changing.
Optimal layer order:
# 1. Base image (changes rarely)
FROM python:3.12-slim
# 2. System dependencies (changes monthly)
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq5 curl && rm -rf /var/lib/apt/lists/*
# 3. Create user (changes never)
RUN groupadd -r appuser && useradd -r -g appuser appuser
# 4. Python dependencies (changes weekly)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 5. Application code (changes every commit)
COPY src/ ./src/
# 6. Runtime config (changes rarely)
USER appuser
EXPOSE 8000
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]
Common mistakes to avoid:
# BAD: Copying everything before installing dependencies (busts cache) COPY . . RUN pip install -r requirements.txt # GOOD: Copy only requirements first, then install, then copy code COPY requirements.txt . RUN pip install -r requirements.txt COPY src/ ./src/
Minimize layers:
# BAD: Multiple RUN commands create multiple layers
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
# GOOD: Single RUN command, single layer, clean up in same layer
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
&& rm -rf /var/lib/apt/lists/*
.dockerignore
Always include a .dockerignore to prevent unnecessary files from entering the build context.
# Version control .git .gitignore # Python __pycache__ *.pyc *.pyo .pytest_cache .mypy_cache .ruff_cache *.egg-info dist/ build/ .venv/ venv/ # Node node_modules/ npm-debug.log* .next/ coverage/ # IDE .vscode/ .idea/ *.swp *.swo # Docker Dockerfile* docker-compose* .dockerignore # Environment files .env .env.* !.env.example # Documentation *.md docs/ LICENSE # CI/CD .github/ .gitlab-ci.yml # OS .DS_Store Thumbs.db
Impact of .dockerignore:
- •Without it: Build context may be 500MB+ (node_modules, .git)
- •With it: Build context typically 5-20MB
- •Faster builds, no risk of leaking secrets from
.envfiles
Security Hardening
Non-Root User
Never run containers as root in production.
# Create a dedicated user with no shell and no home directory
RUN groupadd -r appuser && \
useradd -r -g appuser -d /app -s /sbin/nologin appuser
# Set ownership of application files
COPY --chown=appuser:appuser src/ ./src/
# Switch to non-root user
USER appuser
Security Scanning with Trivy
Scan images for vulnerabilities before deployment.
# Install Trivy curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh # Scan image for vulnerabilities trivy image --severity HIGH,CRITICAL app-backend:latest # Scan and fail if HIGH/CRITICAL vulnerabilities found trivy image --exit-code 1 --severity HIGH,CRITICAL app-backend:latest # Generate JSON report trivy image --format json --output trivy-report.json app-backend:latest # Scan Dockerfile for misconfigurations trivy config Dockerfile
Integrate into CI/CD:
- name: Trivy vulnerability scan
uses: aquasecurity/trivy-action@master
with:
image-ref: 'app-backend:${{ github.sha }}'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
exit-code: '1'
Additional Security Measures
# Read-only filesystem where possible # (set at runtime with docker run --read-only) # No new privileges # (set at runtime with docker run --security-opt=no-new-privileges) # Drop all capabilities, add only what is needed # (set at runtime with docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE) # Use COPY instead of ADD (ADD can auto-extract tarballs, security risk) COPY requirements.txt . # GOOD # ADD requirements.txt . # AVOID unless you need tar extraction
Docker Compose for Local Development
See references/docker-compose-template.yml for the full template.
Architecture:
┌────────────┐ ┌────────────┐
│ Frontend │ │ Backend │
│ React:3000 │───>│ FastAPI:8000│
└────────────┘ └─────┬──────┘
│
┌──────┴──────┐
│ │
┌────┴────┐ ┌────┴────┐
│PostgreSQL│ │ Redis │
│ :5432 │ │ :6379 │
└─────────┘ └─────────┘
Key Compose features for development:
services:
backend:
build:
context: .
dockerfile: Dockerfile.backend
target: builder # Use builder stage for development (has dev tools)
volumes:
- ./src:/app/src # Hot reload
environment:
- DEBUG=true
- DATABASE_URL=postgresql+asyncpg://postgres:postgres@db:5432/app_dev
depends_on:
db:
condition: service_healthy
ports:
- "8000:8000"
frontend:
build:
context: ./frontend
target: builder
volumes:
- ./frontend/src:/app/src # Hot reload
ports:
- "3000:3000"
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
volumes:
- pgdata:/var/lib/postgresql/data
redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
volumes:
pgdata:
Essential Compose patterns:
- •Use
depends_onwithcondition: service_healthyfor startup ordering - •Mount source code as volumes for hot reloading in development
- •Use named volumes for database persistence
- •Set
target: builderto use the build stage with dev dependencies - •Define health checks for all infrastructure services
Image Tagging Strategy
Use a consistent tagging strategy across all environments.
Tag format:
registry.example.com/app-backend:<tag>
Tagging rules:
| Tag | When | Example | Purpose |
|---|---|---|---|
git-<sha> | Every build | git-a1b2c3d | Immutable reference to exact code |
branch-<name> | Every push | branch-main | Latest from branch (mutable) |
v<semver> | Release | v1.2.3 | Semantic version release |
latest | Production deploy | latest | Current production (mutable) |
staging | Staging deploy | staging | Current staging (mutable) |
Implementation:
# Build with git SHA tag (immutable)
GIT_SHA=$(git rev-parse --short HEAD)
docker build -t "app-backend:git-${GIT_SHA}" .
# Tag for the branch
BRANCH=$(git rev-parse --abbrev-ref HEAD)
docker tag "app-backend:git-${GIT_SHA}" "app-backend:branch-${BRANCH}"
# Tag for release
docker tag "app-backend:git-${GIT_SHA}" "app-backend:v1.2.3"
# Tag as latest for production
docker tag "app-backend:git-${GIT_SHA}" "app-backend:latest"
Rules:
- •Always tag with git SHA -- this is the immutable, traceable reference
- •Never deploy using
latesttag -- always use the SHA tag - •Use
latestonly as a convenience alias after a successful production deploy - •Include build metadata in image labels
# Add metadata labels
LABEL org.opencontainers.image.source="https://github.com/org/repo"
LABEL org.opencontainers.image.revision="${GIT_SHA}"
LABEL org.opencontainers.image.created="${BUILD_DATE}"
Quick Reference
# Build images
docker build -t app-backend:$(git rev-parse --short HEAD) -f Dockerfile.backend .
docker build -t app-frontend:$(git rev-parse --short HEAD) -f Dockerfile.frontend .
# Start local development
docker compose up -d && docker compose logs -f backend
# Scan for vulnerabilities
trivy image --severity HIGH,CRITICAL app-backend:latest
# Check image sizes
docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}" | grep app-
# Clean up
docker image prune -f