mcpField
D
docker-best-practices

docker-best-practices

github.com/hieutrtrCloud & DevOps888 installsUpdated 38w ago
Repository

About docker-best-practices

Docker Best Practices is a guide for developers building Docker images for Python backends and React frontends. It covers image construction, local development with Compose, security hardening, scanning, tagging, and troubleshooting.

Key features

  1. 01Multi-stage builds and image optimization
  2. 02Python and React image guidance
  3. 03Docker Compose local development patterns
  4. 04Container hardening and Trivy scans
  5. 05Image tagging strategy

Use cases

  1. 01Write a Dockerfile for a Python backend
  2. 02Reduce image size and improve build caching
  3. 03Configure a local Compose development environment
  4. 04Scan image vulnerabilities and plan tags

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/hieutrtr/ai1-skills --skill docker-best-practices

After installing, type/docker-best-practicesin Claude Code to confirm it loaded.

Bundled files

References and scripts installed along with SKILL.md.

references/
docker-compose-template.yml
python-dockerfile-template
react-dockerfile-template

Details

From the project README

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 CaseBase ImageSizeNotes
Python backendpython:3.12-slim~150MBBest compatibility with binary wheels
Python backend (minimal)python:3.12-alpine~50MBMay need musl workarounds for some packages
React build stagenode:20-alpine~130MBOnly used during build
React runtimenginx:alpine~7MBProduction static file serving
Utility/scriptsalpine:3.19~5MBFor 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:

TagWhenExamplePurpose
git-<sha>Every buildgit-a1b2c3dImmutable reference to exact code
branch-<name>Every pushbranch-mainLatest from branch (mutable)
v<semver>Releasev1.2.3Semantic version release
latestProduction deploylatestCurrent production (mutable)
stagingStaging deploystagingCurrent 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

Comments

Markdown supported