Introduction
Writing efficient Dockerfiles is crucial for fast builds, secure containers, and optimal image sizes. This guide covers best practices for creating production-ready Dockerfiles.
Basic Dockerfile Structure
Simple Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
CMD ["npm", "start"]
Issues:
- ❌ Copies source before dependencies
- ❌ No layer optimization
- ❌ Not using cache effectively
- ❌ Missing security considerations
Best Practices
1. Use Specific Base Images
# ❌ Bad - uses latest (unpredictable)
FROM node:latest
# ✅ Good - specific version
FROM node:18-alpine
# ✅ Better - specific digest
FROM node:18-alpine@sha256:abc123...
Why Alpine?
- Smaller image size (5MB vs 80MB+)
- Security-focused
- Still fully functional
2. Order Instructions for Caching
# ❌ Bad - invalidates cache on every code change
FROM node:18-alpine
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
# ✅ Good - dependencies cached separately
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
Docker caches layers:
- Cache invalidates when layer changes
- Dependencies change less than code
- Copy dependencies first!
3. Use Multi-Stage Builds
# Stage 1: Build
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
# Stage 2: Runtime
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
CMD ["node", "dist/index.js"]
Benefits:
- Smaller final image (no build tools)
- Better security (no dev dependencies)
- Faster deployments
4. Minimize Layers
# ❌ Bad - many layers
RUN apt-get update
RUN apt-get install -y git
RUN apt-get install -y curl
RUN apt-get clean
# ✅ Good - single layer
RUN apt-get update && \
apt-get install -y git curl && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
5. Use .dockerignore
Create .dockerignore:
node_modules
npm-debug.log
.git
.env
dist
*.md
Benefits:
- Faster builds
- Smaller context
- Excludes sensitive files
6. Run as Non-Root User
# ❌ Bad - runs as root
FROM node:18-alpine
CMD ["node", "app.js"]
# ✅ Good - non-root user
FROM node:18-alpine
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
USER nodejs
CMD ["node", "app.js"]
7. Use COPY Instead of ADD
# ❌ ADD has extra features (tar extraction, URLs)
ADD https://example.com/file.tar.gz /app/
# ✅ COPY is explicit and predictable
COPY local-file.txt /app/
Optimization Techniques
Reduce Image Size
# Multi-stage build
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:18-alpine
WORKDIR /app
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/package.json ./
RUN npm ci --only=production && npm cache clean --force
USER nodejs
CMD ["node", "dist/index.js"]
Layer Caching Strategy
FROM node:18-alpine
# Layer 1: Install system dependencies (rarely changes)
RUN apk add --no-cache git python3 make g++
# Layer 2: Copy package files (changes occasionally)
COPY package*.json ./
# Layer 3: Install dependencies (changes with package.json)
RUN npm ci --only=production
# Layer 4: Copy application code (changes frequently)
COPY . .
# Layer 5: Build (changes with code)
RUN npm run build
Security Best Practices
1. Scan Base Images
docker scan node:18-alpine
2. Keep Images Updated
# Regularly update base images
FROM node:20-alpine # Latest LTS
3. Don’t Store Secrets
# ❌ Bad - secrets in image
ENV API_KEY=secret123
COPY .env ./
# ✅ Good - use secrets at runtime
# docker run -e API_KEY=secret123 ...
# Or use Docker secrets
4. Minimal Attack Surface
# Only install what you need
RUN apt-get update && \
apt-get install -y only-required-package && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
Language-Specific Examples
Node.js
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:18-alpine
WORKDIR /app
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/package.json ./
RUN npm ci --only=production && npm cache clean --force
USER nodejs
EXPOSE 3000
CMD ["node", "dist/index.js"]
Python
FROM python:3.11-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user -r requirements.txt
FROM python:3.11-slim
WORKDIR /app
RUN useradd -m -u 1000 appuser
COPY --from=builder /root/.local /home/appuser/.local
COPY --chown=appuser:appuser . .
USER appuser
ENV PATH=/home/appuser/.local/bin:$PATH
CMD ["python", "app.py"]
Go
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 app .
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/app .
CMD ["./app"]
Common Mistakes
❌ Don’t Do This
1. Using :latest tag
FROM node:latest # ❌ Unpredictable
2. Installing unnecessary packages
RUN apt-get install -y build-essential git curl wget vim # ❌ Too much
3. Not cleaning up
RUN apt-get install -y package
# ❌ Leaves apt cache
4. Exposing unnecessary ports
EXPOSE 22 80 443 3000 8000 # ❌ Only expose what's needed
Tools
Use our tools:
- Dockerfile Syntax Checker - Validate syntax
Other tools:
docker buildwith--no-cacheto testdocker historyto see layer sizesdiveto analyze image layers
Conclusion
Writing good Dockerfiles requires:
Key principles:
- Specific base image versions
- Optimal layer ordering
- Multi-stage builds
- Minimal image size
- Security considerations
- Non-root users
Remember:
- Cache dependencies separately
- Use multi-stage builds
- Minimize layers
- Scan for vulnerabilities
- Keep images updated
Next Steps
- Validate with Dockerfile Checker
- Learn about Container Security
- Explore Docker Optimization