.dockerignore, pin versions, run as non-root.FROM node:20-alpine AS build ... FROM node:20-alpine ... COPY --from=build /app/dist ./dist.node_modules/ or build tools into the final stage -- only copy compiled output and production dependencies.latest tag for base images -- pin to specific version.--mount=type=secret or runtime env vars..dockerignore to exclude .git/, node_modules/, tests.COPY instead of ADD unless you need tar extraction.| Practice | Wrong | Right |
|---|---|---|
| Base image | FROM node:latest | FROM node:20.11-alpine3.19 |
| Non-root user | Running as root | USER app |
| Dependencies first | COPY . . then install | COPY package*.json ./ then install |
| Multi-stage | Single FROM with build tools | Build stage + runtime stage |
| .dockerignore | Not using one | Exclude .git/, node_modules/, tests |
| Secrets | COPY .env . | --mount=type=secret |
| Apt cleanup | No cleanup | rm -rf /var/lib/apt/lists/* |
| COPY vs ADD | ADD . . | COPY . . |
| Health check | No HEALTHCHECK | HEALTHCHECK CMD ... |
| CMD form | CMD node app.js | CMD ["node", "app.js"] |
START
├── Compiled language (Go, Rust, C++)?
│ ├── YES → Multi-stage: build → copy binary to scratch/distroless
│ └── NO ↓
├── Node.js application?
│ ├── YES → Multi-stage: build (npm ci + build) → runtime (alpine + dist + prod deps)
│ └── NO ↓
├── Python application?
│ ├── YES → Multi-stage: build (pip install) → runtime (slim + site-packages)
│ └── NO ↓
├── Java application?
│ ├── YES → Multi-stage: Maven/Gradle build → copy JAR to JRE-alpine
│ └── NO ↓
├── Static site (React, Vue)?
│ ├── YES → Multi-stage: node build → nginx:alpine serving dist/
│ └── NO ↓
└── DEFAULT → Multi-stage with appropriate builder and slim runtime
Exclude unnecessary files from build context. [src1]
# .dockerignore
node_modules/
.git/
*.md
.env
.env.*
coverage/
tests/
.github/
.vscode/
*.log
Verify: docker build . → context size should be small.
Use specific versions for reproducible builds. [src1]
FROM node:20.11-alpine3.19
Verify: Same digest every pull.
Copy deps first, install, then copy source. [src1]
COPY package.json package-lock.json ./
RUN npm ci --only=production
COPY . .
Verify: Change source → rebuild → npm ci shows "CACHED".
Separate build from runtime. [src2]
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY --from=build /app/dist ./dist
CMD ["node", "dist/index.js"]
Verify: docker images → final image is significantly smaller.
Run as unprivileged user. [src1]
RUN addgroup -S app && adduser -S app -G app
COPY --chown=app:app . .
USER app
Verify: docker exec <container> whoami → app.
Monitor container health. [src7]
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
Verify: docker inspect <container> → shows health status.
# Input: Node.js app with package.json and src/
# Output: Optimized production container (~150MB)
FROM node:20.11-alpine3.19 AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY tsconfig.json ./
COPY src/ ./src/
RUN npm run build
RUN npm prune --production
FROM node:20.11-alpine3.19
RUN addgroup -S app && adduser -S app -G app
WORKDIR /app
COPY --from=build --chown=app:app /app/node_modules ./node_modules
COPY --from=build --chown=app:app /app/dist ./dist
COPY --from=build --chown=app:app /app/package.json ./
USER app
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
CMD ["node", "dist/index.js"]
# Input: Go app with go.mod
# Output: Minimal container (~15MB)
FROM golang:1.22-alpine AS build
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /server ./cmd/server
FROM scratch
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=build /server /server
EXPOSE 8080
ENTRYPOINT ["/server"]
# Input: Python app with requirements.txt
# Output: Slim production container (~120MB)
FROM python:3.12-slim AS build
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends gcc libpq-dev \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt ./
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
FROM python:3.12-slim
WORKDIR /app
RUN groupadd -r app && useradd -r -g app app
COPY --from=build /install /usr/local
RUN apt-get update && apt-get install -y --no-install-recommends libpq5 \
&& rm -rf /var/lib/apt/lists/*
COPY --chown=app:app . .
USER app
EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:create_app()"]
# Input: React/Vue/Angular project
# Output: Nginx serving static files (~25MB)
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:1.25-alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget --no-verbose --tries=1 --spider http://localhost/ || exit 1
# ❌ BAD — 1GB+ image with build tools and devDependencies
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
CMD ["node", "dist/index.js"]
# ✅ GOOD — ~150MB with only production artifacts
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY --from=build /app/dist ./dist
CMD ["node", "dist/index.js"]
# ❌ BAD — any source change invalidates npm ci cache
COPY . .
RUN npm ci
# ✅ GOOD — npm ci cached unless package.json changes
COPY package*.json ./
RUN npm ci
COPY . .
# ❌ BAD — security risk
FROM node:20-alpine
COPY . .
CMD ["node", "index.js"]
# ✅ GOOD — unprivileged user
FROM node:20-alpine
RUN addgroup -S app && adduser -S app -G app
COPY --chown=app:app . .
USER app
CMD ["node", "index.js"]
COPY . . before install. Fix: copy package files first. [src1]COPY .env . persists in layers. Fix: use --mount=type=secret. [src3]&& rm -rf /var/lib/apt/lists/* in same RUN. [src1]CMD node app.js wraps in sh -c. Fix: CMD ["node", "app.js"]. [src7]# Check image size
docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"
# Analyze layers
docker history <image>:<tag>
# Deep dive with dive tool
dive <image>:<tag>
# Check if running as root
docker exec <container> whoami
# Health check status
docker inspect <container> --format '{{json .State.Health}}'
# Scan vulnerabilities
docker scout cve <image>:<tag>
| Version | Status | Breaking Changes | Migration Notes |
|---|---|---|---|
| Docker 26.x | Current | BuildKit required for --mount | Ensure BuildKit enabled |
| Docker 25.x | Active | Improved compression | — |
| Docker 24.x | Active | BuildKit is default | Remove DOCKER_BUILDKIT=1 |
| Multi-stage | Since 17.05 | — | Supported on all modern Docker |
| BuildKit | Since 18.09 | — | Default since 24.x |
| Distroless | Stable | No shell | Use debug variant for debugging |
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Building production images | Dev-only containers | docker-compose with volumes |
| Need smallest image size | Need debug tools in image | Debug base image for dev |
| Deploying to K8s or cloud | One-off scripts | Direct execution or serverless |
| Security-sensitive deployment | Prototype or hackathon | Simple single-stage Dockerfile |
scratch images have no shell, no package manager. Cannot docker exec into them.npm prune --production may leave npm cache. Use npm cache clean --force after.