--- name: dockerfile-best-practices description: "Create and optimize Dockerfiles with BuildKit, multi-stage builds, advanced caching, and security. Use when: (1) Creating new Dockerfile, (2) Optimizing existing Dockerfile, (3) Reducing image size, (4) Improving security, (5) Using Python with uv, (6) Resolving cache or slow build issues, (7) Setting up CI/CD builds" license: MIT compatibility: "Claude Code, Claude.ai" allowed-tools: "Read Grep Glob Bash Write Edit" metadata: version: "1.0.1" category: development tags: - docker - dockerfile - containers - buildkit - optimization - security - devops - ci-cd triggers: - "create dockerfile" - "optimize dockerfile" - "docker best practices" - "reduce image size" - "docker security" - "multi-stage build" - "buildkit" surfaces: - claude-code - claude-ai author: name: obeone url: https://github.com/obeone --- # Dockerfile Best Practices Comprehensive guide for creating optimized, secure, and fast Docker images using modern BuildKit features. ## Quick Start Workflow When working with Dockerfiles, follow this decision tree: 1. **Choose your language/framework** → Use appropriate template below 2. **Apply security hardening** → Non-root user, pin versions, secrets management 3. **Optimize for cache** → Separate deps from code, use cache mounts 4. **Multi-stage if needed** → Separate build from runtime for compiled languages 5. **Review with analyzer** → Run `scripts/analyze_dockerfile.py` ## Essential Rules (Always Apply) 1. **Start with BuildKit syntax:** ```dockerfile # syntax=docker/dockerfile:1 ``` 2. **Pin runtime versions, not OS versions:** ```dockerfile # ✅ GOOD - Pin runtime, let OS update FROM python:3.12-slim # ❌ BAD - Pins OS version (bookworm), prevents security updates FROM python:3.12-slim-bookworm ``` **Why?** OS versions update with security patches. Pin runtime (python:3.12) for reproducible behavior. 3. **Create `.dockerignore`:** Use template from `assets/dockerignore-template` 4. **Use cache mounts for package managers:** ```dockerfile RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt ``` 5. **APT cache setup (before any apt operations):** ```dockerfile RUN rm -f /etc/apt/apt.conf.d/docker-clean; \ echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache ``` 6. **Never use ARG/ENV for secrets:** ```dockerfile RUN --mount=type=secret,id=api_key curl -H "Authorization: $(cat /run/secrets/api_key)" https://api.example.com ``` 7. **Use non-root user with safe UID/GID:** ```dockerfile # Let system auto-assign (simple) RUN groupadd -r app && useradd -r -g app app # Or use UID/GID >10000 for consistency across environments RUN groupadd -r -g 10001 app && useradd -r -u 10001 -g app app ``` ## Language-Specific Templates ### Python (with uv - Recommended) For detailed Python/uv integration, see `references/uv_integration.md`. ```dockerfile # syntax=docker/dockerfile:1 FROM python:3.12-slim COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ WORKDIR /app # Install dependencies (separate layer for caching) RUN --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=uv.lock,target=uv.lock \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ uv sync --locked --no-install-project # Copy and install project COPY . . RUN --mount=type=cache,target=/root/.cache/uv \ uv sync --locked # Security: non-root user with UID/GID >10000 RUN groupadd -r -g 10001 app && \ useradd -r -u 10001 -g app app && \ chown -R app:app /app USER app ENV PATH="/app/.venv/bin:$PATH" CMD ["python", "-m", "myapp"] ``` ### Node.js ```dockerfile # syntax=docker/dockerfile:1 FROM node:20-alpine WORKDIR /app # Install dependencies COPY package.json yarn.lock ./ RUN --mount=type=cache,target=/root/.yarn \ yarn install --frozen-lockfile --production # Copy source COPY . . # Security: non-root user with UID/GID >10000 RUN addgroup -g 10001 app && \ adduser -u 10001 -G app -S app && \ chown -R app:app /app USER app EXPOSE 3000 CMD ["node", "index.js"] ``` ### Go (Multi-stage) ```dockerfile # syntax=docker/dockerfile:1 # Build stage FROM golang:1-alpine AS builder WORKDIR /app COPY go.mod go.sum ./ RUN --mount=type=cache,target=/go/pkg/mod \ go mod download COPY . . RUN --mount=type=cache,target=/go/pkg/mod \ --mount=type=cache,target=/root/.cache/go-build \ CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o main # Runtime stage FROM alpine:3 RUN addgroup -g 10001 app && adduser -u 10001 -G app -S app USER app COPY --from=builder /app/main /main ENTRYPOINT ["/main"] ``` ### Debian-based (with APT cache) ```dockerfile # syntax=docker/dockerfile:1 FROM debian:stable-slim # Configure APT for cache reuse RUN rm -f /etc/apt/apt.conf.d/docker-clean; \ echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache WORKDIR /app # Install dependencies with cache mount RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \ apt-get update && \ apt-get install -y --no-install-recommends curl ca-certificates # Copy application COPY . . # Security: non-root user with UID/GID >10000 RUN groupadd -r -g 10001 app && \ useradd -r -u 10001 -g app app && \ chown -R app:app /app USER app CMD ["./app"] ``` ### PHP (with Composer) ```dockerfile # syntax=docker/dockerfile:1 FROM php:8-fpm-alpine WORKDIR /app # Install Composer COPY --from=composer:latest /usr/bin/composer /usr/bin/composer # Install dependencies COPY composer.json composer.lock ./ RUN --mount=type=cache,target=/tmp/cache \ composer install --no-dev --optimize-autoloader --no-scripts # Copy source COPY . . RUN composer dump-autoload --optimize # Security: non-root user with UID/GID >10000 RUN addgroup -g 10001 app && \ adduser -u 10001 -G app -S app && \ chown -R app:app /app USER app EXPOSE 9000 CMD ["php-fpm"] ``` ## Security Checklist - [ ] Use specific version tags for base images - [ ] Use minimal base image (alpine, slim, distroless) - [ ] Create and use non-root user - [ ] Never expose secrets via ARG/ENV - [ ] Use `RUN --mount=type=secret` for sensitive data - [ ] Add HEALTHCHECK instruction - [ ] Scan image for vulnerabilities (`docker scan`) ## Performance Checklist - [ ] Add BuildKit syntax directive - [ ] Create comprehensive `.dockerignore` - [ ] Order instructions: manifests → deps → code - [ ] Use `--mount=type=cache` for all package managers - [ ] Implement multi-stage builds for compiled languages - [ ] Chain RUN commands with `&&` - [ ] Clean up in same RUN instruction ## Size Optimization **Quick wins:** 1. Use alpine or slim variants 2. Multi-stage builds 3. Remove unnecessary files in same layer 4. Use `--no-install-recommends` with apt 5. Consider distroless for runtime ## Common Patterns ### Intermediate Layers (Faster Rebuilds) Separate dependency installation from code copy: ```dockerfile # Install deps first (rarely changes) COPY package.json package-lock.json ./ RUN npm ci --production # Copy code (changes frequently) COPY . . ``` ### Remote Cache (CI/CD) ```bash docker buildx build \ --cache-from=type=registry,ref=myregistry.com/app:cache \ --cache-to=type=registry,ref=myregistry.com/app:cache,mode=max \ --push \ -t myregistry.com/app:latest . ``` ### Secret Management ```bash # Build with secret docker buildx build --secret id=api_key,src=./key.txt . # Or from environment docker buildx build --secret id=api_key,env=API_KEY . ``` ## Tools and Scripts ### Analyze Dockerfile ```bash python scripts/analyze_dockerfile.py ./Dockerfile ``` Detects common anti-patterns: - Missing BuildKit syntax - Using :latest tags - ADD instead of COPY - Missing cache mounts - Secrets in ARG/ENV - Missing non-root USER - apt-get without cleanup ### Generate .dockerignore ```bash cp assets/dockerignore-template .dockerignore ``` ## Docker Compose Best Practices For multi-container applications, follow modern Compose practices: ### Key Rules 1. **Don't use `version:` field** - Deprecated since Compose V2 ```yaml # ✅ GOOD - No version field services: app: image: myapp:1.0.0 ``` 2. **Never use `container_name:`** - Prevents scaling and parallel environments ```yaml # ✅ GOOD - Let Compose generate names services: app: image: myapp:1.0.0 # No container_name - allows scaling with --scale # Use project names for environment isolation: # docker compose -p myapp-dev up # docker compose -p myapp-test up ``` 3. **Use specific image tags** - Not `:latest` 4. **Define health checks** - For service dependencies 5. **Set resource limits** - Prevent resource exhaustion For complete Compose guide, see `references/compose_best_practices.md`. ## Reference Documentation For detailed information, consult these references: 1. **`references/optimization_guide.md`** - Complete optimization guide with BuildKit, caching, multi-stage builds 2. **`references/best_practices.md`** - Checklist of all best practices with impact levels 3. **`references/examples.md`** - Real-world before/after optimization examples 4. **`references/uv_integration.md`** - Python with uv package manager (recommended for Python) 5. **`references/compose_best_practices.md`** - Docker Compose modern practices (no version:, no container_name:) ## Common Issues and Solutions ### Slow builds - Add cache mounts: `RUN --mount=type=cache,target=...` - Optimize layer ordering: deps before code - Use remote cache in CI/CD ### Large images - Use multi-stage builds - Switch to alpine/slim base images - Clean up in same RUN instruction - Remove dev dependencies ### Security concerns - Pin versions with SHA256 - Use non-root USER - Never use ARG/ENV for secrets - Use BuildKit secret mounts - Scan with `docker scan` ### Cache invalidation - Separate dependency installation from code - Use `--mount=type=bind` for manifests - Order instructions correctly ## Examples by Use Case ### CLI Tool ```dockerfile # syntax=docker/dockerfile:1 FROM python:3.12-alpine COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/ WORKDIR /app RUN --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=uv.lock,target=uv.lock \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ uv sync --locked --no-install-project COPY . . RUN --mount=type=cache,target=/root/.cache/uv uv sync --locked ENV PATH="/app/.venv/bin:$PATH" ENTRYPOINT ["mytool"] ``` ### Web API ```dockerfile # syntax=docker/dockerfile:1 FROM node:20-alpine WORKDIR /app COPY package.json yarn.lock ./ RUN --mount=type=cache,target=/root/.yarn yarn install --frozen-lockfile --production COPY . . RUN addgroup -g 10001 app && \ adduser -u 10001 -G app -S app && \ chown -R app:app /app USER app EXPOSE 3000 HEALTHCHECK --interval=30s CMD wget -qO- http://localhost:3000/health || exit 1 CMD ["node", "server.js"] ``` ### Static Site ```dockerfile # syntax=docker/dockerfile:1 FROM node:20-alpine AS builder WORKDIR /app COPY package.json yarn.lock ./ RUN --mount=type=cache,target=/root/.yarn yarn install --frozen-lockfile COPY . . RUN yarn build FROM nginx:alpine COPY --from=builder /app/dist /usr/share/nginx/html EXPOSE 80 ``` ## Build Commands Reference ```bash # Basic build docker buildx build -t myapp:latest . # With cache from registry docker buildx build \ --cache-from=type=registry,ref=registry.com/myapp:cache \ --cache-to=type=registry,ref=registry.com/myapp:cache,mode=max \ -t myapp:latest . # With secrets docker buildx build \ --secret id=api_key,src=./key.txt \ -t myapp:latest . # Multi-platform docker buildx build \ --platform linux/amd64,linux/arm64 \ -t myapp:latest --push . # Target specific stage docker buildx build --target builder -t myapp:builder . ``` ## Next Steps After creating your Dockerfile: 1. Run analyzer: `python scripts/analyze_dockerfile.py Dockerfile` 2. Test locally: `docker buildx build -t test .` 3. Check size: `docker images test` 4. Scan for vulnerabilities: `docker scout cves test` 5. Profile build: `docker buildx build --progress=plain . 2>&1 | less`