Docker Patterns
Multi-Stage Build (Go example)
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 -o /bin/app ./cmd/server
FROM gcr.io/distroless/static-debian12
COPY --from=builder /bin/app /app
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/app"]
Multi-Stage Build (Node.js)
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]
.dockerignore
node_modules
.git
.env
.env.*
*.log
dist
coverage
.DS_Store
Dockerfile*
docker-compose*
README.md
Layer Caching — copy deps first
# GOOD: dependencies layer cached unless package.json changes
COPY package*.json ./
RUN npm ci
COPY . .
# BAD: every code change busts the npm ci layer
COPY . .
RUN npm ci
Health Check
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
docker-compose.yml
services:
app:
build:
context: .
target: runner
ports:
- "3000:3000"
environment:
- DATABASE_URL=${DATABASE_URL}
depends_on:
db:
condition: service_healthy
restart: unless-stopped
networks:
- backend
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- pg_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
interval: 10s
timeout: 5s
retries: 5
networks:
- backend
redis:
image: redis:7-alpine
command: redis-server --appendonly yes
volumes:
- redis_data:/data
networks:
- backend
volumes:
pg_data:
redis_data:
networks:
backend:
driver: bridge
Resource Limits
services:
app:
deploy:
resources:
limits:
cpus: "0.5"
memory: 512M
reservations:
cpus: "0.25"
memory: 256M
Key Rules
- Never run as root — add
USER nonrootorUSER node - Pin base image digests in production:
node:20-alpine@sha256:... - Use
COPY --chown=user:userinstead ofRUN chown(avoids extra layer) CMDvsENTRYPOINT: use ENTRYPOINT for the binary, CMD for default argsRUN --mount=type=cachespeeds up package manager installs in BuildKit