NYC
skills/hieutrtr/ai1-skills/docker-best-practices

docker-best-practices

SKILL.md

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-expert or react-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-slim instead of alpine -- 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
  • libpq5 at runtime, libpq-dev only at build -- minimizes runtime dependencies
  • curl in 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-alpine for build -- smallest Node image, only needed at build time
  • nginx:alpine for serving -- ~7MB base, production-grade static file server
  • npm ci -- deterministic installs from lockfile, faster than npm 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:

  1. Never use latest tag -- always pin major.minor version
  2. Prefer -slim variants for Python (avoids musl issues)
  3. Prefer -alpine variants for Node.js and Nginx (smaller images)
  4. 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 .env files

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_on with condition: service_healthy for startup ordering
  • Mount source code as volumes for hot reloading in development
  • Use named volumes for database persistence
  • Set target: builder to 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:

  1. Always tag with git SHA -- this is the immutable, traceable reference
  2. Never deploy using latest tag -- always use the SHA tag
  3. Use latest only as a convenience alias after a successful production deploy
  4. 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
Weekly Installs
11
First Seen
Feb 4, 2026
Installed on
opencode10
gemini-cli9
github-copilot9
codex9
claude-code8
amp8