Dockerfile Best Practices
Writing efficient Dockerfiles is crucial for creating production-ready images. This lesson covers best practices that reduce image size, improve build times, and enhance security.
Layer Optimization
Minimize Layer Count
Each instruction creates a layer. Combine related commands:
# Bad - 3 layers
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
# Good - 1 layer
RUN apt-get update \
&& apt-get install -y curl \
&& rm -rf /var/lib/apt/lists/*
Order by Change Frequency
Put frequently changing instructions last:
# Good order
FROM node:18-alpine
# Rarely changes
WORKDIR /app
# Changes when dependencies change
COPY package*.json ./
RUN npm ci
# Changes with every code update
COPY . .
# Never changes
CMD ["npm", "start"]
Image Size Reduction
Use Minimal Base Images
# Large - 1GB+
FROM node:18
# Medium - 200MB
FROM node:18-slim
# Small - 175MB
FROM node:18-alpine
# Smallest - 5MB
FROM alpine:3.19
Clean Up in Same Layer
# Bad - cleanup in separate layer doesn't reduce size
RUN apt-get update && apt-get install -y build-essential
RUN rm -rf /var/lib/apt/lists/*
# Good - cleanup in same layer
RUN apt-get update \
&& apt-get install -y build-essential \
&& rm -rf /var/lib/apt/lists/*
Use Multi-Stage Builds
# Build stage
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage - only runtime
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]
Don't Include Unnecessary Files
# .dockerignore
node_modules
.git
*.md
.env*
Dockerfile*
docker-compose*
.dockerignore
coverage
.nyc_output
Security Best Practices
Don't Run as Root
FROM node:18-alpine
# Create non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --chown=appuser:appgroup . .
# Switch to non-root
USER appuser
CMD ["node", "app.js"]
Use Specific Image Tags
# Bad - unpredictable
FROM node:latest
# Good - specific version
FROM node:18.19.0-alpine3.19
# Best - use digest
FROM node@sha256:abc123...
Minimize Installed Packages
# Install only what's needed
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
curl \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
Scan for Vulnerabilities
# Use Docker Scout
docker scout cves myimage:tag
# Or Trivy
trivy image myimage:tag
Build Efficiency
Leverage Build Cache
# Dependencies change less often than source
COPY package.json package-lock.json ./
RUN npm ci
# Source code copied after dependencies
COPY src/ ./src/
Use .dockerignore
# .dockerignore
node_modules
npm-debug.log
.git
.gitignore
README.md
.env*
Dockerfile*
docker-compose*
.dockerignore
coverage
.nyc_output
*.log
.DS_Store
Pin Versions
# Pin package versions
RUN apt-get install -y \
curl=7.88.1-10 \
nginx=1.22.1-9
# Pin npm packages
RUN npm ci # Uses package-lock.json
Dockerfile Structure
Standard Order
# 1. Base image
FROM node:18-alpine
# 2. Labels
LABEL maintainer="dev@example.com"
LABEL version="1.0"
# 3. Environment variables
ENV NODE_ENV=production
ENV PORT=3000
# 4. Working directory
WORKDIR /app
# 5. Install dependencies
COPY package*.json ./
RUN npm ci --only=production
# 6. Copy application
COPY . .
# 7. Create non-root user
RUN addgroup -S app && adduser -S app -G app
USER app
# 8. Expose ports
EXPOSE 3000
# 9. Health check
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget --spider http://localhost:3000/health || exit 1
# 10. Entry point / command
CMD ["node", "app.js"]
Specific Language Patterns
Node.js
FROM node:18-alpine
WORKDIR /app
# Install dependencies first (caching)
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
COPY . .
USER node
EXPOSE 3000
CMD ["node", "app.js"]
Python
FROM python:3.11-slim
WORKDIR /app
# Install system deps
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
&& rm -rf /var/lib/apt/lists/*
# Install Python deps
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN useradd -m appuser
USER appuser
EXPOSE 8000
CMD ["python", "app.py"]
Go
# 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 -o main .
# Production stage
FROM scratch
COPY --from=builder /app/main /main
EXPOSE 8080
ENTRYPOINT ["/main"]
Common Anti-Patterns
Don't Use ADD for Simple Copies
# Bad - ADD has extra features you don't need
ADD ./app /app
# Good - COPY is simpler and clearer
COPY ./app /app
Don't Store Secrets in Images
# Bad - secret in image
ENV API_KEY=secret123
# Good - set at runtime
# (Don't include ENV for secrets)
Don't Install Unnecessary Dev Dependencies
# Bad
RUN npm install
# Good - production only
RUN npm ci --only=production
Don't Ignore .dockerignore
# Without .dockerignore, this copies everything
COPY . .
# Including node_modules, .git, secrets, etc.
Checklist
Before pushing an image:
- Using specific base image tag
- Multi-stage build (if applicable)
- .dockerignore configured
- Running as non-root user
- No secrets in image
- Production dependencies only
- Cache-optimized layer order
- Health check defined
- Scanned for vulnerabilities
- Image size reasonable
Key Takeaways
- Order instructions by change frequency for cache efficiency
- Use multi-stage builds to separate build and runtime
- Always run as non-root in production
- Pin versions for reproducibility
- Use .dockerignore to exclude unnecessary files
- Clean up in the same layer as installation
- Choose minimal base images
- Scan images for vulnerabilities

