
Writing Dockerfiles
- 62 installs
- 426 repo stars
- Updated December 11, 2025
- ancoleman/ai-design-components
writing-dockerfiles is a skill that writes optimized, secure, multi-stage Dockerfiles with language-specific patterns and BuildKit features.
About
A skill that writes optimized, secure, multi-stage Dockerfiles with language-specific patterns for Python, Node.js, Go, and Rust. It covers distroless images, BuildKit cache and secret mounts, layer ordering, and security hardening. A developer uses it when containerizing applications, optimizing existing Dockerfiles, or reducing image sizes.
- Writes multi-stage Dockerfiles with language-specific patterns for Python, Node, Go, Rust
- Covers distroless base images, BuildKit cache and secret mounts, and layer optimization
- Includes security hardening: non-root users, secret mounts, and vulnerability scanning
Writing Dockerfiles by the numbers
- 62 all-time installs (skills.sh)
- Ranked #646 of 1,435 DevOps & CI/CD skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
writing-dockerfiles capabilities & compatibility
- Capabilities
- devops · security audit
- Works with
- docker
- Use cases
- devops
What writing-dockerfiles says it does
Writing optimized, secure, multi-stage Dockerfiles with language-specific patterns (Python, Node.js, Go, Rust), BuildKit features, and distroless images.
Use when containerizing applications, optimizing existing Dockerfiles, or reducing image sizes.
npx skills add https://github.com/ancoleman/ai-design-components --skill writing-dockerfilesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 62 |
|---|---|
| repo stars | ★ 426 |
| Last updated | December 11, 2025 |
| Repository | ancoleman/ai-design-components ↗ |
What it does
Write and optimize secure multi-stage Dockerfiles that reduce image size and harden the runtime.
Who is it for?
Containerizing applications and reducing image size with hardened, multi-stage Dockerfiles
Skip if: Runtime orchestration or CI pipeline design
When should I use this skill?
You are writing or optimizing a Dockerfile or need to shrink or secure a container image
By the numbers
- 4+ languages with patterns (Python, Node.js, Go, Rust, Java)
- multi-stage builds cited as 80-95% smaller images
- 5 essential security practices listed
Files
Writing Dockerfiles
Create production-grade Dockerfiles with multi-stage builds, security hardening, and language-specific optimizations.
When to Use This Skill
Invoke when:
- "Write a Dockerfile for [Python/Node.js/Go/Rust] application"
- "Optimize this Dockerfile to reduce image size"
- "Use multi-stage build for..."
- "Secure Dockerfile with non-root user"
- "Use distroless base image"
- "Add BuildKit cache mounts"
- "Prevent secrets from leaking in Docker layers"
Quick Decision Framework
Ask three questions to determine the approach:
1. What language?
- Python → See
references/python-dockerfiles.md - Node.js → See
references/nodejs-dockerfiles.md - Go → See
references/go-dockerfiles.md - Rust → See
references/rust-dockerfiles.md - Java → See
references/java-dockerfiles.md
2. Is security critical?
- YES → Use distroless runtime images (see
references/security-hardening.md) - NO → Use slim/alpine base images
3. Is image size critical?
- YES (<50MB) → Multi-stage + distroless + static linking
- NO (<500MB) → Multi-stage + slim base images
Core Concepts
Multi-Stage Builds
Separate build environment from runtime environment to minimize final image size.
Pattern:
# Stage 1: Build
FROM build-image AS builder
RUN compile application
# Stage 2: Runtime
FROM minimal-runtime-image
COPY --from=builder /app/binary /app/
CMD ["/app/binary"]Benefits:
- 80-95% smaller images (excludes build tools)
- Improved security (no compilers in production)
- Faster deployments
- Better layer caching
Base Image Selection
Decision matrix:
| Language | Build Stage | Runtime Stage | Final Size |
|---|---|---|---|
| Go (static) | golang:1.22-alpine | gcr.io/distroless/static-debian12 | 10-30MB |
| Rust (static) | rust:1.75-alpine | scratch | 5-15MB |
| Python | python:3.12-slim | python:3.12-slim | 200-400MB |
| Node.js | node:20-alpine | node:20-alpine | 150-300MB |
| Java | maven:3.9-eclipse-temurin-21 | eclipse-temurin:21-jre-alpine | 200-350MB |
Distroless images (Google-maintained):
gcr.io/distroless/static-debian12→ Static binaries (2MB)gcr.io/distroless/base-debian12→ Dynamic binaries with libc (20MB)gcr.io/distroless/python3-debian12→ Python runtime (60MB)gcr.io/distroless/nodejs20-debian12→ Node.js runtime (150MB)
See references/base-image-selection.md for complete comparison.
BuildKit Features
Enable BuildKit for advanced caching and security:
export DOCKER_BUILDKIT=1
docker build .
# OR
docker buildx build .Key features:
--mount=type=cache→ Persistent package manager caches--mount=type=secret→ Inject secrets without storing in layers--mount=type=ssh→ SSH agent forwarding for private repos- Parallel stage execution
- Improved layer caching
See references/buildkit-features.md for detailed patterns.
Layer Optimization
Order Dockerfile instructions from least to most frequently changing:
# 1. Base image (rarely changes)
FROM python:3.12-slim
# 2. System packages (rarely changes)
RUN apt-get update && apt-get install -y build-essential
# 3. Dependencies manifest (changes occasionally)
COPY requirements.txt .
RUN pip install -r requirements.txt
# 4. Application code (changes frequently)
COPY . .
# 5. Runtime configuration (rarely changes)
CMD ["python", "app.py"]BuildKit cache mounts:
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txtCache persists across builds, eliminating redundant downloads.
Security Hardening
Essential security practices:
1. Non-root users
# Debian/Ubuntu
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
USER appuser
# Alpine
RUN adduser -D -u 1000 appuser && chown -R appuser:appuser /app
USER appuser
# Distroless (built-in)
USER nonroot:nonroot2. Secret management
# ❌ NEVER: Secret in layer history
RUN git clone https://${GITHUB_TOKEN}@github.com/private/repo.git
# ✅ ALWAYS: BuildKit secret mount
RUN --mount=type=secret,id=github_token \
TOKEN=$(cat /run/secrets/github_token) && \
git clone https://${TOKEN}@github.com/private/repo.gitBuild with:
docker buildx build --secret id=github_token,src=./token.txt .3. Vulnerability scanning
# Trivy (recommended)
trivy image myimage:latest
# Docker Scout
docker scout cves myimage:latest4. Health checks
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1See references/security-hardening.md for comprehensive hardening patterns.
.dockerignore Configuration
Create .dockerignore to exclude unnecessary files:
# Version control
.git
.gitignore
# CI/CD
.github
.gitlab-ci.yml
# IDE
.vscode
.idea
# Testing
tests/
coverage/
**/*_test.go
**/*.test.js
# Build artifacts
node_modules/
dist/
build/
target/
__pycache__/
# Environment
.env
.env.local
*.logReduces build context size and prevents leaking secrets.
Language-Specific Patterns
Python Quick Reference
Three approaches:
1. pip (simple) → Single-stage, requirements.txt 2. poetry (production) → Multi-stage, virtual environment 3. uv (fastest) → 10-100x faster than pip
Example: Poetry multi-stage
FROM python:3.12-slim AS builder
RUN --mount=type=cache,target=/root/.cache/pip \
pip install poetry==1.7.1
COPY pyproject.toml poetry.lock ./
RUN poetry export -f requirements.txt --output requirements.txt
RUN --mount=type=cache,target=/root/.cache/pip \
python -m venv /opt/venv && \
/opt/venv/bin/pip install -r requirements.txt
FROM python:3.12-slim
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
USER 1000:1000
CMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0"]See references/python-dockerfiles.md for complete patterns and examples/python-fastapi.Dockerfile.
Node.js Quick Reference
Key patterns:
- Use
npm ci(notnpm install) for reproducible builds - Multi-stage: Build stage → Production dependencies only
- Built-in
nodeuser (UID 1000) - Alpine variant smallest (~180MB vs 1GB)
Example: Express multi-stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci
COPY . .
RUN npm run build
RUN npm prune --omit=dev
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
USER node
CMD ["node", "dist/index.js"]See references/nodejs-dockerfiles.md for npm/pnpm/yarn patterns and examples/nodejs-express.Dockerfile.
Go Quick Reference
Smallest possible images:
- Static binary (CGO_ENABLED=0) + distroless = 10-30MB
- Strip symbols with
-ldflags="-s -w" - Cache both
/go/pkg/modand build cache
Example: Distroless static
FROM golang:1.22-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="-s -w" -o main .
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/main /app/main
USER nonroot:nonroot
ENTRYPOINT ["/app/main"]See references/go-dockerfiles.md and examples/go-microservice.Dockerfile.
Rust Quick Reference
Ultra-small static binaries:
- musl static linking → No libc dependencies
- scratch base image (0 bytes overhead)
- Final image: 5-15MB
Example: Scratch base
FROM rust:1.75-alpine AS builder
RUN apk add --no-cache musl-dev
WORKDIR /app
# Cache dependencies
COPY Cargo.toml Cargo.lock ./
RUN --mount=type=cache,target=/usr/local/cargo/registry \
mkdir src && echo "fn main() {}" > src/main.rs && \
cargo build --release --target x86_64-unknown-linux-musl && \
rm -rf src
# Build application
COPY src ./src
RUN --mount=type=cache,target=/usr/local/cargo/registry \
cargo build --release --target x86_64-unknown-linux-musl
FROM scratch
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/app /app
USER 1000:1000
ENTRYPOINT ["/app"]See references/rust-dockerfiles.md and examples/rust-actix.Dockerfile.
Package Manager Cache Mounts
BuildKit cache mount locations:
| Language | Package Manager | Cache Mount Target |
|---|---|---|
| Python | pip | --mount=type=cache,target=/root/.cache/pip |
| Python | poetry | --mount=type=cache,target=/root/.cache/pypoetry |
| Python | uv | --mount=type=cache,target=/root/.cache/uv |
| Node.js | npm | --mount=type=cache,target=/root/.npm |
| Node.js | pnpm | --mount=type=cache,target=/root/.local/share/pnpm/store |
| Go | go mod | --mount=type=cache,target=/go/pkg/mod |
| Rust | cargo | --mount=type=cache,target=/usr/local/cargo/registry |
Persistent caches eliminate redundant package downloads across builds.
Validation and Testing
Validate Dockerfile quality:
# Lint Dockerfile
python scripts/validate_dockerfile.py Dockerfile
# Scan for vulnerabilities
trivy image myimage:latest
# Analyze image size
docker images myimage:latest
docker history myimage:latestCompare optimization results:
# Before optimization
docker build -t myapp:before .
# After optimization
docker build -t myapp:after .
# Compare
bash scripts/analyze_image_size.sh myapp:before myapp:afterSee scripts/validate_dockerfile.py for automated Dockerfile linting.
Integration with Related Skills
Upstream (provide input):
testing-strategies→ Test application before containerizingsecurity-hardening→ Application-level security before Docker layer
Downstream (consume Dockerfiles):
building-ci-pipelines→ Build and push Docker images in CIkubernetes-operations→ Deploy containers to K8s clustersinfrastructure-as-code→ Deploy containers with Terraform/Pulumi
Parallel (related context):
secret-management→ Inject runtime secrets (K8s secrets, vaults)observability→ Container logging and metrics collection
Common Patterns Quick Reference
1. Static binary (Go/Rust) → Smallest image
- Build: Language-specific builder image
- Runtime:
gcr.io/distroless/static-debian12orscratch - Size: 5-30MB
2. Interpreted language (Python/Node.js) → Production-optimized
- Build: Install dependencies, build artifacts
- Runtime: Same base, production dependencies only
- Size: 150-400MB
3. JVM (Java) → Optimized runtime
- Build: Maven/Gradle with full JDK
- Runtime: JRE-only image (alpine variant)
- Size: 200-350MB
4. Security-critical → Maximum hardening
- Base: Distroless images
- User: Non-root (nonroot:nonroot)
- Secrets: BuildKit secret mounts
- Scan: Trivy/Docker Scout in CI
5. Development → Fast iteration
- Base: Full language image (not slim)
- Volumes: Mount source code
- Hot reload: Language-specific tools
- Not covered in this skill (see Docker Compose docs)
Anti-Patterns to Avoid
❌ Never:
- Use
latesttags (unpredictable builds) - Run as root in production
- Store secrets in ENV vars or layers
- Install unnecessary packages
- Combine unrelated RUN commands (breaks caching)
- Skip .dockerignore (bloated build context)
✅ Always:
- Pin exact image versions (
python:3.12.1-slim, notpython:3) - Create and use non-root user
- Use BuildKit secret mounts for credentials
- Minimize layers and image size
- Order commands from least to most frequently changing
- Create .dockerignore file
Additional Resources
Base image registries:
- Google Distroless:
gcr.io/distroless/* - Docker Hub Official:
python:*,node:*,golang:* - Red Hat UBI:
registry.access.redhat.com/ubi9/*
Vulnerability scanners:
- Trivy (recommended):
trivy image myimage:latest - Docker Scout:
docker scout cves myimage:latest - Grype:
grype myimage:latest
Reference documentation:
references/base-image-selection.md→ Complete base image comparisonreferences/buildkit-features.md→ Advanced BuildKit patternsreferences/security-hardening.md→ Comprehensive security guide- Language-specific references in
references/directory - Working examples in
examples/directory
# Production-Ready Go Microservice Dockerfile
#
# This example demonstrates:
# - Multi-stage build with distroless runtime
# - Static binary compilation (CGO_ENABLED=0)
# - BuildKit cache mounts for Go modules and build cache
# - Binary stripping for minimal size
# - Non-root user (nonroot in distroless)
# - Distroless static base (smallest possible)
#
# Expected image size: 10-30MB
#
# Build: docker build -f go-microservice.Dockerfile -t go-api:latest .
# Run: docker run -p 8080:8080 go-api:latest
# syntax=docker/dockerfile:1
FROM golang:1.22-alpine AS builder
WORKDIR /app
# Download dependencies first (cached layer)
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
# Verify dependencies
RUN --mount=type=cache,target=/go/pkg/mod \
go mod verify
# Copy source code
COPY . .
# Build static binary with optimizations
# - CGO_ENABLED=0: Static binary, no libc dependency
# - -ldflags="-s -w": Strip debug symbols (30-50% smaller)
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -ldflags="-s -w" -o /app/main .
# Runtime stage: distroless static (minimal base)
FROM gcr.io/distroless/static-debian12
# Copy static binary from builder
COPY --from=builder /app/main /app/main
# Use built-in nonroot user (UID 65532)
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/app/main"]
# Production-Ready Express.js Application Dockerfile
#
# This example demonstrates:
# - Multi-stage build with npm
# - BuildKit cache mounts for npm packages
# - TypeScript compilation
# - Production dependencies only in runtime
# - Non-root user (built-in node user)
# - Health check implementation
#
# Expected image size: 220-300MB
#
# Build: docker build -f nodejs-express.Dockerfile -t express-app:latest .
# Run: docker run -p 3000:3000 express-app:latest
# syntax=docker/dockerfile:1
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package files
COPY package.json package-lock.json ./
# Install all dependencies (including devDependencies for building)
RUN --mount=type=cache,target=/root/.npm \
npm ci
# Copy source code
COPY . .
# Build TypeScript to JavaScript
RUN npm run build
# Prune development dependencies
RUN npm prune --omit=dev
# Runtime stage
FROM node:20-alpine
WORKDIR /app
# Copy production node_modules and built code
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
# Use built-in node user (UID 1000)
USER node
# Production environment
ENV NODE_ENV=production
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
EXPOSE 3000
CMD ["node", "dist/index.js"]
# Production-Ready FastAPI Application Dockerfile
#
# This example demonstrates:
# - Multi-stage build with uv (fastest Python package manager)
# - BuildKit cache mounts for dependencies
# - Non-root user for security
# - Virtual environment isolation
# - Health check implementation
#
# Expected image size: 280-380MB
#
# Build: docker build -f python-fastapi.Dockerfile -t fastapi-app:latest .
# Run: docker run -p 8000:8000 fastapi-app:latest
# syntax=docker/dockerfile:1
FROM python:3.12-slim AS builder
# Install uv (10-100x faster than pip)
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
WORKDIR /app
# Copy dependency files
COPY pyproject.toml uv.lock ./
# Install dependencies with cache mount
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev
# Copy application code
COPY . .
# Runtime stage
FROM python:3.12-slim
WORKDIR /app
# Copy application and virtual environment from builder
COPY --from=builder /app /app
# Create non-root user
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
USER appuser
# Activate virtual environment
ENV PATH="/app/.venv/bin:$PATH"
# Python optimizations
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD python -c "import requests; requests.get('http://localhost:8000/health', timeout=2)" || exit 1
EXPOSE 8000
CMD ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
# Production-Ready Rust Actix-web Application Dockerfile
#
# This example demonstrates:
# - Multi-stage build with scratch runtime
# - musl static linking for zero dependencies
# - Dependency caching with dummy build
# - BuildKit cache mounts for cargo registry
# - Binary stripping for minimal size
# - Ultra-small final image
#
# Expected image size: 8-12MB
#
# Build: docker build -f rust-actix.Dockerfile -t actix-app:latest .
# Run: docker run -p 8080:8080 actix-app:latest
# syntax=docker/dockerfile:1
FROM rust:1.75-alpine AS builder
# Install musl build tools for static linking
RUN apk add --no-cache musl-dev
WORKDIR /app
# Cache dependencies layer (dummy build technique)
COPY Cargo.toml Cargo.lock ./
RUN --mount=type=cache,target=/usr/local/cargo/registry \
mkdir src && \
echo "fn main() {}" > src/main.rs && \
cargo build --release --target x86_64-unknown-linux-musl && \
rm -rf src
# Build actual application
COPY src ./src
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/app/target \
cargo build --release --target x86_64-unknown-linux-musl && \
strip target/x86_64-unknown-linux-musl/release/app
# Runtime stage: scratch (empty base, 0 bytes overhead)
FROM scratch
# Copy only the static binary
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/app /app
# Run as non-root (numeric UID only in scratch)
USER 1000:1000
EXPOSE 8080
ENTRYPOINT ["/app"]
skill: "writing-dockerfiles"
version: "1.0"
domain: "devops"
base_outputs:
- path: "Dockerfile"
must_contain: ["FROM", "COPY", "RUN"]
description: "Primary Dockerfile for building container images"
- path: ".dockerignore"
must_contain: [".git", "node_modules", "__pycache__"]
description: "Exclude unnecessary files from Docker build context"
conditional_outputs:
maturity:
starter:
- path: "Dockerfile"
must_contain: ["FROM", "COPY", "WORKDIR", "CMD"]
description: "Single-stage Dockerfile with basic configuration"
- path: ".dockerignore"
must_contain: [".git", ".env"]
description: "Basic .dockerignore excluding version control and secrets"
intermediate:
- path: "Dockerfile"
must_contain: ["FROM", "AS builder", "COPY --from=builder", "USER"]
description: "Multi-stage Dockerfile with non-root user"
- path: ".dockerignore"
must_contain: [".git", "node_modules", "__pycache__", "tests/"]
description: "Comprehensive .dockerignore excluding build artifacts and tests"
- path: "docker-compose.yml"
must_contain: ["services:", "build:", "ports:"]
description: "Local development environment with Docker Compose"
advanced:
- path: "Dockerfile"
must_contain: ["FROM", "AS builder", "--mount=type=cache", "USER nonroot"]
description: "Optimized multi-stage Dockerfile with BuildKit cache mounts and distroless base"
- path: "Dockerfile.dev"
must_contain: ["FROM", "COPY", "CMD"]
description: "Development Dockerfile with hot-reload and debugging tools"
- path: "docker-compose.yml"
must_contain: ["services:", "build:", "ports:", "volumes:"]
description: "Multi-service Docker Compose configuration"
- path: ".dockerignore"
must_contain: [".git", "node_modules", "__pycache__", "tests/", "coverage/"]
description: "Production-grade .dockerignore with comprehensive exclusions"
- path: "docker/Dockerfile.prod"
must_contain: ["FROM", "AS builder", "--mount=type=cache"]
description: "Production-optimized Dockerfile in dedicated directory"
backend_framework:
python:
- path: "Dockerfile"
must_contain: ["FROM python:", "pip install", "requirements.txt"]
description: "Python Dockerfile with pip dependency management"
- path: "requirements.txt"
must_contain: []
description: "Python dependencies for pip installation"
- path: ".dockerignore"
must_contain: ["__pycache__", "*.pyc", ".pytest_cache"]
description: "Python-specific .dockerignore excluding bytecode and caches"
node:
- path: "Dockerfile"
must_contain: ["FROM node:", "npm ci", "package*.json"]
description: "Node.js Dockerfile with npm dependency management"
- path: ".dockerignore"
must_contain: ["node_modules", "npm-debug.log", "dist"]
description: "Node.js-specific .dockerignore excluding modules and build artifacts"
go:
- path: "Dockerfile"
must_contain: ["FROM golang:", "go build", "CGO_ENABLED=0"]
description: "Go Dockerfile with static binary compilation"
- path: ".dockerignore"
must_contain: ["vendor/", "*.test", "coverage.out"]
description: "Go-specific .dockerignore excluding vendor and test artifacts"
rust:
- path: "Dockerfile"
must_contain: ["FROM rust:", "cargo build --release", "target"]
description: "Rust Dockerfile with cargo build"
- path: ".dockerignore"
must_contain: ["target/", "Cargo.lock"]
description: "Rust-specific .dockerignore excluding build artifacts"
java:
- path: "Dockerfile"
must_contain: ["FROM maven:", "mvn package", "FROM eclipse-temurin"]
description: "Java Dockerfile with Maven build and JRE runtime"
- path: ".dockerignore"
must_contain: ["target/", "*.class", ".mvn"]
description: "Java-specific .dockerignore excluding Maven artifacts"
frontend_framework:
react:
- path: "Dockerfile"
must_contain: ["FROM node:", "npm run build", "nginx"]
description: "React Dockerfile with build stage and nginx serving"
- path: "nginx.conf"
must_contain: ["server", "location", "root"]
description: "Nginx configuration for serving React static assets"
vue:
- path: "Dockerfile"
must_contain: ["FROM node:", "npm run build", "nginx"]
description: "Vue.js Dockerfile with build stage and nginx serving"
- path: "nginx.conf"
must_contain: ["server", "location", "root"]
description: "Nginx configuration for serving Vue static assets"
angular:
- path: "Dockerfile"
must_contain: ["FROM node:", "ng build", "nginx"]
description: "Angular Dockerfile with build stage and nginx serving"
- path: "nginx.conf"
must_contain: ["server", "location", "root"]
description: "Nginx configuration for serving Angular static assets"
scaffolding:
- path: ".dockerignore"
reason: "Essential for reducing build context size and preventing secret leaks"
- path: "Dockerfile"
reason: "Core artifact for containerizing applications"
- path: "docker-compose.yml"
reason: "Local development environment setup (intermediate+)"
- path: ".env.example"
reason: "Template for environment variables (referenced but not committed)"
metadata:
primary_blueprints: ["ci-cd", "api-first"]
contributes_to:
- "Container images for deployment"
- "Multi-stage build artifacts"
- "Optimized production images"
- "Development environment configuration"
typical_file_locations:
- "Dockerfile (project root)"
- "docker/Dockerfile.prod (production variant)"
- "docker/Dockerfile.dev (development variant)"
- ".dockerignore (project root)"
- "docker-compose.yml (local development)"
size_expectations:
python: "200-400MB (slim base)"
node: "150-300MB (alpine base)"
go: "10-30MB (distroless static)"
rust: "5-15MB (scratch base)"
java: "200-350MB (JRE alpine)"
validation_commands:
- "docker build -t test ."
- "trivy image test:latest"
- "docker scout cves test:latest"
- "python scripts/validate_dockerfile.py Dockerfile"
Base Image Selection Guide
Comprehensive guide to selecting the right base image for your Docker containers.
Table of Contents
1. Quick Decision Matrix 2. Base Image Categories 3. Language-Specific Recommendations 4. Distroless Image Variants 5. Alpine vs Debian Slim 6. Version Pinning Strategies 7. Multi-Architecture Images 8. Base Image Registries 9. Security Considerations 10. Summary Table
Quick Decision Matrix
| Language | Build Stage | Runtime Stage | Final Size | Use Case |
|---|---|---|---|---|
| Go | golang:1.22-alpine | gcr.io/distroless/static-debian12 | 10-30MB | Production (recommended) |
| Go | golang:1.22-alpine | scratch | 5-20MB | Ultra-minimal |
| Rust | rust:1.75-alpine | scratch | 5-15MB | Production (recommended) |
| Rust | rust:1.75-alpine | gcr.io/distroless/static-debian12 | 8-18MB | With CA certs |
| Python | python:3.12-slim | python:3.12-slim | 200-400MB | Production |
| Python | python:3.12-slim | gcr.io/distroless/python3-debian12 | 100-200MB | Pure Python only |
| Node.js | node:20-alpine | node:20-alpine | 150-300MB | Production |
| Node.js | node:20-alpine | gcr.io/distroless/nodejs20-debian12 | 150-250MB | Security-focused |
| Java | maven:3.9-eclipse-temurin-21 | eclipse-temurin:21-jre-alpine | 200-350MB | Production |
Base Image Categories
1. Full Images (Largest)
Examples:
python:3.12(1GB)node:20(1GB)golang:1.22(800MB)rust:1.75(1.5GB)
Contents:
- Full OS (Debian/Ubuntu)
- Language runtime
- Build tools (gcc, make, etc.)
- Package manager (apt)
- Shell and utilities
Use for:
- Development only
- Building complex dependencies
- Debugging
Never for production.
2. Slim Images (Medium)
Examples:
python:3.12-slim(150MB)node:20-slim(250MB)debian:bookworm-slim(80MB)
Contents:
- Minimal OS (Debian)
- Language runtime
- Package manager (apt)
- Essential libraries
- Shell and basic utilities
Use for:
- Production (interpreted languages)
- When dependencies need glibc
- When debugging tools needed
Recommended for Python and Node.js production.
3. Alpine Images (Small)
Examples:
python:3.12-alpine(50MB)node:20-alpine(180MB)golang:1.22-alpine(300MB)rust:1.75-alpine(600MB)alpine:3.19(7MB)
Contents:
- Alpine Linux (musl libc, not glibc)
- Language runtime
- Package manager (apk)
- Busybox utilities
- Shell (ash, not bash)
Use for:
- Production (if no glibc dependencies)
- Build stages
- Small image size priority
Caveats:
- Uses musl instead of glibc (wheel compatibility issues for Python)
- Some packages require compilation
- DNS resolution differences
- Not recommended for Python with compiled dependencies
4. Distroless Images (Minimal)
Examples:
gcr.io/distroless/static-debian12(2MB)gcr.io/distroless/base-debian12(20MB)gcr.io/distroless/python3-debian12(60MB)gcr.io/distroless/nodejs20-debian12(150MB)
Contents:
- Application + runtime ONLY
- No package manager
- No shell
- No utilities
- Includes: CA certs, timezone data, /etc/passwd
Use for:
- Production (maximum security)
- Static binaries (Go, Rust)
- Security-critical applications
Caveats:
- Cannot debug with shell
- Cannot install packages at runtime
- Use debug variants for debugging
5. Scratch (Empty)
Example:
scratch(0 bytes)
Contents:
- Literally nothing
- Just the kernel namespace
Use for:
- Static binaries only (Go, Rust)
- Absolute minimum size
- Maximum security
Caveats:
- No shell (cannot exec)
- No CA certificates (unless copied)
- No /etc/passwd (numeric UIDs only)
- No debugging tools
Language-Specific Recommendations
Python
Production (with compiled dependencies):
FROM python:3.12-slim
# 200-400MB, includes glibc for numpy, pandas, etc.Production (pure Python):
FROM gcr.io/distroless/python3-debian12
# 100-200MB, no compiled extensionsDevelopment:
FROM python:3.12
# 1GB, all build toolsAvoid:
FROM python:3.12-alpine
# Compiles numpy from source (slow, large)Node.js
Production:
FROM node:20-alpine
# 150-300MB, recommendedSecurity-focused:
FROM gcr.io/distroless/nodejs20-debian12
# 150-250MB, no shellDevelopment:
FROM node:20
# 1GB, all toolsGo
Production (recommended):
FROM gcr.io/distroless/static-debian12
# 10-30MB, static binaryUltra-minimal:
FROM scratch
# 5-20MB, just the binaryWith debugging:
FROM alpine:3.19
# 15-35MB, includes shellRust
Production (recommended):
FROM scratch
# 5-15MB, musl static binaryWith CA certs:
FROM gcr.io/distroless/static-debian12
# 8-18MB, includes CA certificatesWith debugging:
FROM alpine:3.19
# 12-25MB, includes shellJava
Production:
FROM eclipse-temurin:21-jre-alpine
# 200-350MB, JRE onlyDevelopment:
FROM eclipse-temurin:21-jdk
# 500-700MB, full JDKDistroless Image Variants
Static Variant
Image: gcr.io/distroless/static-debian12 Size: ~2MB Contents: Filesystem, CA certs, timezone data Use for: Static binaries (Go, Rust with musl)
Example:
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/binary /app/binary
USER nonroot:nonroot
ENTRYPOINT ["/app/binary"]Base Variant
Image: gcr.io/distroless/base-debian12 Size: ~20MB Contents: Static + glibc, libssl, tzdata Use for: Dynamic binaries (Go with CGO, Rust with dynamic linking)
Example:
FROM gcr.io/distroless/base-debian12
COPY --from=builder /app/binary /app/binary
USER nonroot:nonroot
ENTRYPOINT ["/app/binary"]CC Variant
Image: gcr.io/distroless/cc-debian12 Size: ~25MB Contents: Base + glibc, libgcc, libstdc++ Use for: C/C++ applications
Python3 Variant
Image: gcr.io/distroless/python3-debian12 Size: ~60MB Contents: Python 3 runtime Use for: Pure Python applications (no compiled extensions)
Limitations:
- No pip (install in builder stage)
- No compiled extensions (numpy, pandas won't work)
Java Variants
Java 17:
FROM gcr.io/distroless/java17-debian12
# ~200MB, Java 17 JREJava 21:
FROM gcr.io/distroless/java21-debian12
# ~200MB, Java 21 JRENode.js Variant
Image: gcr.io/distroless/nodejs20-debian12 Size: ~150MB Contents: Node.js 20 runtime
Alpine vs Debian Slim
Alpine advantages:
- Smaller base image (7MB vs 80MB)
- Faster package manager (apk)
- Smaller final images
Alpine disadvantages:
- musl libc (not glibc) → wheel compatibility issues
- Compilation required for some packages
- DNS resolution differences
- Less common in production
Debian Slim advantages:
- glibc (standard) → better compatibility
- Binary wheels work (Python)
- More predictable behavior
Debian Slim disadvantages:
- Larger base image
- Slower package manager (apt)
Recommendation:
- Python: Use
slim(not alpine) to avoid compilation - Node.js: Use
alpine(smaller, no glibc issues) - Go/Rust: Use
alpinefor build stage,distroless/scratchfor runtime
Version Pinning Strategies
Pin Exact Version (Recommended)
FROM python:3.12.1-slim
# Reproducible, predictablePin Minor Version
FROM python:3.12-slim
# Gets patch updates (3.12.0 → 3.12.1)Pin Major Version (Not Recommended)
FROM python:3-slim
# Unpredictable (3.11 → 3.12 → 3.13)Never Use Latest
FROM python:latest
# ❌ Completely unpredictableMulti-Architecture Images
Most official images support multiple architectures:
# Build for amd64
docker buildx build --platform linux/amd64 -t myapp:amd64 .
# Build for arm64
docker buildx build --platform linux/arm64 -t myapp:arm64 .
# Build for both (manifest list)
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest --push .Common platforms:
linux/amd64(Intel/AMD x86_64)linux/arm64(ARM 64-bit, M1/M2 Macs, AWS Graviton)linux/arm/v7(ARM 32-bit, Raspberry Pi)
Base Image Registries
Docker Hub (Official Images)
Registry: docker.io (default) Images: python:*, node:*, golang:*, etc. Trust: High (Docker Official Images) Rate limits: 100 pulls per 6 hours (anonymous), 200 (authenticated)
Example:
FROM python:3.12-slim
# Pulls from docker.io/library/python:3.12-slimGoogle Container Registry (Distroless)
Registry: gcr.io Images: gcr.io/distroless/* Trust: High (Google-maintained) Rate limits: None
Example:
FROM gcr.io/distroless/static-debian12Red Hat (UBI)
Registry: registry.access.redhat.com Images: ubi9/* Trust: High (Red Hat) Use for: Enterprise environments, RHEL ecosystem
Example:
FROM registry.access.redhat.com/ubi9/python-39GitHub Container Registry
Registry: ghcr.io Images: Various open-source projects Trust: Varies by maintainer
Example:
FROM ghcr.io/astral-sh/uv:latestSecurity Considerations
Choose base images with:
- ✅ Official maintainer (Docker, Google, Red Hat)
- ✅ Regular security updates
- ✅ Minimal CVE count
- ✅ Active community
- ✅ Clear provenance
Avoid:
- ❌ Unmaintained images
- ❌ Images with HIGH/CRITICAL CVEs
- ❌ Images from unknown publishers
- ❌ Images without version tags
Scan before use:
trivy image python:3.12-slim
docker scout cves python:3.12-slimSummary Table
| Category | Size | Security | Compatibility | Debug-ability | Use Case |
|---|---|---|---|---|---|
| Full | 1GB+ | Low | High | High | Development only |
| Slim | 100-300MB | Medium | High | High | Production (Python, Node.js) |
| Alpine | 50-200MB | Medium | Medium | High | Production (Go, Rust build) |
| Distroless | 2-200MB | High | Medium | None | Production (security-focused) |
| Scratch | 0MB | Highest | Low | None | Static binaries only |
Key recommendations:
- Python:
python:3.12-slim(production),gcr.io/distroless/python3-debian12(pure Python) - Node.js:
node:20-alpine(production),gcr.io/distroless/nodejs20-debian12(security) - Go:
gcr.io/distroless/static-debian12(production),scratch(ultra-minimal) - Rust:
scratch(production),gcr.io/distroless/static-debian12(with CA certs) - Java:
eclipse-temurin:21-jre-alpine(production)
Always:
- Pin exact versions
- Scan for vulnerabilities
- Use multi-stage builds
- Choose smallest viable image
- Prefer official images
BuildKit Advanced Features
Comprehensive guide to Docker BuildKit's advanced features for faster, more secure builds.
Table of Contents
1. Enabling BuildKit 2. Cache Mounts 3. Secret Mounts 4. SSH Mounts 5. Bind Mounts 6. Parallel Stage Execution 7. BuildKit Syntax
Enabling BuildKit
BuildKit benefits:
- Parallel stage execution
- Advanced caching mechanisms
- Secret and SSH mounts
- Better layer caching
- 20-50% faster builds
Method 1: Environment Variable
export DOCKER_BUILDKIT=1
docker build -t myapp:latest .Method 2: Docker Buildx (Recommended)
# Buildx is built-in to Docker Desktop
docker buildx build -t myapp:latest .
# Create and use buildx builder
docker buildx create --name mybuilder --use
docker buildx build -t myapp:latest .Method 3: Daemon Configuration
Edit `/etc/docker/daemon.json`:
{
"features": {
"buildkit": true
}
}Restart Docker:
sudo systemctl restart dockerMethod 4: Per-Dockerfile (Syntax Directive)
# syntax=docker/dockerfile:1
FROM python:3.12-slim
# ... rest of DockerfileThis enables BuildKit features even without environment variable.
Cache Mounts
Purpose: Persist package manager caches across builds.
Without cache mount:
# ❌ Re-downloads every build (60s)
RUN pip install -r requirements.txtWith cache mount:
# ✅ Persistent cache (5s on rebuild)
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txtSpeed improvement: 10-100x faster on cache hit.
Python Cache Mounts
pip:
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txtpoetry:
RUN --mount=type=cache,target=/root/.cache/pypoetry \
poetry install --no-devuv:
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-devNode.js Cache Mounts
npm:
RUN --mount=type=cache,target=/root/.npm \
npm cipnpm:
RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
pnpm install --frozen-lockfileyarn:
RUN --mount=type=cache,target=/usr/local/share/.cache/yarn \
yarn install --frozen-lockfileGo Cache Mounts
Module cache:
RUN --mount=type=cache,target=/go/pkg/mod \
go mod downloadBuild cache:
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go build -o main .Rust Cache Mounts
Cargo registry:
RUN --mount=type=cache,target=/usr/local/cargo/registry \
cargo build --releaseTarget directory (build artifacts):
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/app/target \
cargo build --releaseAPT/APK Cache Mounts
Debian/Ubuntu (apt):
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 build-essentialAlpine (apk):
RUN --mount=type=cache,target=/var/cache/apk \
apk add --no-cache build-baseCache Mount Options
Full syntax:
RUN --mount=type=cache,target=/path/to/cache,id=unique-id,sharing=shared,mode=0755,uid=1000,gid=1000 \
commandParameters:
target→ Directory to cache (required)id→ Unique cache identifier (default: hash of target)sharing→shared(default),locked, orprivatemode→ Directory permissions (default: 0755)uid/gid→ Owner UID/GID
Sharing modes:
shared→ Multiple builds can read/write simultaneouslylocked→ One build at a time (safer for apt/yum)private→ Exclusive cache per build
Managing Cache
Inspect cache:
docker buildx duPrune cache:
# Remove unused cache
docker buildx prune
# Remove all cache
docker buildx prune --all
# Remove cache older than 7 days
docker buildx prune --filter until=168hSecret Mounts
Purpose: Inject secrets during build without storing in layers.
Key principle: Secrets available during RUN command, but NOT stored in image.
Pattern 1: Token from File
Dockerfile:
# syntax=docker/dockerfile:1
FROM python:3.12-slim
WORKDIR /app
# Use secret to install from private registry
RUN --mount=type=secret,id=pypi_token \
pip config set global.index-url https://$(cat /run/secrets/pypi_token)@pypi.example.com/simple && \
pip install -r requirements.txt
COPY . .
CMD ["python", "app.py"]Build command:
echo "my_token_here" > pypi_token.txt
docker buildx build --secret id=pypi_token,src=pypi_token.txt -t myapp:latest .
rm pypi_token.txtHow it works:
- Secret mounted at
/run/secrets/pypi_tokenduring build - Accessible only during RUN command execution
- NOT stored in image layers or history
Pattern 2: Token from Environment Variable
Dockerfile (same as above):
RUN --mount=type=secret,id=pypi_token \
pip config set global.index-url https://$(cat /run/secrets/pypi_token)@pypi.example.com/simple && \
pip install -r requirements.txtBuild command:
export PYPI_TOKEN="my_token_here"
echo "$PYPI_TOKEN" | docker buildx build --secret id=pypi_token,src=- -t myapp:latest .Pattern 3: Multiple Secrets
Dockerfile:
RUN --mount=type=secret,id=npm_token \
--mount=type=secret,id=github_token \
echo "//registry.npmjs.org/:_authToken=$(cat /run/secrets/npm_token)" > ~/.npmrc && \
git config --global url."https://$(cat /run/secrets/github_token)@github.com/".insteadOf "https://github.com/" && \
npm ciBuild command:
docker buildx build \
--secret id=npm_token,src=npm_token.txt \
--secret id=github_token,src=github_token.txt \
-t myapp:latest .Pattern 4: .netrc for Go Modules
Dockerfile:
# syntax=docker/dockerfile:1
FROM golang:1.22-alpine AS builder
WORKDIR /app
# Download private Go modules using .netrc
RUN --mount=type=secret,id=netrc,target=/root/.netrc \
go mod download
COPY . .
RUN go build -o main .
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/main /app/main
USER nonroot:nonroot
ENTRYPOINT ["/app/main"].netrc file:
machine github.com
login your-username
password ghp_your_tokenBuild command:
docker buildx build --secret id=netrc,src=.netrc -t myapp:latest .Pattern 5: AWS Credentials
Dockerfile:
# syntax=docker/dockerfile:1
FROM python:3.12-slim
# Install AWS CLI
RUN pip install awscli
# Download from S3 using AWS credentials
RUN --mount=type=secret,id=aws,target=/root/.aws/credentials \
aws s3 cp s3://my-private-bucket/data.tar.gz /app/data.tar.gz && \
tar -xzf /app/data.tar.gz -C /app
CMD ["python", "app.py"]Build command:
docker buildx build --secret id=aws,src=$HOME/.aws/credentials -t myapp:latest .Secret Mount Options
Full syntax:
RUN --mount=type=secret,id=my_secret,target=/run/secrets/my_secret,required=true,mode=0400,uid=1000 \
commandParameters:
id→ Secret identifier (required)target→ Mount path (default:/run/secrets/{id})required→ Fail if secret missing (default: false)mode→ File permissions (default: 0400)uid/gid→ Owner UID/GID
SSH Mounts
Purpose: Use SSH keys for git clones without storing in image.
Pattern 1: Clone Private Repo
Dockerfile:
# syntax=docker/dockerfile:1
FROM alpine
# Install git and SSH client
RUN apk add --no-cache git openssh-client
# Clone private repository using SSH
RUN --mount=type=ssh \
mkdir -p ~/.ssh && \
ssh-keyscan github.com >> ~/.ssh/known_hosts && \
git clone git@github.com:myorg/private-repo.git /app
WORKDIR /app
CMD ["./start.sh"]Build command:
# Start ssh-agent and add key
eval $(ssh-agent)
ssh-add ~/.ssh/id_rsa
# Build with SSH forwarding
docker buildx build --ssh default -t myapp:latest .Pattern 2: Multiple SSH Keys
Dockerfile:
RUN --mount=type=ssh,id=github \
--mount=type=ssh,id=gitlab \
git clone git@github.com:org/repo1.git && \
git clone git@gitlab.com:org/repo2.gitBuild command:
docker buildx build \
--ssh github=~/.ssh/github_key \
--ssh gitlab=~/.ssh/gitlab_key \
-t myapp:latest .SSH Mount Options
Full syntax:
RUN --mount=type=ssh,id=default,target=/root/.ssh/id_rsa,required=true,mode=0600 \
git clone git@github.com:private/repo.gitParameters:
id→ SSH key identifier (default:default)target→ Mount path (default: auto-configured)required→ Fail if key missing (default: false)mode→ File permissions (default: 0600)
Bind Mounts
Purpose: Mount files from other stages or host without COPY.
Pattern 1: Mount from Build Context
Dockerfile:
# syntax=docker/dockerfile:1
FROM golang:1.22-alpine AS builder
WORKDIR /app
# Mount go.mod temporarily (don't copy)
RUN --mount=type=bind,source=go.mod,target=go.mod \
--mount=type=bind,source=go.sum,target=go.sum \
--mount=type=cache,target=/go/pkg/mod \
go mod download
COPY . .
RUN go build -o main .
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/main /app/main
ENTRYPOINT ["/app/main"]Benefits:
- Faster than COPY for large files
- Doesn't create intermediate layer
Pattern 2: Mount from Another Stage
Dockerfile:
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM node:20-alpine AS builder
WORKDIR /app
# Mount node_modules from deps stage
RUN --mount=type=bind,from=deps,source=/app/node_modules,target=/app/node_modules \
npm run build
FROM node:20-alpine
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/index.js"]Bind Mount Options
Full syntax:
RUN --mount=type=bind,source=src,target=dst,from=stage,rw=true \
commandParameters:
source→ Source path (default: build context root)target→ Mount path in container (required)from→ Source stage name (default: build context)rw→ Read-write mount (default: false, read-only)
Parallel Stage Execution
BuildKit automatically parallelizes independent stages.
Sequential execution (old Docker):
FROM base AS stage1
RUN task1
FROM base AS stage2
RUN task2 # Waits for stage1
FROM stage1 AS final
COPY --from=stage2 /output /outputWith BuildKit:
stage1andstage2run in parallelfinalwaits for both
Pattern: Parallel Builds
Dockerfile:
# syntax=docker/dockerfile:1
FROM node:20-alpine AS frontend-builder
WORKDIR /app/frontend
COPY frontend/package*.json ./
RUN npm ci
COPY frontend/ ./
RUN npm run build
FROM golang:1.22-alpine AS backend-builder
WORKDIR /app/backend
COPY backend/go.mod backend/go.sum ./
RUN go mod download
COPY backend/ ./
RUN go build -o server .
# Both stages build in parallel
FROM alpine:3.19
COPY --from=frontend-builder /app/frontend/dist /app/public
COPY --from=backend-builder /app/backend/server /app/server
CMD ["/app/server"]BuildKit parallelizes frontend-builder and backend-builder.
Viewing Build Graph
# Show build graph
docker buildx build --print=outline .
# Show build provenance
docker buildx build --provenance=true --sbom=true -t myapp:latest .BuildKit Syntax
Specify BuildKit version:
# syntax=docker/dockerfile:1.6Available versions:
docker/dockerfile:1→ Latest stable (recommended)docker/dockerfile:1.6→ Specific versiondocker/dockerfile:labs→ Experimental features
Experimental Features (Labs)
Enable labs:
# syntax=docker/dockerfile:1-labsFeatures:
RUN --network=none→ Disable network during RUNCOPY --parents→ Preserve directory structureCOPY --exclude→ Exclude patterns
Example:
# syntax=docker/dockerfile:1-labs
FROM alpine
# Disable network for security
RUN --network=none \
apk add --no-cache ca-certificates
# Copy with exclusions
COPY --exclude=*.test.js . /appComplete BuildKit Example
Production-optimized Dockerfile using all features:
# syntax=docker/dockerfile:1
FROM node:20-alpine AS deps
WORKDIR /app
# Cache npm packages
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci
# Builder stage
FROM node:20-alpine AS builder
WORKDIR /app
# Mount node_modules from deps
RUN --mount=type=bind,from=deps,source=/app/node_modules,target=/app/node_modules \
--mount=type=bind,source=package.json,target=package.json \
--mount=type=bind,source=tsconfig.json,target=tsconfig.json \
--mount=type=bind,source=src,target=src \
npm run build
# Install private packages using secret
RUN --mount=type=secret,id=npm_token \
--mount=type=cache,target=/root/.npm \
echo "//registry.npmjs.org/:_authToken=$(cat /run/secrets/npm_token)" > ~/.npmrc && \
npm ci --omit=dev && \
rm ~/.npmrc
# Runtime stage
FROM node:20-alpine
WORKDIR /app
# Copy production files
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
# Non-root user
USER node
ENV NODE_ENV=production
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
EXPOSE 3000
CMD ["node", "dist/index.js"]Build command:
docker buildx build \
--secret id=npm_token,src=npm_token.txt \
--cache-from type=registry,ref=myapp:cache \
--cache-to type=registry,ref=myapp:cache,mode=max \
-t myapp:latest \
--push \
.Features used:
- ✅ Multi-stage build
- ✅ Cache mounts (npm)
- ✅ Secret mounts (private packages)
- ✅ Bind mounts (avoid COPY for temporary files)
- ✅ Parallel stage execution
- ✅ Remote cache (registry)
Summary
BuildKit features ranked by impact:
| Feature | Speed Improvement | Security Improvement | Use Case |
|---|---|---|---|
| Cache mounts | 10-100x | None | Package managers |
| Secret mounts | None | Critical | Private registries |
| Parallel stages | 2-4x | None | Multi-component builds |
| SSH mounts | None | High | Private git repos |
| Bind mounts | 20-50% | None | Large temporary files |
Key takeaways:
- Always enable BuildKit (
# syntax=docker/dockerfile:1) - Use cache mounts for all package managers (10-100x faster)
- Use secret mounts for credentials (never ENV vars)
- Use SSH mounts for private git clones
- BuildKit automatically parallelizes independent stages
- Cache is persistent across builds (manage with
docker buildx du/prune) - Bind mounts avoid intermediate layers for temporary files
Go Dockerfiles
Complete patterns for containerizing Go applications with minimal image sizes.
Table of Contents
1. Why Go is Perfect for Docker 2. Base Image Selection 3. Pattern 1: Distroless Static (Smallest) 4. Pattern 2: Alpine Runtime 5. Pattern 3: Scratch Base (Advanced) 6. Build Optimization Techniques 7. Common Go Pitfalls
Why Go is Perfect for Docker
Go's advantages for containerization:
- Static binaries: Single executable with no external dependencies
- Small size: 5-30MB final images with distroless/scratch
- Fast startup: No JVM warmup or interpreter overhead
- Cross-compilation: Build for Linux on any platform
- No runtime: Unlike Python/Node.js, no language runtime needed
Typical image sizes:
- Go + distroless/static: 10-30MB
- Go + alpine: 15-35MB
- Go + scratch: 5-20MB
- Python equivalent: 200-400MB
- Node.js equivalent: 180-320MB
Base Image Selection
Recommended Go base images:
| Build Stage | Runtime Stage | Final Size | Use Case |
|---|---|---|---|
golang:1.22-alpine | gcr.io/distroless/static-debian12 | 10-30MB | Production (recommended) |
golang:1.22-alpine | alpine:3.19 | 15-35MB | Need shell for debugging |
golang:1.22-alpine | scratch | 5-20MB | Maximum minimalism |
golang:1.22 | gcr.io/distroless/base-debian12 | 15-35MB | CGO dependencies |
Version pinning:
# ✅ Good: Exact version
FROM golang:1.22.0-alpine
# ⚠️ OK: Minor version pinned
FROM golang:1.22-alpine
# ❌ Bad: Unpredictable
FROM golang:alpine
FROM golang:latestPattern 1: Distroless Static (Smallest)
Use when:
- Pure Go code (no CGO)
- Production deployments
- Security is priority
- Minimal image size needed
Multi-stage Dockerfile:
# syntax=docker/dockerfile:1
FROM golang:1.22-alpine AS builder
WORKDIR /app
# Download dependencies first (cached layer)
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
# Copy source code
COPY . .
# Build static binary
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -ldflags="-s -w" -o /app/main .
# Runtime stage: distroless static
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/main /app/main
# Use built-in nonroot user (UID 65532)
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/app/main"]Key build flags explained:
CGO_ENABLED=0→ Disable CGO, produce pure static binaryGOOS=linux→ Target Linux (default, explicit for clarity)GOARCH=amd64→ Target amd64 (or arm64 for ARM)-ldflags="-s -w"→ Strip debug symbols (30-50% smaller)-s→ Omit symbol table-w→ Omit DWARF debug info
Build command:
docker build -t myapp:latest .Expected size: 10-30MB
Pattern 2: Alpine Runtime
Use when:
- Need shell access for debugging
- Need to install runtime packages
- Slightly larger image acceptable
Multi-stage Dockerfile:
# syntax=docker/dockerfile:1
FROM golang:1.22-alpine AS builder
WORKDIR /app
# Download dependencies
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
# Copy source code
COPY . .
# Build static binary
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 go build -ldflags="-s -w" -o /app/main .
# Runtime stage: Alpine
FROM alpine:3.19
# Install CA certificates (for HTTPS requests)
RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY --from=builder /app/main /app/main
# Create non-root user
RUN addgroup -g 1000 appuser && \
adduser -D -u 1000 -G appuser appuser && \
chown -R appuser:appuser /app
USER appuser
EXPOSE 8080
ENTRYPOINT ["/app/main"]When to use Alpine runtime:
- Need
shshell for debugging - Need runtime utilities (
curl,wget) - Need to install packages at runtime
Build command:
docker build -t myapp:latest .Expected size: 15-35MB
Pattern 3: Scratch Base (Advanced)
Use when:
- Absolute minimum size required
- Static binary with zero dependencies
- No HTTPS calls (no CA certs needed)
Multi-stage Dockerfile:
# syntax=docker/dockerfile:1
FROM golang:1.22-alpine AS builder
WORKDIR /app
# Download dependencies
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
# Copy source code
COPY . .
# Build completely static binary
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags="-s -w -extldflags '-static'" -o /app/main .
# Runtime stage: scratch (empty base)
FROM scratch
# Copy CA certificates from builder (if HTTPS needed)
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# Copy binary
COPY --from=builder /app/main /app/main
# Run as non-root (numeric UID only, no user creation possible)
USER 1000:1000
EXPOSE 8080
ENTRYPOINT ["/app/main"]Scratch base limitations:
- No shell (cannot
docker exec -it) - No utilities (no debugging tools)
- Cannot install packages
- Numeric UID only (no username resolution)
When scratch works:
- Pure Go services
- No debugging needed in production
- Maximum security posture
Build command:
docker build -t myapp:latest .Expected size: 5-20MB
Build Optimization Techniques
Technique 1: Layer Caching for Dependencies
Problem: Re-downloads all modules on every code change.
# ❌ Re-downloads modules on every build
COPY . .
RUN go mod downloadSolution: Copy go.mod/go.sum first, cache layer:
# ✅ Cached layer if dependencies unchanged
COPY go.mod go.sum ./
RUN go mod download
# Code changes don't invalidate dependency cache
COPY . .
RUN go build -o main .Technique 2: BuildKit Cache Mounts
Without cache mount:
# ❌ Re-downloads every build
RUN go mod downloadWith cache mount:
# ✅ Persistent cache across builds
RUN --mount=type=cache,target=/go/pkg/mod \
go mod downloadDual cache mounts (dependencies + build cache):
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go build -o main .Speed improvement: 5-10x faster rebuilds.
Technique 3: Stripping Debug Symbols
Default build (with debug symbols):
RUN go build -o main .
# Binary size: 20MBStripped build:
RUN go build -ldflags="-s -w" -o main .
# Binary size: 12MB (40% smaller)Advanced stripping:
RUN go build -ldflags="-s -w -extldflags '-static'" -o main .
# Additional flags for static linkingTechnique 4: Multi-Architecture Builds
Build for multiple platforms:
ARG TARGETOS=linux
ARG TARGETARCH=amd64
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \
go build -ldflags="-s -w" -o /app/main .Build command:
# Build for amd64
docker buildx build --platform linux/amd64 -t myapp:amd64 .
# Build for arm64
docker buildx build --platform linux/arm64 -t myapp:arm64 .
# Build for both (multi-arch)
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest .Common Go Pitfalls
Pitfall 1: CGO Enabled with Distroless/Scratch
Problem: CGO requires libc, which distroless/static doesn't have.
# ❌ This will fail at runtime
FROM golang:1.22-alpine AS builder
RUN go build -o main . # CGO_ENABLED=1 by default
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/main /app/main
ENTRYPOINT ["/app/main"]
# Runtime error: binary needs libcSolution 1: Disable CGO:
# ✅ Static binary
RUN CGO_ENABLED=0 go build -o main .
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/main /app/main
ENTRYPOINT ["/app/main"]Solution 2: Use distroless/base (includes libc):
# ✅ Dynamic binary with libc
RUN go build -o main . # CGO_ENABLED=1
FROM gcr.io/distroless/base-debian12
COPY --from=builder /app/main /app/main
ENTRYPOINT ["/app/main"]Pitfall 2: Missing CA Certificates for HTTPS
Problem: HTTPS calls fail without CA certificates.
FROM scratch
COPY --from=builder /app/main /app/main
ENTRYPOINT ["/app/main"]
# Runtime error: x509: certificate signed by unknown authoritySolution: Copy CA certificates from builder:
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app/main /app/main
ENTRYPOINT ["/app/main"]Alternative: Use distroless (includes CA certs):
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/main /app/main
ENTRYPOINT ["/app/main"]Pitfall 3: Not Using go.sum for Verification
Problem: Dependencies change unexpectedly.
# ❌ No checksum verification
COPY go.mod ./
RUN go mod downloadSolution: Copy go.sum for verification:
# ✅ Checksum verification
COPY go.mod go.sum ./
RUN go mod downloadPitfall 4: Large Binary Due to Debug Symbols
Problem: Binary includes debug symbols, stack traces, etc.
# ❌ 20MB binary
RUN go build -o main .Solution: Strip symbols with ldflags:
# ✅ 12MB binary (40% smaller)
RUN go build -ldflags="-s -w" -o main .Pitfall 5: Running as Root
Problem: Security risk.
# ❌ Runs as root
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/main /app/main
ENTRYPOINT ["/app/main"]Solution: Use nonroot user:
# ✅ Runs as UID 65532
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/main /app/main
USER nonroot:nonroot
ENTRYPOINT ["/app/main"]HTTP Server Complete Example
Production-ready Go HTTP server:
# syntax=docker/dockerfile:1
FROM golang:1.22-alpine AS builder
WORKDIR /app
# Download dependencies
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
# Copy source code
COPY . .
# Build static binary
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/server ./cmd/server
# Runtime stage
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/server /app/server
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/app/server"]main.go (cmd/server/main.go):
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "OK")
})
log.Println("Server starting on :8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}go.mod:
module github.com/myuser/myapp
go 1.22Expected size: 10-15MB
Gin Framework Complete Example
Production-ready Gin API:
# syntax=docker/dockerfile:1
FROM golang:1.22-alpine AS builder
WORKDIR /app
# Download dependencies
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
# Verify dependencies
RUN --mount=type=cache,target=/go/pkg/mod \
go mod verify
# Copy source code
COPY . .
# Build static binary
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/server .
# Runtime stage
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/server /app/server
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/app/server"]main.go:
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/health", func(c *gin.Context) {
c.JSON(200, gin.H{
"status": "healthy",
})
})
r.Run(":8080")
}Expected size: 15-25MB
gRPC Service Complete Example
Production-ready gRPC service:
# syntax=docker/dockerfile:1
FROM golang:1.22-alpine AS builder
# Install protobuf compiler (if generating protos)
RUN apk add --no-cache protobuf-dev
WORKDIR /app
# Download dependencies
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
# Copy source code
COPY . .
# Generate protobuf code (if needed)
# RUN go generate ./...
# Build static binary
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/server ./cmd/server
# Runtime stage
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/server /app/server
USER nonroot:nonroot
EXPOSE 50051
ENTRYPOINT ["/app/server"]Expected size: 20-35MB (includes gRPC libraries)
Private Go Module Example
Using BuildKit secret mount for GITHUB_TOKEN:
# syntax=docker/dockerfile:1
FROM golang:1.22-alpine AS builder
WORKDIR /app
# Configure git to use token
RUN apk add --no-cache git
# Download dependencies using secret
COPY go.mod go.sum ./
RUN --mount=type=secret,id=github_token \
--mount=type=cache,target=/go/pkg/mod \
git config --global url."https://$(cat /run/secrets/github_token)@github.com/".insteadOf "https://github.com/" && \
go mod download
# Copy source and build
COPY . .
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 go build -ldflags="-s -w" -o /app/main .
# Runtime stage
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/main /app/main
USER nonroot:nonroot
ENTRYPOINT ["/app/main"]Build command:
echo "ghp_your_token" > github_token.txt
docker buildx build --secret id=github_token,src=github_token.txt -t myapp:latest .
rm github_token.txtSummary
Go Dockerfile patterns ranked:
| Pattern | Size | Security | Debug-ability | Use Case |
|---|---|---|---|---|
| Distroless static | 10-30MB | Highest | None | Production (recommended) |
| Alpine | 15-35MB | High | Shell access | Development, debugging |
| Scratch | 5-20MB | Highest | None | Ultra-minimal production |
| Distroless base | 15-35MB | High | None | CGO dependencies |
Key takeaways:
- Always use multi-stage builds
- Disable CGO for static binaries (
CGO_ENABLED=0) - Strip symbols with
-ldflags="-s -w"(30-50% smaller) - Use BuildKit cache mounts for
/go/pkg/modand build cache - Use distroless/static for production (smallest + most secure)
- Copy go.mod/go.sum before source code (layer caching)
- Use nonroot:nonroot user in distroless
- Copy CA certificates if making HTTPS calls
- Pin Go version in base image
- Final images: 10-30MB (vs 200-400MB for Python/Node.js)
Java Dockerfiles
Complete patterns for containerizing Java applications with Maven and Gradle.
Table of Contents
1. Base Image Selection 2. Pattern 1: Maven Multi-Stage 3. Pattern 2: Gradle Multi-Stage 4. Pattern 3: Spring Boot 5. JVM Optimization 6. Common Java Pitfalls
Base Image Selection
Recommended Java base images:
| Build Stage | Runtime Stage | Final Size | Use Case |
|---|---|---|---|
maven:3.9-eclipse-temurin-21 | eclipse-temurin:21-jre-alpine | 200-350MB | Production (recommended) |
gradle:8-jdk21 | eclipse-temurin:21-jre-alpine | 200-350MB | Gradle projects |
maven:3.9-eclipse-temurin-17 | eclipse-temurin:17-jre-alpine | 180-320MB | Java 17 projects |
maven:3.9-eclipse-temurin-21 | gcr.io/distroless/java21-debian12 | 250-400MB | Maximum security |
Version pinning:
# ✅ Good: Exact version
FROM eclipse-temurin:21.0.1-jre-alpine
# ⚠️ OK: Minor version pinned
FROM eclipse-temurin:21-jre-alpine
# ❌ Bad: Unpredictable
FROM eclipse-temurin:jre-alpine
FROM openjdk:latestPattern 1: Maven Multi-Stage
Use when:
- Standard Maven projects
- Spring Boot applications
- Traditional Java applications
Multi-stage Dockerfile:
# syntax=docker/dockerfile:1
FROM maven:3.9-eclipse-temurin-21 AS builder
WORKDIR /app
# Copy pom.xml first for dependency caching
COPY pom.xml .
# Download dependencies (cached layer)
RUN --mount=type=cache,target=/root/.m2 \
mvn dependency:go-offline
# Copy source code
COPY src ./src
# Build application
RUN --mount=type=cache,target=/root/.m2 \
mvn clean package -DskipTests
# Runtime stage
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
# Copy JAR from builder
COPY --from=builder /app/target/*.jar app.jar
# Create non-root user
RUN addgroup -g 1000 appuser && \
adduser -D -u 1000 -G appuser appuser && \
chown -R appuser:appuser /app
USER appuser
# JVM options
ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=30s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health || exit 1
EXPOSE 8080
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]Key features:
- Dependency caching with BuildKit
- JRE-only runtime (no JDK)
- Non-root user
- Container-aware JVM settings
- Health check for Spring Boot actuator
Build command:
docker build -t java-app:latest .Expected size: 250-350MB
Pattern 2: Gradle Multi-Stage
Use when:
- Gradle-based projects
- Android backend services
- Kotlin applications
Multi-stage Dockerfile:
# syntax=docker/dockerfile:1
FROM gradle:8-jdk21 AS builder
WORKDIR /app
# Copy Gradle files for dependency caching
COPY build.gradle settings.gradle ./
COPY gradle ./gradle
# Download dependencies (cached layer)
RUN --mount=type=cache,target=/root/.gradle \
gradle dependencies --no-daemon
# Copy source code
COPY src ./src
# Build application
RUN --mount=type=cache,target=/root/.gradle \
gradle clean build -x test --no-daemon
# Runtime stage
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
# Copy JAR from builder
COPY --from=builder /app/build/libs/*.jar app.jar
# Create non-root user
RUN addgroup -g 1000 appuser && \
adduser -D -u 1000 -G appuser appuser && \
chown -R appuser:appuser /app
USER appuser
# JVM options
ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"
EXPOSE 8080
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]Key Gradle commands:
gradle dependencies→ Download dependencies onlygradle build -x test→ Build without running tests--no-daemon→ Don't start Gradle daemon (Docker doesn't need it)
Build command:
docker build -t gradle-app:latest .Expected size: 250-350MB
Pattern 3: Spring Boot
Optimized Spring Boot Dockerfile with layered JARs:
# syntax=docker/dockerfile:1
FROM maven:3.9-eclipse-temurin-21 AS builder
WORKDIR /app
# Copy pom.xml
COPY pom.xml .
# Download dependencies
RUN --mount=type=cache,target=/root/.m2 \
mvn dependency:go-offline
# Copy source
COPY src ./src
# Build with layers enabled
RUN --mount=type=cache,target=/root/.m2 \
mvn clean package -DskipTests && \
java -Djarmode=layertools -jar target/*.jar extract
# Runtime stage
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
# Copy layers separately for better caching
COPY --from=builder /app/dependencies/ ./
COPY --from=builder /app/spring-boot-loader/ ./
COPY --from=builder /app/snapshot-dependencies/ ./
COPY --from=builder /app/application/ ./
# Create non-root user
RUN addgroup -g 1000 appuser && \
adduser -D -u 1000 -G appuser appuser && \
chown -R appuser:appuser /app
USER appuser
# JVM options
ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -XX:+UseG1GC"
# Spring Boot actuator health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=30s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health || exit 1
EXPOSE 8080
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS org.springframework.boot.loader.JarLauncher"]pom.xml configuration:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<layers>
<enabled>true</enabled>
</layers>
</configuration>
</plugin>
</plugins>
</build>Layered JAR benefits:
- Better layer caching (dependencies change less than application code)
- Faster rebuilds when only application code changes
- Smaller layer pushes to registry
Expected size: 280-380MB
JVM Optimization
Container-Aware JVM Settings
Modern JVMs (Java 10+) are container-aware:
ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"Key flags:
-XX:+UseContainerSupport→ Respect container memory limits-XX:MaxRAMPercentage=75.0→ Use 75% of container memory for heap-XX:InitialRAMPercentage=50.0→ Initial heap size-XX:MinRAMPercentage=50.0→ Minimum heap size for small containers
Garbage Collector Selection
G1GC (default, recommended):
ENV JAVA_OPTS="-XX:+UseG1GC -XX:MaxGCPauseMillis=200"ZGC (low latency):
ENV JAVA_OPTS="-XX:+UseZGC -XX:+ZGenerational"Shenandoah (low latency alternative):
ENV JAVA_OPTS="-XX:+UseShenandoahGC"Memory Configuration
For 512MB container:
ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0 -XX:InitialRAMPercentage=50.0"
# Heap: ~384MB max, ~256MB initialFor 2GB container:
ENV JAVA_OPTS="-XX:MaxRAMPercentage=70.0"
# Heap: ~1.4GBDiagnostic Options
Enable JVM diagnostics:
ENV JAVA_OPTS="-XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+PrintGCTimeStamps"Heap dump on OutOfMemoryError:
ENV JAVA_OPTS="-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/app/heapdumps"Common Java Pitfalls
Pitfall 1: Using JDK in Runtime
Problem: JDK is much larger than JRE.
# ❌ JDK in runtime (500MB+)
FROM eclipse-temurin:21
COPY app.jar .
CMD ["java", "-jar", "app.jar"]Solution: Use JRE in runtime stage:
# ✅ JRE only (200MB)
FROM eclipse-temurin:21-jre-alpine
COPY app.jar .
CMD ["java", "-jar", "app.jar"]Pitfall 2: Not Caching Dependencies
Problem: Re-downloads dependencies every build.
# ❌ Re-downloads every time
COPY . .
RUN mvn packageSolution: Copy pom.xml first, cache dependencies:
# ✅ Cached dependencies
COPY pom.xml .
RUN --mount=type=cache,target=/root/.m2 \
mvn dependency:go-offline
COPY src ./src
RUN --mount=type=cache,target=/root/.m2 \
mvn packagePitfall 3: Not Setting Memory Limits
Problem: JVM doesn't respect container limits (older Java).
# ❌ JVM may use more than container limit
CMD ["java", "-jar", "app.jar"]Solution: Use container-aware settings:
# ✅ Container-aware JVM
ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"
CMD ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]Pitfall 4: Running Tests in Docker Build
Problem: Slow builds, external dependencies may fail.
# ❌ Runs tests during build (slow, flaky)
RUN mvn clean packageSolution: Skip tests, run in CI separately:
# ✅ Skip tests in Docker build
RUN mvn clean package -DskipTestsPitfall 5: Not Using Layered JARs (Spring Boot)
Problem: Application code changes invalidate entire JAR layer.
# ❌ Entire JAR in one layer
COPY --from=builder /app/target/*.jar app.jar
# Small code change = entire 100MB JAR re-pushedSolution: Use layered JARs:
# ✅ Separate layers (dependencies cached)
COPY --from=builder /app/dependencies/ ./
COPY --from=builder /app/application/ ./
# Small code change = only application layer re-pushed (~10MB)Distroless Java Example
Maximum security with distroless:
# syntax=docker/dockerfile:1
FROM maven:3.9-eclipse-temurin-21 AS builder
WORKDIR /app
COPY pom.xml .
RUN --mount=type=cache,target=/root/.m2 \
mvn dependency:go-offline
COPY src ./src
RUN --mount=type=cache,target=/root/.m2 \
mvn clean package -DskipTests
# Runtime stage: distroless Java
FROM gcr.io/distroless/java21-debian12
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
USER nonroot:nonroot
# Note: JAVA_OPTS via environment, not command line
ENV JAVA_TOOL_OPTIONS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]Key differences with distroless:
- No shell → Use
ENTRYPOINT ["java", ...]directly - No sh → Can't use
sh -c - Set JVM options via
JAVA_TOOL_OPTIONSenvironment variable - Built-in nonroot user (UID 65532)
Expected size: 300-450MB
Complete Spring Boot Example
Production-ready Spring Boot microservice:
# syntax=docker/dockerfile:1
FROM maven:3.9-eclipse-temurin-21 AS builder
WORKDIR /app
# Copy Maven files
COPY pom.xml .
COPY .mvn .mvn
# Download dependencies
RUN --mount=type=cache,target=/root/.m2 \
mvn dependency:go-offline
# Copy source
COPY src ./src
# Build with layers
RUN --mount=type=cache,target=/root/.m2 \
mvn clean package -DskipTests && \
java -Djarmode=layertools -jar target/*.jar extract
# Runtime stage
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
# Copy layers
COPY --from=builder /app/dependencies/ ./
COPY --from=builder /app/spring-boot-loader/ ./
COPY --from=builder /app/snapshot-dependencies/ ./
COPY --from=builder /app/application/ ./
# Create non-root user
RUN addgroup -g 1000 appuser && \
adduser -D -u 1000 -G appuser appuser && \
chown -R appuser:appuser /app
USER appuser
# JVM options
ENV JAVA_OPTS="-XX:+UseContainerSupport \
-XX:MaxRAMPercentage=75.0 \
-XX:+UseG1GC \
-Dspring.profiles.active=prod"
# Health check (Spring Boot Actuator)
HEALTHCHECK --interval=30s --timeout=3s --start-period=30s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health/liveness || exit 1
EXPOSE 8080
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS org.springframework.boot.loader.JarLauncher"]application.yml:
management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
probes:
enabled: trueExpected size: 300-400MB
Summary
Java Dockerfile patterns ranked:
| Pattern | Size | Build Speed | Use Case |
|---|---|---|---|
| Maven + JRE Alpine | 250-350MB | Medium | Production (recommended) |
| Gradle + JRE Alpine | 250-350MB | Medium | Gradle projects |
| Spring Boot Layered | 280-380MB | Fast | Spring Boot apps |
| Distroless Java | 300-450MB | Medium | Maximum security |
Key takeaways:
- Always use multi-stage builds (JDK for build, JRE for runtime)
- Use BuildKit cache mounts for Maven/Gradle dependencies
- Enable Spring Boot layered JARs for better caching
- Use container-aware JVM settings (
-XX:+UseContainerSupport) - Set MaxRAMPercentage to 70-75% of container memory
- Use JRE Alpine images for smallest size
- Create non-root user
- Skip tests in Docker build (run in CI)
- Pin Java version
- Use actuator health checks for Spring Boot
Node.js Dockerfiles
Complete patterns for containerizing Node.js applications with npm, pnpm, and yarn.
Table of Contents
1. Base Image Selection 2. Pattern 1: npm (Standard) 3. Pattern 2: pnpm (Monorepos) 4. Pattern 3: yarn (Classic) 5. TypeScript Compilation 6. Common Node.js Pitfalls
Base Image Selection
Recommended Node.js base images:
| Image | Size | Use Case |
|---|---|---|
node:20-alpine | ~180MB | Production (recommended) |
node:20-slim | ~250MB | If Alpine compatibility issues |
node:20 | ~1GB | Development only |
gcr.io/distroless/nodejs20-debian12 | ~150MB | Maximum security |
Version pinning:
# ✅ Good: Exact version
FROM node:20.11.0-alpine
# ⚠️ OK: Minor version pinned
FROM node:20-alpine
# ❌ Bad: Unpredictable
FROM node:alpine
FROM node:latestBuilt-in node user: All official Node images include a node user (UID 1000) for non-root execution.
Pattern 1: npm (Standard)
Use when:
- Standard Node.js projects
- Single-repo applications
- No workspace/monorepo requirements
Multi-stage Dockerfile:
# syntax=docker/dockerfile:1
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package files
COPY package.json package-lock.json ./
# Install dependencies (use npm ci for reproducible builds)
RUN --mount=type=cache,target=/root/.npm \
npm ci
# Copy source code
COPY . .
# Build application (if TypeScript or bundling needed)
RUN npm run build
# Prune dev dependencies
RUN npm prune --omit=dev
# Runtime stage
FROM node:20-alpine
WORKDIR /app
# Copy production node_modules and built code
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
# Use built-in node user
USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]Key commands:
npm ci→ Clean install from package-lock.json (reproducible)npm install→ May update package-lock.json (avoid in Docker)npm prune --omit=dev→ Remove dev dependencies
Build command:
docker build -t myapp:latest .Expected size: 200-350MB
Pattern 2: pnpm (Monorepos)
Use when:
- Monorepo projects
- Need efficient disk usage (pnpm store)
- Turborepo or Nx workspaces
Multi-stage Dockerfile (monorepo-optimized):
# syntax=docker/dockerfile:1
FROM node:20-alpine AS builder
# Enable Corepack for pnpm
RUN corepack enable && corepack prepare pnpm@8.15.0 --activate
WORKDIR /app
# Copy workspace configuration
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
# Copy package.json files for all packages (leverage layer caching)
COPY packages/api/package.json ./packages/api/
COPY packages/shared/package.json ./packages/shared/
# Install all dependencies
RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
pnpm install --frozen-lockfile
# Copy source code
COPY . .
# Build specific package
RUN pnpm --filter=api build
# Prune to production dependencies for specific package
RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
pnpm --filter=api --prod deploy pruned
# Runtime stage
FROM node:20-alpine
WORKDIR /app
# Copy pruned production dependencies and built code
COPY --from=builder /app/pruned/node_modules ./node_modules
COPY --from=builder /app/pruned/dist ./dist
COPY --from=builder /app/pruned/package.json ./
# Use built-in node user
USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]pnpm-workspace.yaml:
packages:
- 'packages/*'Key pnpm commands:
pnpm install --frozen-lockfile→ Reproducible installpnpm --filter=api build→ Build specific packagepnpm --prod deploy pruned→ Create production-only deploy
Build command:
docker build -t myapp-api:latest .Expected size: 180-320MB
Pattern 3: yarn (Classic)
Use when:
- Existing yarn projects
- Yarn 1.x (classic) in use
Multi-stage Dockerfile:
# syntax=docker/dockerfile:1
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package files
COPY package.json yarn.lock ./
# Install dependencies
RUN --mount=type=cache,target=/usr/local/share/.cache/yarn \
yarn install --frozen-lockfile --production=false
# Copy source code
COPY . .
# Build application
RUN yarn build
# Install production dependencies only
RUN --mount=type=cache,target=/usr/local/share/.cache/yarn \
yarn install --frozen-lockfile --production=true --ignore-scripts --prefer-offline
# Runtime stage
FROM node:20-alpine
WORKDIR /app
# Copy production dependencies and built code
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
# Use built-in node user
USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]Key yarn commands:
yarn install --frozen-lockfile→ Reproducible install--production=false→ Install all deps (for building)--production=true→ Production deps only
Build command:
docker build -t myapp:latest .Expected size: 200-350MB
TypeScript Compilation
Pattern: Separate build and runtime stages
# syntax=docker/dockerfile:1
FROM node:20-alpine AS builder
WORKDIR /app
# Install dependencies
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci
# Copy TypeScript source
COPY tsconfig.json ./
COPY src ./src
# Compile TypeScript to JavaScript
RUN npm run build
# Prune dev dependencies (TypeScript no longer needed)
RUN npm prune --omit=dev
# Runtime stage (no TypeScript, just Node.js)
FROM node:20-alpine
WORKDIR /app
# Copy compiled JavaScript and production dependencies
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]tsconfig.json (example):
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}package.json scripts:
{
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "ts-node src/index.ts"
}
}Common Node.js Pitfalls
Pitfall 1: Using npm install Instead of npm ci
Problem: npm install may update package-lock.json, causing non-reproducible builds.
# ❌ Non-reproducible builds
RUN npm installSolution: Always use npm ci in Docker:
# ✅ Reproducible builds from package-lock.json
RUN npm ciPitfall 2: Not Pruning Dev Dependencies
Problem: Dev dependencies bloat production image.
# ❌ Includes dev dependencies (jest, typescript, etc.)
RUN npm ci
# Image size: 500MB+Solution: Prune dev dependencies or use --omit=dev:
# ✅ Production dependencies only
RUN npm ci --omit=dev
# OR
RUN npm ci && npm prune --omit=dev
# Image size: 250MBPitfall 3: Copying node_modules Before Install
Problem: Local node_modules (from different OS) conflicts with Docker install.
# ❌ Copies local node_modules (macOS) to Linux container
COPY . .
RUN npm ciSolution: Copy package files first, install, then copy source:
# ✅ Correct order
COPY package.json package-lock.json ./
RUN npm ci
COPY . .Better: Use .dockerignore to exclude node_modules:
node_modules/
dist/Pitfall 4: Not Using Cache Mounts for npm
Problem: Re-downloads packages every build.
# ❌ Re-downloads every time
RUN npm ciSolution: Use BuildKit cache mount:
# ✅ Persistent cache across builds
RUN --mount=type=cache,target=/root/.npm \
npm ciPitfall 5: Running as Root
Problem: Security risk if container is compromised.
# ❌ Runs as root (UID 0)
CMD ["node", "dist/index.js"]Solution: Use built-in node user:
# ✅ Runs as non-root (UID 1000)
USER node
CMD ["node", "dist/index.js"]Pitfall 6: Not Setting NODE_ENV
Problem: Express and other frameworks behave differently in production.
# ❌ Defaults to development mode
CMD ["node", "dist/index.js"]Solution: Set NODE_ENV=production:
# ✅ Production optimizations enabled
ENV NODE_ENV=production
CMD ["node", "dist/index.js"]Express.js Complete Example
Production-ready Express API:
# syntax=docker/dockerfile:1
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package files
COPY package.json package-lock.json ./
# Install dependencies
RUN --mount=type=cache,target=/root/.npm \
npm ci
# Copy source code
COPY . .
# Build application (if TypeScript)
RUN npm run build
# Prune dev dependencies
RUN npm prune --omit=dev
# Runtime stage
FROM node:20-alpine
WORKDIR /app
# Copy production files
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
# Use built-in node user
USER node
# Production environment
ENV NODE_ENV=production
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
EXPOSE 3000
CMD ["node", "dist/index.js"]src/index.ts:
import express from 'express';
const app = express();
const PORT = process.env.PORT || 3000;
app.get('/health', (req, res) => {
res.status(200).json({ status: 'healthy' });
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});Expected size: 220-300MB
Next.js Complete Example
Production-optimized Next.js application:
# syntax=docker/dockerfile:1
FROM node:20-alpine AS deps
WORKDIR /app
# Copy package files
COPY package.json package-lock.json ./
# Install dependencies
RUN --mount=type=cache,target=/root/.npm \
npm ci
# Builder stage
FROM node:20-alpine AS builder
WORKDIR /app
# Copy dependencies
COPY --from=deps /app/node_modules ./node_modules
# Copy source code
COPY . .
# Build Next.js application
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build
# Runtime stage
FROM node:20-alpine
WORKDIR /app
# Copy necessary files
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
# Use built-in node user
USER node
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
EXPOSE 3000
CMD ["node", "server.js"]next.config.js (required for standalone):
module.exports = {
output: 'standalone',
};Expected size: 150-250MB (standalone output is optimized)
NestJS Complete Example
# syntax=docker/dockerfile:1
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package files
COPY package.json package-lock.json ./
# Install dependencies
RUN --mount=type=cache,target=/root/.npm \
npm ci
# Copy source code
COPY . .
# Build NestJS application
RUN npm run build
# Prune dev dependencies
RUN npm prune --omit=dev
# Runtime stage
FROM node:20-alpine
WORKDIR /app
# Copy production files
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
# Use built-in node user
USER node
ENV NODE_ENV=production
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
EXPOSE 3000
CMD ["node", "dist/main.js"]Expected size: 250-350MB
Private npm Registry Example
Using BuildKit secret mount for NPM_TOKEN:
# syntax=docker/dockerfile:1
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package files
COPY package.json package-lock.json ./
# Install from private registry using secret
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
--mount=type=cache,target=/root/.npm \
npm ci
# Copy source and build
COPY . .
RUN npm run build
RUN npm prune --omit=dev
# Runtime stage
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
USER node
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", "dist/index.js"].npmrc file (local, not committed):
//registry.npmjs.org/:_authToken=${NPM_TOKEN}Build command:
# Create temporary .npmrc with token
echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc.tmp
# Build with secret mount
docker buildx build --secret id=npmrc,src=.npmrc.tmp -t myapp:latest .
# Clean up
rm .npmrc.tmpAlternative: Environment variable substitution
RUN --mount=type=secret,id=npm_token \
echo "//registry.npmjs.org/:_authToken=$(cat /run/secrets/npm_token)" > ~/.npmrc && \
npm ci && \
rm ~/.npmrcSummary
Choose package manager based on needs:
| Package Manager | Use Case | Build Time | Cache Efficiency | Monorepo Support |
|---|---|---|---|---|
| npm | Standard projects | Medium | Good | Limited |
| pnpm | Monorepos, efficiency | Fast | Excellent | Excellent |
| yarn | Existing yarn projects | Medium | Good | Good |
Key takeaways:
- Always use multi-stage builds
- Use
npm ci(notnpm install) for reproducible builds - Use BuildKit cache mounts for package managers
- Prune dev dependencies in production
- Use built-in
nodeuser (UID 1000) - Set
NODE_ENV=production - Use Alpine variants for smaller images
- Copy package files before source code (layer caching)
- Exclude node_modules in .dockerignore
Python Dockerfiles
Complete patterns for containerizing Python applications with pip, poetry, and uv.
Table of Contents
1. Base Image Selection 2. Pattern 1: pip (Simple) 3. Pattern 2: Poetry (Production) 4. Pattern 3: uv (Fastest) 5. Virtual Environment Best Practices 6. Common Python Pitfalls
Base Image Selection
Recommended Python base images:
| Image | Size | Use Case |
|---|---|---|
python:3.12-slim | ~150MB | Production (recommended) |
python:3.12-alpine | ~50MB | Smallest (compilation issues possible) |
python:3.12 | ~1GB | Development only |
gcr.io/distroless/python3-debian12 | ~60MB | Maximum security (pure Python only) |
Version pinning:
# ✅ Good: Exact version
FROM python:3.12.1-slim
# ⚠️ OK: Minor version pinned
FROM python:3.12-slim
# ❌ Bad: Unpredictable
FROM python:3-slim
FROM python:latestPattern 1: pip (Simple)
Use when:
- Small projects with simple dependencies
- No complex dependency resolution needed
- Quick prototypes
Single-stage Dockerfile:
# syntax=docker/dockerfile:1
FROM python:3.12-slim
WORKDIR /app
# Install dependencies with cache mount
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Create non-root user
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
USER appuser
EXPOSE 8000
CMD ["python", "app.py"]requirements.txt format:
# Pin all versions
fastapi==0.109.0
uvicorn[standard]==0.27.0
pydantic==2.5.3Build command:
docker build -t myapp:latest .Expected size: 200-300MB
Pattern 2: Poetry (Production)
Use when:
- Production applications
- Complex dependency management
- Lock file reproducibility required
Multi-stage Dockerfile:
# syntax=docker/dockerfile:1
FROM python:3.12-slim AS builder
# Install poetry
RUN --mount=type=cache,target=/root/.cache/pip \
pip install poetry==1.7.1
# Configure poetry to not create virtual env (we handle it manually)
ENV POETRY_NO_INTERACTION=1 \
POETRY_VIRTUALENVS_IN_PROJECT=1 \
POETRY_VIRTUALENVS_CREATE=1 \
POETRY_CACHE_DIR=/tmp/poetry_cache
WORKDIR /app
# Copy dependency files
COPY pyproject.toml poetry.lock ./
# Install dependencies
RUN --mount=type=cache,target=/tmp/poetry_cache \
poetry install --no-root --only main
# Runtime stage
FROM python:3.12-slim
WORKDIR /app
# Copy virtual environment from builder
COPY --from=builder /app/.venv /app/.venv
# Copy application code
COPY . .
# Create non-root user
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
USER appuser
# Activate virtual environment
ENV PATH="/app/.venv/bin:$PATH"
EXPOSE 8000
CMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]Alternative: Export to requirements.txt
# In builder stage
RUN poetry export -f requirements.txt --output requirements.txt --without-hashes
# Then install with pip (faster than poetry install)
RUN --mount=type=cache,target=/root/.cache/pip \
python -m venv /opt/venv && \
/opt/venv/bin/pip install --no-cache-dir -r requirements.txtBuild command:
docker build -t myapp:latest .Expected size: 250-400MB
Pattern 3: uv (Fastest)
Use when:
- Large dependency trees (10-100x faster than pip)
- CI/CD pipelines (speed critical)
- Modern Python projects
Multi-stage Dockerfile:
# syntax=docker/dockerfile:1
FROM python:3.12-slim AS builder
# Install uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
WORKDIR /app
# Copy dependency files
COPY pyproject.toml uv.lock ./
# Install dependencies
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev
# Copy application code
COPY . .
# Runtime stage
FROM python:3.12-slim
WORKDIR /app
# Copy application and dependencies from builder
COPY --from=builder /app /app
# Create non-root user
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
USER appuser
# Activate virtual environment
ENV PATH="/app/.venv/bin:$PATH"
EXPOSE 8000
CMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]pyproject.toml example:
[project]
name = "myapp"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.109.0",
"uvicorn[standard]>=0.27.0",
]
[tool.uv]
dev-dependencies = [
"pytest>=7.4.0",
"black>=23.12.0",
]Build command:
docker build -t myapp:latest .Expected size: 250-400MB
Speed comparison:
- pip: ~60 seconds (cold cache)
- poetry: ~45 seconds (cold cache)
- uv: ~3-6 seconds (cold cache)
Virtual Environment Best Practices
Why use virtual environments in Docker?
- Dependency isolation
- Explicit Python path
- Compatible with local development
- Easier to copy between stages
Pattern: Separate virtual environment
# Create virtual environment in builder
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
# Install dependencies in virtual env
RUN pip install -r requirements.txt
# In runtime stage
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"Alternative: Poetry in-project venv
ENV POETRY_VIRTUALENVS_IN_PROJECT=1
RUN poetry install
# Runtime
COPY --from=builder /app/.venv /app/.venv
ENV PATH="/app/.venv/bin:$PATH"Common Python Pitfalls
Pitfall 1: Compiled Dependencies on Alpine
Problem: Alpine uses musl libc, not glibc. Wheels (pre-compiled packages) don't work.
# ❌ This will compile numpy from source (slow, large image)
FROM python:3.12-alpine
RUN pip install numpy pandasSolution: Use slim base or install build dependencies:
# ✅ Use slim (glibc-based)
FROM python:3.12-slim
RUN pip install numpy pandas
# OR install Alpine build deps (not recommended)
FROM python:3.12-alpine
RUN apk add --no-cache gcc musl-dev python3-dev
RUN pip install numpy pandasPitfall 2: Missing System Dependencies
Problem: Some Python packages require system libraries.
# ❌ psycopg2 needs PostgreSQL client libraries
FROM python:3.12-slim
RUN pip install psycopg2
# ERROR: pg_config not foundSolution: Install system packages first:
# ✅ Install PostgreSQL client libraries
FROM python:3.12-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq-dev \
gcc \
&& rm -rf /var/lib/apt/lists/*
RUN pip install psycopg2Better: Use binary wheel variants:
# ✅ psycopg2-binary includes compiled libraries
FROM python:3.12-slim
RUN pip install psycopg2-binaryPitfall 3: .pyc Files Bloat
Problem: Bytecode cache files increase image size.
Solution: Disable .pyc generation:
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1Pitfall 4: pip Cache Not Used
Problem: Without cache mounts, pip re-downloads every build.
# ❌ Re-downloads every time
RUN pip install -r requirements.txtSolution: Use BuildKit cache mount:
# ✅ Persistent cache across builds
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txtPitfall 5: Development Dependencies in Production
Problem: Installing dev dependencies bloats production image.
# ❌ Installs pytest, black, etc. in production
RUN poetry installSolution: Install production dependencies only:
# ✅ Skip dev dependencies
RUN poetry install --only main
# OR with pip
RUN pip install -r requirements.txt
# (requirements.txt should not include dev deps)FastAPI Complete Example
Production-ready FastAPI application:
# syntax=docker/dockerfile:1
FROM python:3.12-slim AS builder
# Install uv (fastest)
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
WORKDIR /app
# Copy dependency files
COPY pyproject.toml uv.lock ./
# Install dependencies
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev
# Copy application
COPY . .
# Runtime stage
FROM python:3.12-slim
WORKDIR /app
# Copy application and venv
COPY --from=builder /app /app
# Install runtime system dependencies (if needed)
# RUN apt-get update && apt-get install -y --no-install-recommends \
# libpq5 \
# && rm -rf /var/lib/apt/lists/*
# Create non-root user
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
USER appuser
# Activate virtual environment
ENV PATH="/app/.venv/bin:$PATH"
# Python optimizations
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD python -c "import requests; requests.get('http://localhost:8000/health')" || exit 1
EXPOSE 8000
CMD ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]Expected size: 280-380MB
Django Complete Example
# syntax=docker/dockerfile:1
FROM python:3.12-slim AS builder
# Install poetry
RUN --mount=type=cache,target=/root/.cache/pip \
pip install poetry==1.7.1
WORKDIR /app
# Copy dependency files
COPY pyproject.toml poetry.lock ./
# Install dependencies
ENV POETRY_NO_INTERACTION=1 \
POETRY_VIRTUALENVS_IN_PROJECT=1 \
POETRY_VIRTUALENVS_CREATE=1 \
POETRY_CACHE_DIR=/tmp/poetry_cache
RUN --mount=type=cache,target=/tmp/poetry_cache \
poetry install --no-root --only main
# Runtime stage
FROM python:3.12-slim
WORKDIR /app
# Install PostgreSQL client library
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq5 \
&& rm -rf /var/lib/apt/lists/*
# Copy virtual environment
COPY --from=builder /app/.venv /app/.venv
# Copy application
COPY . .
# Collect static files
ENV PATH="/app/.venv/bin:$PATH"
RUN python manage.py collectstatic --noinput
# Create non-root user
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
USER appuser
# Django settings
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
DJANGO_SETTINGS_MODULE=config.settings.production
EXPOSE 8000
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "4"]Expected size: 350-450MB
Summary
Choose pattern based on needs:
| Pattern | Use Case | Build Time | Image Size | Complexity |
|---|---|---|---|---|
| pip | Simple apps, prototypes | Fast | 200-300MB | Low |
| poetry | Production, lock files | Medium | 250-400MB | Medium |
| uv | Large deps, speed critical | Very fast | 250-400MB | Low |
Key takeaways:
- Always use multi-stage builds for production
- Use BuildKit cache mounts for package managers
- Pin Python version and all dependencies
- Create non-root user
- Use slim base images (not alpine for compiled deps)
- Disable .pyc file generation
- Install only production dependencies
Rust Dockerfiles
Complete patterns for containerizing Rust applications with ultra-small static binaries.
Table of Contents
1. Why Rust Excels at Docker 2. Base Image Selection 3. Pattern 1: Scratch Base (Smallest) 4. Pattern 2: Distroless Static 5. Pattern 3: Alpine Runtime 6. Build Optimization Techniques 7. Common Rust Pitfalls
Why Rust Excels at Docker
Rust's advantages for containerization:
- Static binaries: musl linking produces zero-dependency executables
- Ultra-small: 5-15MB final images with scratch base
- Memory safe: No runtime overhead, maximum performance
- No runtime: Unlike Python/Node.js, just the binary
- Cross-compilation: Build for any platform from any platform
Typical image sizes:
- Rust + scratch: 5-15MB
- Rust + distroless/static: 8-18MB
- Rust + alpine: 12-25MB
- Go equivalent: 10-30MB
- Python equivalent: 200-400MB
Base Image Selection
Recommended Rust base images:
| Build Stage | Runtime Stage | Final Size | Use Case |
|---|---|---|---|
rust:1.75-alpine | scratch | 5-15MB | Production (recommended) |
rust:1.75-alpine | gcr.io/distroless/static-debian12 | 8-18MB | Need CA certs built-in |
rust:1.75-alpine | alpine:3.19 | 12-25MB | Need shell for debugging |
rust:1.75 | debian:bookworm-slim | 20-40MB | Dynamic linking needed |
Version pinning:
# ✅ Good: Exact version
FROM rust:1.75.0-alpine
# ⚠️ OK: Minor version pinned
FROM rust:1.75-alpine
# ❌ Bad: Unpredictable
FROM rust:alpine
FROM rust:latestPattern 1: Scratch Base (Smallest)
Use when:
- Pure Rust code
- Production deployments
- Absolute minimum size required
- Maximum security posture
Multi-stage Dockerfile with musl:
# syntax=docker/dockerfile:1
FROM rust:1.75-alpine AS builder
# Install musl build tools
RUN apk add --no-cache musl-dev
WORKDIR /app
# Cache dependencies layer (dummy build)
COPY Cargo.toml Cargo.lock ./
RUN --mount=type=cache,target=/usr/local/cargo/registry \
mkdir src && \
echo "fn main() {}" > src/main.rs && \
cargo build --release --target x86_64-unknown-linux-musl && \
rm -rf src
# Build actual application
COPY src ./src
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/app/target \
cargo build --release --target x86_64-unknown-linux-musl && \
strip target/x86_64-unknown-linux-musl/release/app
# Runtime stage: scratch (empty base)
FROM scratch
# Copy binary only
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/app /app
# Run as non-root (numeric UID only)
USER 1000:1000
EXPOSE 8080
ENTRYPOINT ["/app"]Key features:
- musl static linking → No libc dependencies
- scratch base → 0 bytes overhead
- Dummy build → Caches dependencies
- strip → Further reduces binary size
- Final image: 5-15MB
Build command:
docker build -t myapp:latest .Expected size: 5-15MB
Pattern 2: Distroless Static
Use when:
- Need CA certificates for HTTPS
- Want minimal base with some structure
- Security-critical production
Multi-stage Dockerfile:
# syntax=docker/dockerfile:1
FROM rust:1.75-alpine AS builder
RUN apk add --no-cache musl-dev
WORKDIR /app
# Cache dependencies
COPY Cargo.toml Cargo.lock ./
RUN --mount=type=cache,target=/usr/local/cargo/registry \
mkdir src && \
echo "fn main() {}" > src/main.rs && \
cargo build --release --target x86_64-unknown-linux-musl && \
rm -rf src
# Build application
COPY src ./src
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/app/target \
cargo build --release --target x86_64-unknown-linux-musl && \
strip target/x86_64-unknown-linux-musl/release/app
# Runtime stage: distroless static
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/app /app/app
# Use built-in nonroot user
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/app/app"]Benefits over scratch:
- Includes CA certificates (for HTTPS)
- Includes timezone data
- Includes /etc/passwd (for nonroot user)
- Still minimal (2MB base)
Build command:
docker build -t myapp:latest .Expected size: 8-18MB
Pattern 3: Alpine Runtime
Use when:
- Need shell access for debugging
- Need runtime utilities
- Slightly larger image acceptable
Multi-stage Dockerfile:
# syntax=docker/dockerfile:1
FROM rust:1.75-alpine AS builder
RUN apk add --no-cache musl-dev
WORKDIR /app
# Cache dependencies
COPY Cargo.toml Cargo.lock ./
RUN --mount=type=cache,target=/usr/local/cargo/registry \
mkdir src && \
echo "fn main() {}" > src/main.rs && \
cargo build --release --target x86_64-unknown-linux-musl && \
rm -rf src
# Build application
COPY src ./src
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/app/target \
cargo build --release --target x86_64-unknown-linux-musl
# Runtime stage: Alpine
FROM alpine:3.19
# Install CA certificates
RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/app /app/app
# Create non-root user
RUN addgroup -g 1000 appuser && \
adduser -D -u 1000 -G appuser appuser && \
chown -R appuser:appuser /app
USER appuser
EXPOSE 8080
ENTRYPOINT ["/app/app"]When to use Alpine runtime:
- Development/staging environments
- Need debugging tools
- Need to install runtime packages
Build command:
docker build -t myapp:latest .Expected size: 12-25MB
Build Optimization Techniques
Technique 1: Dependency Caching
Problem: Cargo recompiles all dependencies on every code change.
# ❌ Recompiles dependencies every time
COPY . .
RUN cargo build --releaseSolution: Dummy build to cache dependencies:
# ✅ Cached dependencies if Cargo.toml unchanged
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && \
echo "fn main() {}" > src/main.rs && \
cargo build --release && \
rm -rf src
# Now copy real source (dependencies already cached)
COPY src ./src
RUN cargo build --releaseSpeed improvement: 10-50x faster rebuilds.
Technique 2: BuildKit Cache Mounts
Dual cache mounts:
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/app/target \
cargo build --release --target x86_64-unknown-linux-muslWhat's cached:
/usr/local/cargo/registry→ Downloaded crates/app/target→ Compiled artifacts
Speed improvement: 5-10x faster rebuilds.
Technique 3: Binary Stripping
Default build:
RUN cargo build --release
# Binary size: 15MBStripped build:
RUN cargo build --release && \
strip target/release/app
# Binary size: 8MB (47% smaller)Alternative: Cargo.toml profile:
[profile.release]
strip = true
lto = true
codegen-units = 1
panic = "abort"Technique 4: LTO and Size Optimization
Cargo.toml optimization profile:
[profile.release]
opt-level = "z" # Optimize for size
lto = true # Link-time optimization
codegen-units = 1 # Single codegen unit (slower build, smaller binary)
strip = true # Strip symbols
panic = "abort" # Smaller panic handlerBinary size reduction: Up to 60% smaller than default release build.
Technique 5: Multi-Architecture Builds
Build for multiple platforms:
ARG TARGETARCH
FROM rust:1.75-alpine AS builder
RUN apk add --no-cache musl-dev
# Install cross-compilation target
RUN case ${TARGETARCH} in \
"amd64") RUST_TARGET=x86_64-unknown-linux-musl ;; \
"arm64") RUST_TARGET=aarch64-unknown-linux-musl ;; \
*) echo "Unsupported architecture" && exit 1 ;; \
esac && \
rustup target add ${RUST_TARGET}
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
RUN --mount=type=cache,target=/usr/local/cargo/registry \
mkdir src && \
echo "fn main() {}" > src/main.rs && \
cargo build --release --target ${RUST_TARGET} && \
rm -rf src
COPY src ./src
RUN --mount=type=cache,target=/usr/local/cargo/registry \
cargo build --release --target ${RUST_TARGET}
FROM scratch
ARG TARGETARCH
COPY --from=builder /app/target/${RUST_TARGET}/release/app /app
USER 1000:1000
ENTRYPOINT ["/app"]Build command:
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest .Common Rust Pitfalls
Pitfall 1: Dynamic Linking with Scratch
Problem: Default Rust build dynamically links libc.
# ❌ This will fail at runtime
FROM rust:1.75-alpine AS builder
RUN cargo build --release
FROM scratch
COPY --from=builder /app/target/release/app /app
ENTRYPOINT ["/app"]
# Runtime error: binary needs libcSolution: Use musl target for static linking:
# ✅ Static binary
RUN apk add --no-cache musl-dev
RUN cargo build --release --target x86_64-unknown-linux-musl
FROM scratch
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/app /app
ENTRYPOINT ["/app"]Pitfall 2: Missing CA Certificates for HTTPS
Problem: HTTPS calls fail without CA certificates.
FROM scratch
COPY --from=builder /app/target/release/app /app
ENTRYPOINT ["/app"]
# Runtime error: certificate signed by unknown authoritySolution 1: Copy CA certs from builder:
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app/target/release/app /app
ENTRYPOINT ["/app"]Solution 2: Use distroless (includes CA certs):
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/target/release/app /app
ENTRYPOINT ["/app"]Pitfall 3: Not Caching Dependencies
Problem: Rebuilds all dependencies on every code change.
# ❌ Recompiles everything
COPY . .
RUN cargo build --release
# Takes 10+ minutes every buildSolution: Dummy build for dependency caching:
# ✅ Dependencies cached
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs && \
cargo build --release && \
rm -rf src
COPY src ./src
RUN cargo build --release
# Takes 30 seconds for incremental buildsPitfall 4: Large Binary Due to Debug Info
Problem: Binary includes debug symbols, stack traces, etc.
# ❌ 15MB binary
RUN cargo build --releaseSolution: Strip symbols:
# ✅ 8MB binary
RUN cargo build --release && \
strip target/release/appBetter: Configure in Cargo.toml:
[profile.release]
strip = truePitfall 5: Slow Builds Without LTO
Problem: Cargo builds quickly but binaries are larger.
Solution: Enable LTO in Cargo.toml:
[profile.release]
lto = true # Link-time optimization
codegen-units = 1 # Better optimization, slower buildTrade-off:
- Build time: 2-3x slower
- Binary size: 20-40% smaller
- Runtime performance: 5-15% faster
Actix-Web Complete Example
Production-ready Actix-web API:
# syntax=docker/dockerfile:1
FROM rust:1.75-alpine AS builder
RUN apk add --no-cache musl-dev
WORKDIR /app
# Cache dependencies
COPY Cargo.toml Cargo.lock ./
RUN --mount=type=cache,target=/usr/local/cargo/registry \
mkdir src && \
echo "fn main() {}" > src/main.rs && \
cargo build --release --target x86_64-unknown-linux-musl && \
rm -rf src
# Build application
COPY src ./src
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/app/target \
cargo build --release --target x86_64-unknown-linux-musl && \
strip target/x86_64-unknown-linux-musl/release/app
# Runtime stage
FROM scratch
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/app /app
USER 1000:1000
EXPOSE 8080
ENTRYPOINT ["/app"]Cargo.toml:
[package]
name = "app"
version = "0.1.0"
edition = "2021"
[dependencies]
actix-web = "4.4"
tokio = { version = "1.35", features = ["full"] }
[profile.release]
opt-level = "z"
lto = true
codegen-units = 1
strip = true
panic = "abort"src/main.rs:
use actix_web::{web, App, HttpResponse, HttpServer};
async fn health() -> HttpResponse {
HttpResponse::Ok().body("OK")
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.route("/health", web::get().to(health))
})
.bind("0.0.0.0:8080")?
.run()
.await
}Expected size: 8-12MB
Rocket Framework Complete Example
# syntax=docker/dockerfile:1
FROM rust:1.75-alpine AS builder
RUN apk add --no-cache musl-dev
WORKDIR /app
# Cache dependencies
COPY Cargo.toml Cargo.lock ./
RUN --mount=type=cache,target=/usr/local/cargo/registry \
mkdir src && \
echo "fn main() {}" > src/main.rs && \
cargo build --release --target x86_64-unknown-linux-musl && \
rm -rf src
# Build application
COPY src ./src
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/app/target \
cargo build --release --target x86_64-unknown-linux-musl
# Runtime stage
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/app /app/app
USER nonroot:nonroot
EXPOSE 8000
ENTRYPOINT ["/app/app"]Cargo.toml:
[package]
name = "app"
version = "0.1.0"
edition = "2021"
[dependencies]
rocket = "0.5"
[profile.release]
opt-level = "z"
lto = true
strip = trueExpected size: 10-15MB
Private Crate Registry Example
Using BuildKit secret mount for CARGO_TOKEN:
# syntax=docker/dockerfile:1
FROM rust:1.75-alpine AS builder
RUN apk add --no-cache musl-dev
WORKDIR /app
# Configure cargo to use private registry
RUN --mount=type=secret,id=cargo_token \
mkdir -p ~/.cargo && \
echo "[registries.private]" > ~/.cargo/config.toml && \
echo "index = \"https://my-registry.com/git/index\"" >> ~/.cargo/config.toml && \
echo "[registry]" >> ~/.cargo/config.toml && \
echo "token = \"$(cat /run/secrets/cargo_token)\"" >> ~/.cargo/config.toml
# Cache dependencies
COPY Cargo.toml Cargo.lock ./
RUN --mount=type=secret,id=cargo_token \
--mount=type=cache,target=/usr/local/cargo/registry \
mkdir src && \
echo "fn main() {}" > src/main.rs && \
cargo build --release --target x86_64-unknown-linux-musl && \
rm -rf src
# Build application
COPY src ./src
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/app/target \
cargo build --release --target x86_64-unknown-linux-musl
FROM scratch
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/app /app
USER 1000:1000
ENTRYPOINT ["/app"]Build command:
echo "my_cargo_token" > cargo_token.txt
docker buildx build --secret id=cargo_token,src=cargo_token.txt -t myapp:latest .
rm cargo_token.txtSummary
Rust Dockerfile patterns ranked:
| Pattern | Size | Security | Debug-ability | Use Case |
|---|---|---|---|---|
| Scratch | 5-15MB | Highest | None | Production (smallest) |
| Distroless static | 8-18MB | Highest | None | Production (with CA certs) |
| Alpine | 12-25MB | High | Shell access | Development, debugging |
Key takeaways:
- Always use multi-stage builds
- Use musl target for static binaries (
x86_64-unknown-linux-musl) - Enable LTO and size optimizations in Cargo.toml
- Strip symbols with
stripcommand orstrip = truein profile - Use BuildKit cache mounts for cargo registry and target
- Dummy build caches dependencies (10-50x faster rebuilds)
- Use scratch base for smallest images (5-15MB)
- Copy CA certificates if making HTTPS calls
- Configure release profile for size optimization
- Final images: 5-15MB (smallest of any language)
Cargo.toml release profile (copy-paste):
[profile.release]
opt-level = "z" # Optimize for size
lto = true # Link-time optimization
codegen-units = 1 # Single codegen unit
strip = true # Strip symbols
panic = "abort" # Smaller panic handler#!/bin/bash
#
# Docker Image Size Analysis Script
#
# Compares image sizes before and after optimization.
# Shows layer-by-layer breakdown and size differences.
#
# Usage:
# ./analyze_image_size.sh image:tag
# ./analyze_image_size.sh image:before image:after
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Function to format bytes to human-readable
format_bytes() {
local bytes=$1
if [ $bytes -lt 1024 ]; then
echo "${bytes}B"
elif [ $bytes -lt 1048576 ]; then
echo "$(awk "BEGIN {printf \"%.1f\", $bytes/1024}")KB"
elif [ $bytes -lt 1073741824 ]; then
echo "$(awk "BEGIN {printf \"%.1f\", $bytes/1048576}")MB"
else
echo "$(awk "BEGIN {printf \"%.2f\", $bytes/1073741824}")GB"
fi
}
# Function to analyze single image
analyze_image() {
local image=$1
echo -e "${BLUE}=== Analyzing: $image ===${NC}\n"
# Check if image exists
if ! docker image inspect "$image" &>/dev/null; then
echo -e "${RED}Error: Image '$image' not found${NC}"
return 1
fi
# Get image size
local size=$(docker image inspect "$image" --format='{{.Size}}')
local size_hr=$(format_bytes $size)
echo -e "${GREEN}Total Size:${NC} $size_hr ($size bytes)"
# Get layer count
local layers=$(docker history "$image" --no-trunc --format='{{.ID}}' | wc -l)
echo -e "${GREEN}Layer Count:${NC} $layers"
# Show layer breakdown
echo -e "\n${YELLOW}Layer Breakdown:${NC}"
docker history "$image" --human --no-trunc --format='table {{.Size}}\t{{.CreatedBy}}' | head -n 20
# Get base image if FROM instruction found
local base=$(docker history "$image" --human --no-trunc | tail -n 1 | awk '{print $1}')
if [ "$base" != "<missing>" ]; then
echo -e "\n${GREEN}Base Image Size:${NC} $base"
fi
echo ""
}
# Function to compare two images
compare_images() {
local image1=$1
local image2=$2
echo -e "${BLUE}=== Comparing Images ===${NC}\n"
# Get sizes
local size1=$(docker image inspect "$image1" --format='{{.Size}}')
local size2=$(docker image inspect "$image2" --format='{{.Size}}')
local size1_hr=$(format_bytes $size1)
local size2_hr=$(format_bytes $size2)
echo -e "${YELLOW}Image 1:${NC} $image1"
echo -e "${GREEN}Size:${NC} $size1_hr ($size1 bytes)"
echo ""
echo -e "${YELLOW}Image 2:${NC} $image2"
echo -e "${GREEN}Size:${NC} $size2_hr ($size2 bytes)"
echo ""
# Calculate difference
local diff=$(($size2 - $size1))
local diff_hr=$(format_bytes ${diff#-}) # Remove negative sign for formatting
local diff_pct=$(awk "BEGIN {printf \"%.1f\", ($diff / $size1) * 100}")
if [ $diff -lt 0 ]; then
echo -e "${GREEN}Size Reduction:${NC} $diff_hr (${diff_pct#-}% smaller)"
elif [ $diff -gt 0 ]; then
echo -e "${RED}Size Increase:${NC} $diff_hr (${diff_pct}% larger)"
else
echo -e "${YELLOW}Same Size${NC}"
fi
echo ""
}
# Main script
if [ $# -eq 1 ]; then
# Single image analysis
analyze_image "$1"
elif [ $# -eq 2 ]; then
# Compare two images
if ! docker image inspect "$1" &>/dev/null; then
echo -e "${RED}Error: Image '$1' not found${NC}"
exit 1
fi
if ! docker image inspect "$2" &>/dev/null; then
echo -e "${RED}Error: Image '$2' not found${NC}"
exit 1
fi
analyze_image "$1"
echo ""
analyze_image "$2"
echo ""
compare_images "$1" "$2"
else
echo "Usage:"
echo " $0 image:tag # Analyze single image"
echo " $0 image:before image:after # Compare two images"
echo ""
echo "Examples:"
echo " $0 myapp:latest"
echo " $0 myapp:before myapp:after"
exit 1
fi
Related skills
FAQ
How do I shrink a container image?
Use multi-stage builds to separate build from runtime, then use distroless or slim base images and static linking where possible.
How do I keep secrets out of image layers?
Use BuildKit secret mounts (--mount=type=secret) instead of copying tokens into RUN commands or layer history.