Expert reference for writing production-grade Dockerfiles. Every instruction, every pattern, every optimization.
When to Use
ALWAYS use this skill when the user mentions:
- "Dockerfile", "How to write a Dockerfile", "Dockerfile instructions"
- "Multi-stage build", "multi-stage build"
- "Dockerfile layer optimization", "layer caching"
- "Dockerfile templates", language-specific: "Go Dockerfile", "Java Dockerfile", "Python Dockerfile"
- Need to create or optimize a Dockerfile
- "Dockerfile best practices"
Instruction Reference
FROM — Base Image
FROM <image>[:<tag>] [AS <stage-name>]
FROM alpine:3.20 # Tag
FROM alpine:3.20@sha256:abc123...def456 # Digest (production!)
FROM golang:1.22-alpine AS builder # Named stage
FROM scratch # Empty image (for static binaries)
| Best Practice | Why |
|---|---|
| Pin digest for production | Tags are mutable; digest is immutable |
| Use Alpine/slim variants | Smaller attack surface, smaller image |
FROM scratch for Go/Rust |
Static binaries need nothing else |
RUN — Execute Commands
# ✅ Chain commands, clean in same layer
RUN apk add --no-cache curl && \
curl -fsSL https://example.com/script.sh -o /usr/local/bin/script && \
chmod +x /usr/local/bin/script
# ❌ Each RUN = new layer (bloat)
RUN apk add curl
RUN curl ... -o /usr/local/bin/script
RUN chmod +x /usr/local/bin/script
# Multi-line readability
RUN set -eux; \
apk add --no-cache \
curl \
ca-certificates \
tzdata; \
curl -fsSL ... | tar xz -C /usr/local
COPY — Copy Files
COPY [--chown=<user>:<group>] <src>... <dest>
COPY . /app
COPY --chown=app:app ./binary /usr/local/bin/
COPY --from=builder /app/build /app # From another stage (multi-stage)
# ✅ Layer-friendly: copy deps first, then source
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# ❌ Source change invalidates dependency cache
COPY . .
RUN go mod download
ADD — Copy + Auto-extract
# ADD auto-extracts tar archives
ADD archive.tar.gz /app/
# Prefer COPY unless you need tar extraction
COPY archive.tar.gz /app/
RUN tar xzf /app/archive.tar.gz -C /app
WORKDIR — Set Working Directory
WORKDIR /app
# All subsequent RUN/COPY/CMD use /app as base
# Prefer over:
RUN cd /app && npm install # ❌ cd doesn't persist
ENV & ARG
# ARG: build-time only (not in final image)
ARG VERSION=1.0.0
FROM myapp:${VERSION}
# ENV: runtime (persists in image)
ENV NODE_ENV=production \
PORT=8080
# Combine: pass ARG to ENV
ARG APP_VERSION
ENV APP_VERSION=${APP_VERSION}
EXPOSE — Document Ports
EXPOSE 8080
EXPOSE 8080/tcp # Protocol-specific
EXPOSE 8080/udp
# Note: EXPOSE does NOT publish ports. Use -p at runtime:
# docker run -p 8080:8080 myapp
CMD vs ENTRYPOINT
# CMD: default command (overridable)
CMD ["nginx", "-g", "daemon off;"]
# docker run myimage echo hello → overrides CMD
# ENTRYPOINT: fixed entry (not overridable)
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]
# docker run myimage → runs: docker-entrypoint.sh nginx -g 'daemon off;'
# docker run myimage echo hello → runs: docker-entrypoint.sh echo hello
# Common pattern: script + default args
ENTRYPOINT ["/entrypoint.sh"]
CMD ["start"]
USER — Switch to Non-Root
# Create user and group
RUN addgroup --system app && adduser --system --ingroup app app
USER app
HEALTHCHECK
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD wget -qO- http://localhost:8080/health || exit 1
# Dockerfile without shell (scratch):
HEALTHCHECK --interval=30s CMD /app/healthcheck || exit 1
SHELL — Change Default Shell
SHELL ["/bin/bash", "-euxo", "pipefail", "-c"]
RUN echo "Now using bash with strict mode"
Multi-Stage Build Patterns
Pattern 1: Build Binary + Scratch (Go/Rust)
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o server .
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app/server /server
USER 1000:1000
CMD ["/server"]
Pattern 2: Build + Minimal Runtime (Java)
FROM maven:3.9-eclipse-temurin-21 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package -DskipTests
FROM eclipse-temurin:21-jre-alpine
RUN addgroup --system app && adduser -S -G app app
COPY --from=builder /app/target/*.jar /app.jar
USER app
CMD ["java", "-jar", "/app.jar"]
Pattern 3: Layer-Optimized (Spring Boot)
FROM eclipse-temurin:21-jre-alpine AS builder
WORKDIR /app
COPY build/libs/*.jar app.jar
RUN java -Djarmode=layertools -jar app.jar extract
FROM eclipse-temurin:21-jre-alpine
RUN addgroup --system app && adduser -S -G app app
# Layers in dependency order (max cache)
COPY --from=builder /app/dependencies/ ./
COPY --from=builder /app/spring-boot-loader/ ./
COPY --from=builder /app/snapshot-dependencies/ ./
COPY --from=builder /app/application/ ./
USER app
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]
Pattern 4: Build + Alpline (Node.js)
FROM node:22-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-alpine
RUN addgroup --system app && adduser -S -G app app
COPY --from=builder /app/dist /app
COPY --from=builder /app/node_modules /app/node_modules
USER app
CMD ["node", "/app/index.js"]
Pattern 5: Python Dependencies + Slim Runtime
FROM python:3.12 AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
FROM python:3.12-slim
RUN groupadd --system app && useradd --system -g app app
COPY --from=builder /root/.local /home/app/.local
COPY . /app
ENV PATH=/home/app/.local/bin:$PATH
USER app
CMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0"]
Layer Caching Strategy
Docker builds layers from top to bottom.
A changed layer invalidates ALL layers below it.
✅ CORRECT order:
FROM base ← rarely changes
RUN install-system-deps ← changes with system updates
COPY go.mod go.sum ./ ← changes with dependency changes
RUN go mod download ← changes with dependency changes
COPY . . ← changes every commit ← MUST BE LAST
❌ WRONG order (slow builds):
COPY . . ← changes every commit
RUN go mod download ← reruns every time!
.dockerignore
# .dockerignore
.git
.gitignore
*.md
.env
.env.*
Dockerfile
docker-compose*.yml
node_modules
__pycache__
*.pyc
.git
.idea
.vscode
*.log
tmp/
Workflow — Recommended Writing Process
Step 1: Determine language and runtime: Go/Java/Node.js/Python → choose base image
Step 2: Choose build mode: single-stage/multi-stage/Spring Boot layered → select template from templates
Step 3: Write Dockerfile: first COPY dependencies → RUN install → COPY source code → CMD
Step 4: Verify: docker build -t app . + docker run + dive analyze image size
Step 5: Production hardening: non-root USER, HEALTHCHECK, pinned digest, Security checks
Gotchas — Common Pitfalls
COPY . .beforeRUN install: Every code change invalidates the dependency layer — rebuilds from scratch. → Recovery: Always copy deps first:COPY package.json . → RUN install → COPY src/ ..- Root user by default: Always
USER <non-root>at the end of Dockerfile. Escaping the container as root = host root. → Recovery: AddRUN addgroup -S app && adduser -S app -G app+USER app; verify withdocker exec myapp whoami. ENV SECRET=valuein Dockerfile: Baked into image layers forever. → Recovery: Use BuildKit--mount=type=secretor runtime injectiondocker run -e SECRET=$VAL.- No
.dockerignore: Sends entire project to build context — slow and leaks sensitive files. → Recovery: Create.dockerignorewith at minimum.git node_modules .env; verify withdocker build --no-cache . 2>&1 | head -1. RUN apt update && apt installwithout cleanup: Leaves package lists in layer. → Recovery: Chain with&& rm -rf /var/lib/apt/lists/*; for apk:--no-cacheflag.- Heavy base image:
ubuntu:22.04(77 MB) vsalpine:3.20(7 MB). → Recovery: Prefer alpine; if glibc needed, usedebian:bookworm-slim; check size withdocker images.
Boundary — Capability boundaries (applicable and non-applicable scenarios)
| Category | Scenario | Description |
|---|---|---|
| ✅ Can do | Write production-grade Dockerfile | Complete reference for 14 instructions + best practices |
| ✅ Can do | Multi-stage build (Go/Java/Node/Python) | 5 language-specific templates |
| ✅ Can do | Layer caching optimization | COPY dependencies first + RUN consolidation + BuildKit cache mount |
| ✅ Can do | Image slimming | 5-step roadmap: multi-stage→Alpine→distroless→cleanup→dive |
| ⚠️ Conditional | Private dependency installation | Requires BuildKit --secret or SSH forwarding |
| ⚠️ Conditional | CMD vs ENTRYPOINT selection | See the decision tree in the instruction reference (use ENTRYPOINT for tools, CMD for services) |
| ❌ Out of scope | docker build command execution | Use docker-build |
| ❌ Out of scope | Multi-platform build (arm64/amd64) | Use docker-buildx |
| ❌ Out of scope | Container orchestration (multi-container) | Use docker-compose |
When NOT to Use This Skill
| ❌ Skip | ✅ Use Instead |
|---|---|
Building images (docker build ...) |
docker-build |
| Multi-platform builds | docker-buildx |
| Compose file authoring | docker-compose |
| Docker basics | docker-basics |
| Running containers | docker-run |
Security & Stability
- All Dockerfile templates are educational. Review and harden before production use.
- Never embed secrets in Dockerfile. Use BuildKit
--mount=type=secretor runtime injection. - Always
USER <non-root>for production. UseCOPY --chownwhen copying files for that user. - Pin base image digests for production reproducibility.
📚 Official documentation reference
| Document | Address |
|---|---|
| Dockerfile reference | https://docs.docker.com/reference/dockerfile/ |
| Docker Build overview | https://docs.docker.com/build/ |
| Build best practices | https://docs.docker.com/build/building/best-practices/ |
| Multi-stage builds | https://docs.docker.com/build/building/multi-stage/ |
| .dockerignore | https://docs.docker.com/build/building/context/#dockerignore-files |
| Image layers and caching | https://docs.docker.com/build/cache/ |
🧭 Docker Skills Journey
📍 You are here:
docker-dockerfile— Dockerfile writing
basics → dockerfile → build → buildx → run → compose → ...
→ Next: docker-build — Build images with docker build