Dockerfile Generator
Generates production-ready Dockerfiles with multi-stage builds, layer caching, security hardening (non-root users, minimal base images), health checks, and automatic .dockerignore creation. Validates output using devops-skills:dockerfile-validator with iterative error fixing. Use when a user wants to create, generate, write, or build a Dockerfile or docker image, containerize an app, set up a container, or needs a Dockerfile for Node.js, Python, Go, Java, or another language. Also applies when the user says 'containerize my app', 'docker-compose setup', 'container setup', or 'optimize my Docker build'.
npx skills add github:pantheon-org/tekhne --skill infrastructure--dockerfile--generatorOverview
Generates production-ready Dockerfiles with security, optimization, and best practices built-in: multi-stage builds, security-hardened configurations, optimized layer structures, automatic validation, and iterative error fixing.
Philosophy
- Secure and reproducible by default: pinned tags, non-root user, minimal base images, no secrets in the image.
- Order layers so the cache does the work: dependencies before application code.
- Every generated Dockerfile ships with a .dockerignore and is validated before delivery.
- Iterate on validator findings instead of suppressing them, and justify any warning left in place.
When to Use
- The user wants to create, generate or write a Dockerfile for Node.js, Python, Go, Java or another language.
- The user asks to containerize an app, set up a container or optimize a Docker build.
- A multi-stage build, layer caching or security hardening is needed.
- A .dockerignore is needed alongside the Dockerfile.
When Not to Use
- Validating an existing Dockerfile without changing it (use devops-skills:dockerfile-validator).
- Building or running containers, or debugging a running container.
- Managing image registries.
- Generating Kubernetes manifests or Helm charts (use k8s-generator or helm-generator).
Dockerfile Generation Workflow
Follow this workflow when generating Dockerfiles. Adapt based on user needs:
Stage 1: Gather Requirements
Objective: Understand what needs to be containerized and gather all necessary information.
Information to Collect (use AskUserQuestion if missing or unclear):
- Language/version, application type, framework, entry point
- Package manager, system dependencies, build-time vs runtime deps
- Port(s), environment variables, config files, health check endpoint, volume mounts
- Build/test commands, compilation needs, static asset generation
- Image size constraints, security requirements, resource constraints
Stage 2: Framework/Library Documentation Lookup (if needed)
Objective: Research framework-specific containerization patterns and best practices.
When to Perform This Stage:
- User mentions a specific framework (Next.js, Django, FastAPI, Spring Boot, etc.)
- Application has complex build requirements
- Need guidance on framework-specific optimization
Research Process:
-
Try context7 MCP first (preferred):
Use mcp__context7__resolve-library-id with the framework name Examples: - "next.js" for Next.js applications - "django" for Django applications - "fastapi" for FastAPI applications - "spring-boot" for Spring Boot applications - "express" for Express.js applications Then use mcp__context7__get-library-docs with: - context7CompatibleLibraryID from resolve step - topic: "docker deployment production build" - page: 1 (fetch additional pages if needed) -
Fallback to WebSearch if context7 fails:
Search query pattern: "<framework>" "<version>" dockerfile best practices production 2025 Examples: - "Next.js 14 dockerfile best practices production 2025" - "FastAPI dockerfile best practices production 2025" - "Spring Boot 3 dockerfile best practices production 2025" -
Extract key information:
- Recommended base images
- Build optimization techniques
- Framework-specific environment variables
- Production vs development configurations
- Security considerations
Stage 3: Generate Dockerfile
Objective: Create a production-ready, multi-stage Dockerfile following best practices.
Core Principles:
-
Multi-Stage Builds (REQUIRED for compiled languages, RECOMMENDED for all):
- Separate build stage from runtime stage
- Keep build tools out of final image
- Copy only necessary artifacts
-
Security Hardening (REQUIRED):
- Use specific version tags (NEVER use :latest)
- Run as non-root user (create dedicated user)
- Use minimal base images (alpine, distroless)
- No hardcoded secrets
- Scan base images for vulnerabilities
-
Layer Optimization (REQUIRED):
- Order instructions from least to most frequently changing
- Copy dependency files before application code
- Combine related RUN commands with &&
- Clean up package manager caches in same layer
- Leverage build cache effectively
-
Production Readiness (REQUIRED):
- Add HEALTHCHECK for services
- Use exec form for ENTRYPOINT/CMD
- Set WORKDIR to absolute paths
- Document exposed ports with EXPOSE
Language-Specific Templates:
For detailed templates and examples, see:
- Node.js/Next.js:
references/language_specific_guides.md- Node.js section - Python/FastAPI:
references/language_specific_guides.md- Python section - Go:
references/language_specific_guides.md- Go section - Java/Spring Boot:
references/language_specific_guides.md- Java section
Quick Template Selection:
- Node.js: Use for JavaScript/TypeScript applications (Express, Next.js, etc.)
- Python: Use for Python applications (FastAPI, Django, Flask)
- Go: Use for Go applications (minimal images with distroless)
- Java: Use for Spring Boot, Quarkus, or other Java frameworks
- Generic: Adapt patterns from references for other languages
Always Include:
- Syntax directive:
# syntax=docker/dockerfile:1 - Multi-stage build (build + production stages)
- Non-root user creation and usage
- HEALTHCHECK for services (if applicable)
- Proper WORKDIR settings
- EXPOSE for documented ports
- Clean package manager caches
- exec form for CMD/ENTRYPOINT
Stage 4: Generate .dockerignore
Objective: Create comprehensive .dockerignore to reduce build context and prevent secret leaks.
Always create .dockerignore with generated Dockerfile.
Standard .dockerignore Template:
# Git
.git
.gitignore
.gitattributes
# CI/CD
.github
.gitlab-ci.yml
.travis.yml
.circleci
# Documentation
README.md
CHANGELOG.md
CONTRIBUTING.md
LICENSE
*.md
docs/
# Docker
Dockerfile*
docker-compose*.yml
.dockerignore
# Environment
.env
.env.*
*.local
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Dependencies (language-specific - add as needed)
node_modules/
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
venv/
.venv/
target/
*.class
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store
# Testing
coverage/
.coverage
*.cover
.pytest_cache/
.tox/
test-results/
# Build artifacts
dist/
build/
*.egg-info/
Customize based on language:
- Node.js: Add
node_modules/,npm-debug.log,yarn-error.log - Python: Add
__pycache__/,*.pyc,.venv/,.pytest_cache/ - Go: Add
vendor/,*.exe,*.test - Java: Add
target/,*.class,*.jar(except final artifact)
Stage 5: Validate with devops-skills:dockerfile-validator
Objective: Ensure generated Dockerfile follows best practices and has no issues.
REQUIRED: Always validate after generation.
Validation Process:
-
Invoke devops-skills:dockerfile-validator skill:
Use the Skill tool to invoke devops-skills:dockerfile-validator This will run: - hadolint (syntax and best practices) - Checkov (security scanning) - Custom validation (layer optimization, etc.) -
Parse validation results:
- Categorize issues by severity (error, warning, info)
- Identify actionable fixes
- Prioritize security issues
-
Expected validation output:
[1/4] Syntax Validation (hadolint) [2/4] Security Scan (Checkov) [3/4] Best Practices Validation [4/4] Optimization Analysis
Stage 6: Iterate on Validation Errors
Objective: Fix any validation errors and re-validate.
REQUIRED: Iterate at least ONCE if validation finds errors.
Iteration Process:
-
If validation finds errors:
- Analyze each error
- Apply fixes to Dockerfile
- Re-run validation
- Repeat until clean OR maximum 3 iterations
-
If validation finds warnings:
- Assess if warnings are acceptable
- Apply fixes for critical warnings
- Document suppressed warnings with justification
-
Common fixes:
- Add version tags to base images
- Add USER directive before CMD
- Add HEALTHCHECK for services
- Combine RUN commands
- Clean up package caches
- Use COPY instead of ADD
Example iteration:
Iteration 1:
- Error: DL3006 - Missing version tag
- Fix: Change FROM node:alpine to FROM node:20-alpine
- Re-validate
Iteration 2:
- Warning: DL3059 - Multiple consecutive RUN commands
- Fix: Combine RUN commands with &&
- Re-validate
Iteration 3:
- All checks passed ✓
Stage 7: Final Review and Recommendations
Objective: Provide comprehensive summary and next steps.
Deliverables:
-
Generated Files:
- Dockerfile (validated and optimized)
- .dockerignore (comprehensive)
-
Validation Summary:
- All validation results
- Any remaining warnings (with justification)
- Security scan results
-
Usage Instructions:
# Build the image docker build -t myapp:1.0 . # Run the container docker run -p 3000:3000 myapp:1.0 # Test health check (if applicable) curl http://localhost:3000/health -
Optimization Metrics (REQUIRED - provide explicit estimates):
Always include a summary like this:
## Optimization Metrics | Metric | Estimate | |--------|----------| | Image Size | ~150MB (vs ~500MB without multi-stage, 70% reduction) | | Build Cache | Layer caching enabled for dependencies | | Security | Non-root user, minimal base image, no secrets |Language-specific size estimates:
- Node.js: ~50-150MB with Alpine (vs ~1GB with full node image)
- Python: ~150-250MB with slim (vs ~900MB with full python image)
- Go: ~5-20MB with distroless/scratch (vs ~800MB with full golang image)
- Java: ~200-350MB with JRE (vs ~500MB+ with JDK)
-
Next Steps (REQUIRED - always include as bulleted list):
Always provide explicit next steps:
## Next Steps - [ ] Test the build locally: `docker build -t myapp:1.0 .` - [ ] Run and verify the container works as expected - [ ] Update CI/CD pipeline to use the new Dockerfile - [ ] Consider BuildKit cache mounts for faster builds (see Modern Docker Features) - [ ] Set up automated vulnerability scanning with `docker scout` or `trivy` - [ ] Add to container registry and deploy
Generation Scripts (Optional Reference)
The scripts/ directory contains standalone bash scripts for manual Dockerfile generation outside of this skill:
generate_nodejs.sh- CLI tool for Node.js Dockerfilesgenerate_python.sh- CLI tool for Python Dockerfilesgenerate_golang.sh- CLI tool for Go Dockerfilesgenerate_java.sh- CLI tool for Java Dockerfilesgenerate_dockerignore.sh- CLI tool for .dockerignore generation
Purpose: These scripts are reference implementations and manual tools for users who want to generate Dockerfiles via command line without this skill. They demonstrate the same best practices embedded in this skill.
When using this skill: the agent generates Dockerfiles directly using the templates and patterns documented in this skill.md, rather than invoking these scripts. The templates in this document are the authoritative source.
Script usage example:
# Manual Dockerfile generation
cd scripts
./generate_nodejs.sh --version 20 --port 3000 --output Dockerfile
Best Practices Reference
For detailed best practices, see the reference documentation:
- Security:
references/security_best_practices.md- Non-root users, minimal images, secrets management - Optimization:
references/optimization_patterns.md- Layer caching, multi-stage builds, BuildKit features - Multi-stage builds:
references/multistage_builds.md- Comprehensive multi-stage patterns
Quick Security Checklist:
- ✓ Use specific version tags (e.g.,
node:20-alpine, notnode:latest) - ✓ Run as non-root user
- ✓ Use minimal base images (alpine, distroless)
- ✓ Never hardcode secrets in ENV
Quick Optimization Checklist:
- ✓ Order layers from least to most frequently changing
- ✓ Copy dependency files before application code
- ✓ Combine related RUN commands with &&
- ✓ Clean package manager caches in same layer
Common Patterns
For complete pattern examples, see references/language_specific_guides.md:
- Next.js with multi-stage builds
- FastAPI with health checks
- Go CLI tools with distroless
- Spring Boot with JRE optimization
Modern Docker Features
For BuildX multi-platform builds, SBOM generation, and BuildKit cache mounts, see references/modern_docker_features.md.
Error Handling
Common Generation Issues
-
Missing dependency files:
- Ensure package.json, requirements.txt, go.mod, pom.xml exist
- Ask user to provide or generate template
-
Unknown framework:
- Use WebSearch or context7 to research
- Fall back to generic template
- Ask user for specific requirements
-
Validation failures:
- Apply fixes automatically
- Iterate until clean
- Document any suppressions
Integration with Other Skills
This skill works well in combination with:
- devops-skills:dockerfile-validator - Validates generated Dockerfiles (REQUIRED)
- k8s-generator - Generate Kubernetes deployments for the container
- helm-generator - Create Helm charts with the container image
Anti-Patterns
NEVER use latest as the base image tag
- WHY:
FROM node:latestresolves to a different image on each build, producing non-reproducible images and silently pulling in breaking changes or vulnerabilities. - BAD:
FROM node:latest - GOOD:
FROM node:20.18-alpine3.20(specific version and variant)
NEVER split related package installation across multiple RUN layers
- WHY: Each
RUNcreates a new layer; separateapt-get updateandapt-get installlayers can leak stale package lists and cache files into the image, increasing size unnecessarily. - BAD:
RUN apt-get update\nRUN apt-get install -y curl\nRUN rm -rf /var/lib/apt/lists/* - GOOD:
RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/*
NEVER run containers as root
- WHY: Running as the default root user allows any exploited process full container access; always create a non-root user for application processes.
- BAD: No
USERinstruction (defaults to root) - GOOD:
RUN addgroup -S appgroup && adduser -S appuser -G appgroupfollowed byUSER appuser
NEVER copy the entire build context before installing dependencies
- WHY: Copying all files before
npm installorpip installbusts the layer cache on every code change, making builds slower than necessary. - BAD:
COPY . .followed byRUN npm ci - GOOD:
COPY package*.json ./thenRUN npm cithenCOPY . .: dependency layer is cached until package.json changes
NEVER use ADD when COPY is sufficient
- WHY:
ADDhas implicit behaviors (auto-extracting tarballs, fetching URLs) that make Dockerfiles harder to reason about;COPYis explicit and predictable. - BAD:
ADD app.tar.gz /app/ - GOOD:
COPY app.tar.gz /app/and explicitly extract if needed:RUN tar -xzf /app/app.tar.gz -C /app/
NEVER hardcode secrets in ENV or ARG
- WHY: Values baked into image layers are readable by anyone with the image; the security requirements forbid hardcoded secrets.
- BAD:
ENV API_KEY=abc123in the Dockerfile. - GOOD: Inject secrets at runtime (environment variables or a secrets mount) and keep
.envfiles in.dockerignore.
NEVER ship a Dockerfile without a .dockerignore
- WHY: Without it the build context includes
.git,.envand local dependencies, which slows builds and can leak secrets into layers. - BAD: Deliver only the Dockerfile.
- GOOD: Always generate a
.dockerignorealongside it, customised for the language (see Stage 4).
NEVER use shell form for CMD or ENTRYPOINT
- WHY: The Production Readiness rules require exec form so the application receives signals directly and shuts down cleanly.
- BAD:
CMD node server.js - GOOD:
CMD ["node", "server.js"]
NEVER omit HEALTHCHECK for services
- WHY: Production Readiness requires a HEALTHCHECK for services so orchestrators can detect an unhealthy container.
- BAD: A web service image with no
HEALTHCHECKinstruction. - GOOD:
HEALTHCHECK CMD curl -f http://localhost:3000/health || exit 1against the application’s health endpoint.
NEVER deliver a Dockerfile that has not been validated
- WHY: Stage 5 requires validation with devops-skills:dockerfile-validator, and Stage 6 requires iterating on errors (maximum 3 iterations).
- BAD: Present the first draft without running hadolint or Checkov.
- GOOD: Validate, fix errors, re-validate until clean, and document any suppressed warning with a justification.
Notes
- Always use multi-stage builds for compiled languages
- Always create non-root user for security
- Always generate .dockerignore to prevent secret leaks
- Always validate with devops-skills:dockerfile-validator
- Iterate at least once if validation finds errors
- Use alpine or distroless base images when possible
- Pin all version tags (never use :latest)
- Clean up package manager caches in same layer
- Order Dockerfile instructions from least to most frequently changing
- Use BuildKit features for advanced optimization
- Test builds locally before committing
- Keep Dockerfiles simple and maintainable
- Document any non-obvious patterns with comments
Explicit NEVER/WHY/consequence triplets with side-by-side bad/good examples. Generic warnings score low — concrete failure modes with real impact (security, correctness, performance) score high.
Runtime validation via eval scenarios proving the skill actually changes agent behaviour. Requires evals/ directory, instructions.json, ≥3 scenarios, and ≥80% instruction coverage in summary.json.
Appropriate rigidity for the skill type: strict rules for critical/security skills, balanced steps for process skills, flexible options for tool skills. Mismatched calibration (too rigid or too loose) reduces score.
Expert-only knowledge the AI couldn't derive on its own — domain-specific patterns, production gotchas, decision frameworks, and non-obvious tool behaviour. High scores mean <5% redundancy with what the model already knows.
A clear philosophy plus numbered step-by-step workflows with explicit entry/exit points. Should answer: what is the core principle, how to execute it, and when (and when not) to apply it.
Rich activation keywords and trigger scenarios in the description so the skill fires when needed. Best descriptions read like an exhaustive list of the tasks and concepts that should invoke this skill.
Copy-paste-ready code, complete runnable examples, real file paths and commands. Pseudocode and abstract descriptions score low — concrete, executable content scores high.
SKILL.md as a <100-line navigation hub pointing to focused references/ files. Content should be lazy-loaded on demand, not front-loaded. Includes actionable 'When to Use' conditions per reference.
Proper frontmatter, single-task focus, activation keywords, and cross-harness portability. Skills must work across 40+ agentic harnesses without hardcoded paths or agent-specific references.
Expert-only knowledge the AI couldn't derive on its own — domain-specific patterns, production gotchas, decision frameworks, and non-obvious tool behaviour. High scores mean <5% redundancy with what the model already knows.
A clear philosophy plus numbered step-by-step workflows with explicit entry/exit points. Should answer: what is the core principle, how to execute it, and when (and when not) to apply it.
Explicit NEVER/WHY/consequence triplets with side-by-side bad/good examples. Generic warnings score low — concrete failure modes with real impact (security, correctness, performance) score high.
Proper frontmatter, single-task focus, activation keywords, and cross-harness portability. Skills must work across 40+ agentic harnesses without hardcoded paths or agent-specific references.
SKILL.md as a <100-line navigation hub pointing to focused references/ files. Content should be lazy-loaded on demand, not front-loaded. Includes actionable 'When to Use' conditions per reference.
Appropriate rigidity for the skill type: strict rules for critical/security skills, balanced steps for process skills, flexible options for tool skills. Mismatched calibration (too rigid or too loose) reduces score.
Rich activation keywords and trigger scenarios in the description so the skill fires when needed. Best descriptions read like an exhaustive list of the tasks and concepts that should invoke this skill.
Copy-paste-ready code, complete runnable examples, real file paths and commands. Pseudocode and abstract descriptions score low — concrete, executable content scores high.
No eval scenarios available.
Language-Specific Dockerfile Guides
Overview
This guide provides best practices and optimized Dockerfile templates for popular programming languages and frameworks.
Node.js
Basic Node.js Application
# syntax=docker/dockerfile:1
FROM node:20-alpine AS base
WORKDIR /app
# Dependencies stage
FROM base AS deps
COPY package.json package-lock.json ./
RUN npm ci --only=production && npm cache clean --force
# Production stage
FROM base AS production
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
USER nodejs
EXPOSE 3000
HEALTHCHECK CMD node -e "require('http').get('http://localhost:3000/health',(r)=>{process.exit(r.statusCode===200?0:1)})"
CMD ["node", "server.js"]
Next.js Application
# syntax=docker/dockerfile:1
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs
COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
USER nextjs
EXPOSE 3000
CMD ["npm", "start"]
Express.js API
# syntax=docker/dockerfile:1
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
COPY . .
RUN addgroup -g 1001 -S appgroup && adduser -S appuser -u 1001 -G appgroup && \
chown -R appuser:appgroup /app
USER appuser
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s CMD node -e "require('http').get('http://localhost:3000/health',(r)=>{process.exit(r.statusCode===200?0:1)})"
CMD ["node", "server.js"]
Best Practices:
- Use
npm ciinstead ofnpm installfor deterministic builds - Always use
package-lock.jsonfor version locking - Run
npm cache clean --forceafter install - Use
--only=productionto exclude dev dependencies - Set
NODE_ENV=production
Python
Basic Python Application
# syntax=docker/dockerfile:1
FROM python:3.12-slim AS builder
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends gcc && \
rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir --user -r requirements.txt
FROM python:3.12-slim
WORKDIR /app
RUN useradd -m -u 1001 appuser
COPY --from=builder /root/.local /home/appuser/.local
COPY --chown=appuser:appuser . .
ENV PATH=/home/appuser/.local/bin:$PATH
USER appuser
EXPOSE 8000
CMD ["python", "app.py"]
FastAPI Application
# syntax=docker/dockerfile:1
FROM python:3.12-slim AS builder
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends gcc && \
rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir --user -r requirements.txt
FROM python:3.12-slim
WORKDIR /app
RUN useradd -m -u 1001 appuser
COPY --from=builder /root/.local /home/appuser/.local
COPY --chown=appuser:appuser . .
ENV PATH=/home/appuser/.local/bin:$PATH
USER appuser
EXPOSE 8000
HEALTHCHECK CMD python -c "import urllib.request;urllib.request.urlopen('http://localhost:8000/health')" || exit 1
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Django Application
# syntax=docker/dockerfile:1
FROM python:3.12-slim AS builder
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc postgresql-client libpq-dev && \
rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /wheels -r requirements.txt
FROM python:3.12-slim
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
postgresql-client libpq-dev && \
rm -rf /var/lib/apt/lists/*
COPY --from=builder /wheels /wheels
RUN pip install --no-cache /wheels/* && rm -rf /wheels
RUN useradd -m -u 1001 appuser
COPY --chown=appuser:appuser . .
RUN python manage.py collectstatic --noinput
USER appuser
EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "project.wsgi:application"]
Best Practices:
- Use
pip install --no-cache-dirto reduce image size - Install build dependencies in builder stage only
- Use wheels for compiled dependencies
- Set
PYTHONUNBUFFERED=1for real-time logs - Use
--userflag for pip install (non-root)
Go
Basic Go Application
# syntax=docker/dockerfile:1
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -ldflags="-s -w" -o main .
FROM alpine:3.21
RUN apk --no-cache add ca-certificates
WORKDIR /app
RUN addgroup -g 1001 -S appgroup && adduser -S appuser -u 1001 -G appgroup
COPY --from=builder /app/main .
USER appuser
EXPOSE 8080
HEALTHCHECK CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
CMD ["./main"]
Go with Distroless (Minimal)
# syntax=docker/dockerfile:1
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -ldflags="-s -w" -o /main .
FROM gcr.io/distroless/static-debian12
COPY --from=builder /main /main
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/main"]
Go with Scratch (Absolute Minimal)
# syntax=docker/dockerfile:1
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -ldflags="-s -w" -o /main .
FROM scratch
COPY --from=builder /main /main
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
USER 1001:1001
ENTRYPOINT ["/main"]
Best Practices:
- Use
CGO_ENABLED=0for static binaries - Use
-ldflags="-s -w"to strip debug symbols - Use scratch or distroless for minimal images
- Include ca-certificates if making HTTPS requests
- Use go modules (go.mod) for dependency management
Java
Spring Boot with Maven
# syntax=docker/dockerfile:1
FROM eclipse-temurin:21-jdk-jammy AS builder
WORKDIR /app
COPY mvnw pom.xml ./
COPY .mvn .mvn
RUN ./mvnw dependency:go-offline
COPY src ./src
RUN ./mvnw clean package -DskipTests && mv target/*.jar target/app.jar
FROM eclipse-temurin:21-jre-jammy
WORKDIR /app
RUN useradd -m -u 1001 appuser
COPY --from=builder --chown=appuser:appuser /app/target/app.jar ./app.jar
USER appuser
EXPOSE 8080
HEALTHCHECK CMD curl -f http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["java", "-jar", "app.jar"]
Spring Boot with Gradle
# syntax=docker/dockerfile:1
FROM eclipse-temurin:21-jdk-jammy AS builder
WORKDIR /app
COPY gradlew ./
COPY gradle gradle
COPY build.gradle settings.gradle ./
RUN ./gradlew dependencies --no-daemon
COPY src ./src
RUN ./gradlew build -x test --no-daemon && \
mv build/libs/*.jar build/libs/app.jar
FROM eclipse-temurin:21-jre-jammy
WORKDIR /app
RUN useradd -m -u 1001 appuser
COPY --from=builder --chown=appuser:appuser /app/build/libs/app.jar ./app.jar
USER appuser
EXPOSE 8080
HEALTHCHECK CMD curl -f http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["java", "-jar", "app.jar"]
Spring Boot Layered (Optimized)
# syntax=docker/dockerfile:1
FROM eclipse-temurin:21-jdk-jammy AS builder
WORKDIR /app
COPY mvnw pom.xml ./
COPY .mvn .mvn
RUN ./mvnw dependency:go-offline
COPY src ./src
RUN ./mvnw clean package -DskipTests
RUN java -Djarmode=layertools -jar target/*.jar extract --destination target/extracted
FROM eclipse-temurin:21-jre-jammy
WORKDIR /app
RUN useradd -m -u 1001 appuser
USER appuser
COPY --from=builder /app/target/extracted/dependencies/ ./
COPY --from=builder /app/target/extracted/spring-boot-loader/ ./
COPY --from=builder /app/target/extracted/snapshot-dependencies/ ./
COPY --from=builder /app/target/extracted/application/ ./
EXPOSE 8080
HEALTHCHECK CMD curl -f http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]
Best Practices:
- Use JRE instead of JDK for runtime (smaller)
- Use layered JARs for better caching
- Run
./mvnw dependency:go-offlineto cache dependencies - Use
--no-daemonwith Gradle to avoid background processes - Set appropriate Java heap size with
-Xmxand-Xms
Rust
Rust Application
# syntax=docker/dockerfile:1
FROM rust:1.75-alpine AS builder
WORKDIR /app
RUN apk add --no-cache musl-dev
COPY Cargo.toml Cargo.lock ./
# Cache dependencies
RUN mkdir src && echo "fn main() {}" > src/main.rs && \
cargo build --release && \
rm -rf src
COPY src ./src
RUN touch src/main.rs && cargo build --release
FROM alpine:3.21
RUN apk --no-cache add ca-certificates
WORKDIR /app
RUN addgroup -g 1001 -S appgroup && adduser -S appuser -u 1001 -G appgroup
COPY --from=builder /app/target/release/app ./
USER appuser
EXPOSE 8080
CMD ["./app"]
Ruby
Ruby on Rails
# syntax=docker/dockerfile:1
FROM ruby:3.3-alpine AS builder
WORKDIR /app
RUN apk add --no-cache build-base postgresql-dev nodejs yarn
COPY Gemfile Gemfile.lock ./
RUN bundle install --without development test
COPY package.json yarn.lock ./
RUN yarn install --production
COPY . .
RUN bundle exec rake assets:precompile
FROM ruby:3.3-alpine
WORKDIR /app
RUN apk add --no-cache postgresql-client nodejs
RUN addgroup -g 1001 -S appgroup && adduser -S appuser -u 1001 -G appgroup
COPY --from=builder /usr/local/bundle /usr/local/bundle
COPY --from=builder --chown=appuser:appgroup /app ./
USER appuser
EXPOSE 3000
CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"]
PHP
PHP with Laravel
# syntax=docker/dockerfile:1
FROM composer:2 AS composer
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist
FROM php:8.3-fpm-alpine
WORKDIR /app
RUN apk add --no-cache postgresql-dev && \
docker-php-ext-install pdo pdo_pgsql
COPY --from=composer /app/vendor ./vendor
COPY . .
RUN composer dump-autoload --optimize --classmap-authoritative
RUN addgroup -g 1001 -S appgroup && adduser -S appuser -u 1001 -G appgroup && \
chown -R appuser:appgroup /app
USER appuser
EXPOSE 9000
CMD ["php-fpm"]
.NET
ASP.NET Core
# syntax=docker/dockerfile:1
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS builder
WORKDIR /app
COPY *.csproj ./
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o out
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
RUN useradd -m -u 1001 appuser
COPY --from=builder --chown=appuser:appuser /app/out ./
USER appuser
EXPOSE 8080
HEALTHCHECK CMD curl -f http://localhost:8080/health || exit 1
ENTRYPOINT ["dotnet", "MyApp.dll"]
Common Patterns Across Languages
Development vs Production
# syntax=docker/dockerfile:1
ARG NODE_ENV=production
FROM node:20-alpine AS development
ENV NODE_ENV=development
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["npm", "run", "dev"]
FROM node:20-alpine AS production
ENV NODE_ENV=production
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
USER nodejs
CMD ["node", "server.js"]
FROM ${NODE_ENV} AS final
With Database Migrations
# Add entrypoint script
COPY docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["python", "app.py"]
docker-entrypoint.sh:
#!/bin/sh
set -e
# Run migrations
python manage.py migrate
# Start application
exec "$@"
Framework-Specific Tips
Next.js
- Use standalone output mode for smaller images
- Set
NEXT_TELEMETRY_DISABLED=1 - Copy only
.next/standaloneand.next/static
Django
- Run
collectstaticduring build - Use gunicorn or uvicorn for production
- Include database client libraries
Spring Boot
- Use layered JARs for better caching
- Include actuator for health checks
- Set appropriate JVM memory limits
FastAPI
- Use uvicorn with workers for production
- Include health check endpoint
- Use
--proxy-headersif behind proxy
References
Modern Docker Features (2025)
Multi-Platform Builds with BuildX
Use Case: Build images that work on both AMD64 and ARM64 architectures (e.g., x86 servers and Apple Silicon Macs).
Enable BuildX:
# BuildX is included in Docker Desktop by default
# For Linux, ensure BuildX is installed
docker buildx version
Create Multi-Platform Images:
# Build for multiple platforms
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t myapp:latest \
--push \
.
# Build and load for current platform (testing)
docker buildx build \
--platform linux/amd64 \
-t myapp:latest \
--load \
.
Dockerfile Considerations:
# Most Dockerfiles work across platforms automatically
# Use platform-specific base images when needed
FROM --platform=$BUILDPLATFORM node:20-alpine AS builder
# Access build arguments for platform info
ARG TARGETPLATFORM
ARG BUILDPLATFORM
RUN echo "Building on $BUILDPLATFORM for $TARGETPLATFORM"
When to Use:
- Deploying to mixed infrastructure (x86 + ARM)
- Supporting Apple Silicon Macs in development
- Optimizing for AWS Graviton (ARM-based) instances
- Building cross-platform CLI tools
Software Bill of Materials (SBOM)
Use Case: Generate SBOM for supply chain security and compliance (increasingly required in 2025).
Generate SBOM During Build:
# Generate SBOM with BuildKit (Docker 24.0+)
docker buildx build \
--sbom=true \
-t myapp:latest \
.
# SBOM is attached as attestation to the image
# View SBOM
docker buildx imagetools inspect myapp:latest --format "{{ json .SBOM }}"
Generate SBOM from Existing Image:
# Using Syft
syft myapp:latest -o json > sbom.json
# Using Docker Scout
docker scout sbom myapp:latest
SBOM Benefits: Vulnerability tracking, license compliance, dependency transparency, and audit trail for security reviews.
Integration with CI/CD:
# GitHub Actions example
- name: Build with SBOM
run: |
docker buildx build \
--sbom=true \
--provenance=true \
-t myapp:latest \
--push \
.
BuildKit Cache Mounts (Advanced)
Use Case: Persist package manager caches across builds for faster iteration.
Already covered in detail in optimization_patterns.md.
Quick reference:
# syntax=docker/dockerfile:1
# NPM cache mount
RUN --mount=type=cache,target=/root/.npm \
npm ci
# Go module cache
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
# Pip cache
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
Multi-Stage Docker Builds
Overview
Multi-stage builds allow you to use multiple FROM statements in your Dockerfile. Each FROM instruction can use a different base, and each begins a new stage of the build. You can selectively copy artifacts from one stage to another, leaving behind everything you don’t want in the final image.
Benefits:
- Smaller images: 50-85% size reduction
- Separation of concerns: Build vs runtime environments
- Better security: No build tools in production images
- Faster deployments: Smaller images transfer faster
- Cleaner builds: No manual cleanup scripts needed
Basic Syntax
# syntax=docker/dockerfile:1
# Stage 1: Named "builder"
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp
# Stage 2: Final production image
FROM alpine:3.21
COPY --from=builder /app/myapp /usr/local/bin/
CMD ["myapp"]
Stage Naming
Explicit Names (Recommended)
# Name stages explicitly for clarity
FROM node:20 AS dependencies
RUN npm install
FROM node:20 AS builder
COPY --from=dependencies /app/node_modules ./node_modules
RUN npm run build
FROM node:20-alpine AS production
COPY --from=builder /app/dist ./dist
Numeric References (Less Clear)
# Reference previous stages by number (0-indexed)
FROM node:20
RUN npm install
FROM node:20
COPY --from=0 /app/node_modules ./node_modules
RUN npm run build
FROM node:20-alpine
COPY --from=1 /app/dist ./dist
Common Patterns
Pattern 1: Build and Runtime Separation
Use Case: Compiled languages (Go, Rust, C++, Java)
# Build stage
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -ldflags="-s -w" -o main .
# Runtime stage
FROM scratch
COPY --from=builder /app/main /main
ENTRYPOINT ["/main"]
Size Impact:
- Builder stage: ~300MB
- Final image: ~8MB
- Reduction: 97%
Pattern 2: Dependency Installation
Use Case: Separate dependency installation from application code
# Dependency stage
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# Production stage
FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=deps /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]
Benefits:
- Cached dependency layer (only rebuilds when package.json changes)
- Production dependencies only in final image
- Faster builds with layer caching
Pattern 3: Test Stage
Use Case: Run tests without including test dependencies in final image
# Dependency stage
FROM python:3.12-slim AS deps
WORKDIR /app
COPY requirements.txt requirements-dev.txt ./
RUN pip install --user -r requirements.txt
# Test stage
FROM deps AS test
RUN pip install --user -r requirements-dev.txt
COPY . .
RUN pytest tests/
# Production stage
FROM python:3.12-slim AS production
WORKDIR /app
COPY --from=deps /root/.local /root/.local
COPY . .
ENV PATH=/root/.local/bin:$PATH
CMD ["python", "app.py"]
Build with tests:
docker build --target test -t myapp:test .
Build production (tests automatically run before this stage):
docker build -t myapp:latest .
Pattern 4: Multi-Architecture
Use Case: Build for different platforms
FROM --platform=$BUILDPLATFORM golang:1.21-alpine AS builder
ARG TARGETARCH
ARG TARGETOS
WORKDIR /app
COPY . .
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o app
FROM alpine:3.21
COPY --from=builder /app/app /app
ENTRYPOINT ["/app"]
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest .
Pattern 5: Development vs Production
Use Case: Different images for dev and prod
# Base stage with common dependencies
FROM node:20-alpine AS base
WORKDIR /app
COPY package*.json ./
RUN npm ci
# Development stage
FROM base AS development
ENV NODE_ENV=development
COPY . .
CMD ["npm", "run", "dev"]
# Build stage
FROM base AS builder
COPY . .
RUN npm run build
# Production stage
FROM node:20-alpine AS production
ENV NODE_ENV=production
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/server.js"]
Build for development:
docker build --target development -t myapp:dev .
Build for production:
docker build --target production -t myapp:prod .
Language-Specific Examples
Node.js (Next.js)
# syntax=docker/dockerfile:1
# Dependencies stage
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
# Builder stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# Runner stage
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/public ./public
USER nextjs
EXPOSE 3000
CMD ["npm", "start"]
Python (Django/FastAPI)
# syntax=docker/dockerfile:1
# Builder stage
FROM python:3.12-slim AS builder
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
postgresql-dev \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /app/wheels -r requirements.txt
# Runner stage
FROM python:3.12-slim AS runner
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
postgresql-client \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/wheels /wheels
RUN pip install --no-cache /wheels/*
COPY . .
RUN useradd -m -u 1001 appuser
USER appuser
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0"]
Go (Minimal)
# syntax=docker/dockerfile:1
# Builder stage
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags="-s -w" -o main .
# Runner stage (scratch = 0 bytes base)
FROM scratch
COPY --from=builder /app/main /main
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
USER 1001:1001
ENTRYPOINT ["/main"]
Java (Spring Boot)
# syntax=docker/dockerfile:1
# Dependencies stage
FROM eclipse-temurin:21-jdk-jammy AS deps
WORKDIR /app
COPY mvnw pom.xml ./
COPY .mvn .mvn
RUN ./mvnw dependency:go-offline
# Builder stage
FROM deps AS builder
COPY src ./src
RUN ./mvnw clean package -DskipTests
# Extractor stage (for layered JAR)
FROM builder AS extractor
WORKDIR /app
RUN java -Djarmode=layertools -jar target/*.jar extract --destination target/extracted
# Runner stage
FROM eclipse-temurin:21-jre-jammy AS runner
WORKDIR /app
RUN useradd -m -u 1001 appuser
USER appuser
COPY --from=extractor /app/target/extracted/dependencies/ ./
COPY --from=extractor /app/target/extracted/spring-boot-loader/ ./
COPY --from=extractor /app/target/extracted/snapshot-dependencies/ ./
COPY --from=extractor /app/target/extracted/application/ ./
EXPOSE 8080
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]
Advanced Techniques
Copying from External Images
# Copy from official images
FROM alpine:3.21 AS production
COPY --from=nginx:alpine /usr/share/nginx/html /usr/share/nginx/html
COPY --from=myregistry/common:latest /app/lib /app/lib
Conditional Stages (BuildKit)
# syntax=docker/dockerfile:1
ARG BUILD_ENV=production
FROM node:20 AS development
ENV NODE_ENV=development
CMD ["npm", "run", "dev"]
FROM node:20-alpine AS production
ENV NODE_ENV=production
CMD ["node", "dist/server.js"]
FROM ${BUILD_ENV} AS final
docker build --build-arg BUILD_ENV=development -t myapp:dev .
docker build --build-arg BUILD_ENV=production -t myapp:prod .
Parallel Stages
# syntax=docker/dockerfile:1
# Independent stages run in parallel
FROM alpine AS fetch-config
RUN wget https://example.com/config.json -O /config.json
FROM alpine AS fetch-data
RUN wget https://example.com/data.csv -O /data.csv
# Final stage waits for both
FROM alpine AS final
COPY --from=fetch-config /config.json /app/
COPY --from=fetch-data /data.csv /app/
Build Optimization
Target Specific Stage
# Build only up to specific stage
docker build --target builder -t myapp:builder .
# Useful for debugging
docker run -it myapp:builder /bin/sh
Cache Mounts with Multi-Stage
# syntax=docker/dockerfile:1
FROM golang:1.21-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 \
go build -o app
FROM alpine:3.21
COPY --from=builder /app/app /app
CMD ["/app"]
Best Practices
1. Order Stages Logically
# dependencies → builder → test → production
FROM node:20 AS deps
# Install dependencies
FROM deps AS builder
# Build application
FROM builder AS test
# Run tests
FROM node:20-alpine AS production
# Minimal runtime
2. Name Stages Clearly
# Good
FROM golang:1.21 AS compiler
FROM alpine:3.21 AS runtime
# Bad
FROM golang:1.21 AS stage1
FROM alpine:3.21 AS stage2
3. Minimize Final Stage
# Copy only what's needed
COPY --from=builder /app/binary /app/binary
# Don't copy unnecessary files
# Bad: COPY --from=builder /app /app
4. Use Specific Base Images per Stage
# JDK for building
FROM eclipse-temurin:21-jdk-jammy AS builder
# JRE for running (smaller)
FROM eclipse-temurin:21-jre-jammy AS runner
5. Document Stages
# syntax=docker/dockerfile:1
# Stage 1: Install and cache dependencies
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
# Stage 2: Build application with dependencies
FROM deps AS builder
COPY . .
RUN npm run build
# Stage 3: Production runtime with minimal footprint
FROM node:20-alpine AS production
ENV NODE_ENV=production
WORKDIR /app
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/server.js"]
Common Pitfalls
1. Not Using Stage Names
# Hard to maintain
FROM node:20
COPY --from=0 /app/node_modules ./node_modules
# Better
FROM node:20 AS deps
COPY --from=deps /app/node_modules ./node_modules
2. Copying Entire Stage
# Bad - copies everything including build tools
COPY --from=builder /app /app
# Good - copy only artifacts
COPY --from=builder /app/dist /app/dist
3. Not Leveraging Cache
# Bad - copy all before installing
COPY . .
RUN npm install
# Good - install dependencies first (cached)
COPY package*.json ./
RUN npm install
COPY . .
Troubleshooting
Check Stage Sizes
# Build with specific target
docker build --target builder -t myapp:builder .
docker build --target production -t myapp:production .
# Compare sizes
docker images | grep myapp
Debug Intermediate Stages
# Build up to specific stage
docker build --target builder -t debug:builder .
# Run and inspect
docker run -it debug:builder /bin/sh
View Build Process
# Enable BuildKit with progress
DOCKER_BUILDKIT=1 docker build --progress=plain .
# See all stages and their timing
References
Dockerfile Optimization Patterns
Overview
This guide provides comprehensive optimization techniques for reducing Docker image size, improving build times, and enhancing runtime performance.
Image Size Optimization
Use Multi-Stage Builds
Impact: 50-85% size reduction
# Before: Single stage (500MB)
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
CMD ["node", "dist/server.js"]
# After: Multi-stage (150MB)
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/server.js"]
Choose Minimal Base Images
Size Comparison:
ubuntu:22.04 → 77MB
node:20 → 996MB
node:20-slim → 239MB
node:20-alpine → 132MB
alpine:3.21 → 7.8MB
distroless/static → 2MB
scratch → 0MB
Selection Guide:
- Full OS (ubuntu, debian): When you need many system tools
- Slim variants: Good balance of size and compatibility
- Alpine: Minimal size, may have glibc compatibility issues
- Distroless: Highest security, minimal attack surface
- Scratch: For static binaries only (Go, Rust)
Remove Unnecessary Files
# Clean up in same RUN layer
RUN apt-get update && \
apt-get install -y --no-install-recommends curl && \
rm -rf /var/lib/apt/lists/*
# Remove build artifacts
RUN npm run build && \
rm -rf src/ tests/ docs/
# Use .dockerignore
# (prevents files from being sent to build context)
Build Time Optimization
Layer Caching Strategy
Order instructions from least to most frequently changing:
# Bad - Invalidates cache on any code change
COPY . /app
RUN npm install
# Good - Cache dependencies separately
COPY package*.json /app/
RUN npm install
COPY . /app
Optimal Layer Order:
- Base image (FROM)
- System packages (RUN apt-get)
- Application dependencies (package.json, requirements.txt)
- Application code (COPY . .)
- Build commands (RUN build)
- Runtime configuration (CMD, ENTRYPOINT)
BuildKit Cache Mounts
Mount external caches to persist across builds:
# syntax=docker/dockerfile:1
# NPM cache mount
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci
# Go module cache
FROM golang:1.21-alpine
WORKDIR /app
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
# Pip cache
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
Enable BuildKit:
export DOCKER_BUILDKIT=1
docker build .
Parallel Stage Execution
# syntax=docker/dockerfile:1
FROM alpine AS fetch-1
RUN wget https://example.com/file1
FROM alpine AS fetch-2
RUN wget https://example.com/file2
FROM alpine AS final
COPY --from=fetch-1 /file1 .
COPY --from=fetch-2 /file2 .
BuildKit automatically parallelizes independent stages.
Layer Optimization
Combine RUN Commands
# Bad - Creates 5 layers
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y git
RUN apt-get install -y vim
RUN rm -rf /var/lib/apt/lists/*
# Good - Creates 1 layer
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl \
git \
vim && \
rm -rf /var/lib/apt/lists/*
Minimize Layer Count
Each instruction creates a layer:
- FROM, RUN, COPY, ADD create layers
- ENV, WORKDIR, EXPOSE, USER do not create significant layers
- Combine related operations
# Bad - Many layers
COPY package.json .
COPY package-lock.json .
COPY tsconfig.json .
COPY src/ ./src/
COPY tests/ ./tests/
# Good - Fewer layers (use .dockerignore)
COPY . .
Layer Size Analysis
# Inspect layer sizes
docker history myimage:latest
# Find large layers
docker history --no-trunc --format "table {{.Size}}\t{{.CreatedBy}}" myimage:latest | sort -hr | head -10
Dependency Optimization
Install Only Production Dependencies
Node.js:
# Development dependencies excluded
RUN npm ci --only=production
# Or use package.json scripts
RUN npm ci --omit=dev
Python:
# Create separate requirements files
# requirements.txt (production)
# requirements-dev.txt (development)
RUN pip install --no-cache-dir -r requirements.txt
Java (Maven):
# Skip tests during build
RUN ./mvnw clean package -DskipTests
Remove Development Tools
# Multi-stage: Keep build tools in builder stage
FROM golang:1.21 AS builder
RUN apk add --no-cache git make
RUN make build
FROM alpine:3.21
# No build tools in final image
COPY --from=builder /app/binary /app/
.dockerignore Optimization
Reduces build context size and build time:
# Version control
.git
.gitignore
# Dependencies (installed during build)
node_modules
vendor
__pycache__
# IDE files
.vscode
.idea
*.swp
# Build artifacts
dist
build
target
# Documentation
*.md
docs/
# CI/CD
.github
.gitlab-ci.yml
# Environment files
.env
.env.*
# Logs
*.log
logs/
# Tests
tests/
*.test
coverage/
Impact:
- Smaller build context → Faster upload to Docker daemon
- Fewer files → Faster COPY operations
- No accidental secret leaks
Runtime Performance
Use Exec Form for CMD/ENTRYPOINT
# Bad - Shell form (spawns extra sh process)
CMD node server.js
# Good - Exec form (direct process execution)
CMD ["node", "server.js"]
# Benefits:
# - Proper signal handling (SIGTERM, SIGINT)
# - Faster startup
# - Lower memory usage
Optimize Application
# Node.js: Use production mode
ENV NODE_ENV=production
# Python: Use optimized bytecode
ENV PYTHONOPTIMIZE=1
# Java: Set heap size
ENV JAVA_OPTS="-Xms512m -Xmx2048m"
# Go: Build with optimizations
RUN go build -ldflags="-s -w" -o app
Health Checks
# Efficient health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
# Avoid heavy health checks
# Bad: HEALTHCHECK CMD curl http://localhost/full-db-check
Build Optimization Checklist
- Use multi-stage builds
- Choose minimal base images (Alpine, distroless)
- Order layers from least to most frequently changing
- Combine RUN commands where logical
- Use BuildKit cache mounts
- Create comprehensive .dockerignore
- Install only production dependencies
- Clean package manager caches in same layer
- Remove development tools from final image
- Use exec form for CMD/ENTRYPOINT
- Optimize application for production
- Add efficient health checks
Advanced Techniques
Squash Layers (Use with Caution)
# Squash all layers into one (loses layer caching)
docker build --squash -t myapp:latest .
Use Cases:
- Final production images
- When layer caching isn’t important
- To hide sensitive information in layers (better: use multi-stage)
Drawbacks:
- Loses layer caching benefits
- Larger initial download
- Less transparent image history
BuildKit Secrets (Zero-Copy Secrets)
# syntax=docker/dockerfile:1
FROM alpine
RUN --mount=type=secret,id=aws_credentials \
aws configure set credentials $(cat /run/secrets/aws_credentials)
docker build --secret id=aws_credentials,src=$HOME/.aws/credentials .
Cross-Platform Builds
# Build for multiple platforms
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t myapp:latest \
.
Measuring Optimization Impact
Before and After Comparison
# Build original
docker build -t myapp:before .
docker images myapp:before
# Build optimized
docker build -t myapp:after .
docker images myapp:after
# Compare sizes
docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}" | grep myapp
Build Time Measurement
# Measure build time
time docker build -t myapp:latest .
# Measure with cache
docker build -t myapp:cached .
# Measure without cache
docker build --no-cache -t myapp:no-cache .
Dive Tool (Layer Analysis)
# Install dive
brew install dive # macOS
# or download from https://github.com/wagoodman/dive
# Analyze image
dive myapp:latest
Real-World Examples
Node.js Optimization
Before (996MB):
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]
After (50MB, 95% reduction):
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/server.js ./
CMD ["node", "server.js"]
Go Optimization
Before (800MB):
FROM golang:1.21
WORKDIR /app
COPY . .
RUN go build -o app
CMD ["./app"]
After (8MB, 99% reduction):
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o app
FROM scratch
COPY --from=builder /app/app /app
ENTRYPOINT ["/app"]
References
Dockerfile Security Best Practices
Overview
This guide provides comprehensive security best practices for writing secure Dockerfiles. Security should be a primary consideration when containerizing applications.
Table of Contents
- Base Image Security
- User Management
- Secrets Management
- Dependency Management
- Attack Surface Reduction
- Vulnerability Scanning
Base Image Security
Use Specific Tags
Problem: Using :latest tag can lead to unpredictable builds and security vulnerabilities.
# Bad - Unpredictable, may pull vulnerable versions
FROM node:latest
# Good - Specific version
FROM node:20-alpine
# Better - Specific version with digest for reproducibility
FROM node:20-alpine@sha256:2c6c59cf4d34d4f937ddfcf33bab9d8bbad8658d1b9de7b97622566a52167f5b
Use Minimal Base Images
Principle: Smaller images have fewer attack vectors.
# Large attack surface (>1GB)
FROM ubuntu:22.04
# Medium attack surface (~200MB)
FROM node:20
# Small attack surface (~50MB)
FROM node:20-alpine
# Minimal attack surface (~2MB for Go apps)
FROM gcr.io/distroless/static-debian12
Recommended Base Images:
- Alpine Linux: Minimal Linux distribution (~5MB base)
- Distroless: Google’s minimal container images (no shell, package managers)
- Scratch: Empty base image (for static binaries only)
Scan Base Images
# Use Trivy to scan base images
trivy image node:20-alpine
# Use Snyk
snyk container test node:20-alpine
# Use Docker Scout
docker scout cves node:20-alpine
User Management
Never Run as Root
Problem: Running as root gives attackers full system access if container is compromised.
# Bad - Runs as root (default)
FROM node:20-alpine
COPY . /app
CMD ["node", "server.js"]
# Good - Creates and uses non-root user
FROM node:20-alpine
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
COPY --chown=nodejs:nodejs . /app
USER nodejs
CMD ["node", "server.js"]
User Creation Patterns
Alpine Linux:
RUN addgroup -g 1001 -S appgroup && \
adduser -S appuser -u 1001 -G appgroup
USER appuser
Debian/Ubuntu:
RUN useradd -m -u 1001 appuser
USER appuser
Distroless (built-in nonroot user):
FROM gcr.io/distroless/static-debian12
USER nonroot:nonroot
Set Proper File Permissions
# Copy with ownership
COPY --chown=appuser:appuser . /app
# Ensure executables have correct permissions
RUN chmod +x /app/entrypoint.sh
Secrets Management
Never Hardcode Secrets
# Bad - Hardcoded secrets
ENV API_KEY=sk_live_abc123
ENV DATABASE_PASSWORD=password123
# Good - Use runtime environment variables
ENV API_KEY=""
ENV DATABASE_PASSWORD=""
Use Build Secrets (BuildKit)
# Mount secrets during build (never stored in image layers)
# syntax=docker/dockerfile:1
FROM alpine
RUN --mount=type=secret,id=api_key \
API_KEY=$(cat /run/secrets/api_key) && \
echo "Configuring with API key..." && \
# Use API_KEY here
rm -f /run/secrets/api_key
Build command:
docker build --secret id=api_key,src=.env .
Avoid Secrets in Layers
# Bad - Secret remains in image history
FROM alpine
RUN echo "SECRET_KEY=abc123" > /app/config
RUN rm /app/config # Secret still in previous layer!
# Good - Use multi-stage builds or build secrets
FROM alpine
RUN --mount=type=secret,id=config \
cat /run/secrets/config > /app/config && \
process_config && \
rm /app/config
Dependency Management
Pin Dependency Versions
# Bad - Unpinned versions
RUN apt-get install -y curl git
# Good - Pinned versions
RUN apt-get install -y \
curl=7.81.0-1ubuntu1.16 \
git=1:2.34.1-1ubuntu1.11
Node.js:
# Use package-lock.json or yarn.lock
COPY package*.json ./
RUN npm ci # Uses lock file
Python:
# Pin versions in requirements.txt
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
Go:
# go.sum ensures reproducible builds
COPY go.mod go.sum ./
RUN go mod download
Clean Package Manager Caches
# Alpine (apk)
RUN apk add --no-cache curl
# Debian/Ubuntu (apt)
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
&& rm -rf /var/lib/apt/lists/*
# Node.js (npm)
RUN npm ci && npm cache clean --force
# Python (pip)
RUN pip install --no-cache-dir -r requirements.txt
Attack Surface Reduction
Minimize Installed Packages
# Bad - Installs unnecessary packages
RUN apt-get install -y \
curl \
wget \
vim \
sudo \
ssh
# Good - Only essential packages
RUN apt-get install -y --no-install-recommends \
curl \
ca-certificates
Use Multi-Stage Builds
# Build stage - can include build tools
FROM golang:1.21-alpine AS builder
RUN apk add --no-cache git make
COPY . .
RUN make build
# Production stage - minimal runtime
FROM alpine:3.21
COPY --from=builder /app/binary /app/binary
CMD ["/app/binary"]
Remove Build Dependencies
# Install, use, and remove in same layer
RUN apk add --no-cache --virtual .build-deps \
gcc \
musl-dev \
&& pip install --no-cache-dir -r requirements.txt \
&& apk del .build-deps
Disable Unnecessary Services
# Don't include SSH, telnet, or other services
# Use docker exec for debugging instead
Network Security
Expose Only Necessary Ports
# Document exposed ports
EXPOSE 8080
# Don't expose unnecessary ports
# Bad: EXPOSE 22 (SSH)
# Bad: EXPOSE 3306 (MySQL in app container)
Use Non-Privileged Ports
# Good - Use port > 1024 (doesn't require root)
EXPOSE 8080
# Avoid privileged ports (< 1024)
# EXPOSE 80 # Requires root
Vulnerability Scanning
Integrate Scanning into CI/CD
# GitHub Actions example
- name: Scan Docker image
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:latest
format: 'sarif'
output: 'trivy-results.sarif'
Regular Image Updates
# Rebuild images regularly to get security patches
FROM node:20-alpine # Alpine releases security updates frequently
Scan for Secrets
# Use gitleaks or trufflehog
docker run -v $(pwd):/path zricethezav/gitleaks:latest detect --source=/path
# Use hadolint to detect some secret patterns
hadolint Dockerfile
Security Checklist
- Use specific base image tags (no
:latest) - Use minimal base images (Alpine, distroless)
- Create and use non-root user
- Never hardcode secrets
- Use build secrets for sensitive data
- Pin dependency versions
- Clean package manager caches
- Minimize installed packages
- Use multi-stage builds
- Expose only necessary ports
- Scan images for vulnerabilities
- Update base images regularly
- Use HEALTHCHECK for services
- Validate with hadolint and Checkov
Common Vulnerabilities
CVE-Related Issues
- Outdated base images: Rebuild regularly
- Known vulnerable packages: Scan and update
- Missing security patches: Use latest patch versions
Configuration Issues
- Running as root: Create non-root user
- Exposed secrets: Use build secrets or runtime config
- Unnecessary packages: Minimize attack surface
- Missing health checks: Add HEALTHCHECK directive
Tools for Security
- Trivy: Comprehensive vulnerability scanner
- Snyk: Security scanning and monitoring
- Checkov: Policy-as-code security scanning
- hadolint: Dockerfile linting with security rules
- Docker Scout: Docker’s official security tool
- Clair: Container vulnerability analysis
- Anchore: Container security and compliance