Container Best Practices Skill
Overview
Apply industry-standard best practices for building optimized, maintainable, and efficient Docker containers. Master multi-stage builds, layer caching, base image selection, and build optimization strategies to create production-ready container images.
Core Principles
Write Efficient Dockerfiles
Order Instructions for Maximum Cache Efficiency:
Structure Dockerfile layers from least to most frequently changing:
# 1. Base image (changes rarely)
FROM node:20-alpine AS base
# 2. System dependencies (changes occasionally)
RUN apk add --no-cache \
python3 \
make \
g++
# 3. Working directory setup
WORKDIR /app
# 4. Package manifest files (changes moderately)
COPY package.json package-lock.json ./
# 5. Dependencies installation (leverages cache when manifest unchanged)
RUN npm ci --only=production
# 6. Application code (changes frequently)
COPY . .
# 7. Build step (only when code changes)
RUN npm run build
# 8. Runtime configuration
EXPOSE 3000
CMD ["node", "dist/main.js"]
Combine RUN Commands to Reduce Layers:
Minimize image size by chaining related commands:
# Bad: Creates 3 layers
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get clean
# Good: Creates 1 layer
RUN apt-get update && \
apt-get install -y curl && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
Use .dockerignore to Exclude Unnecessary Files:
Create .dockerignore to prevent copying build artifacts and sensitive files:
# Development files node_modules/ npm-debug.log* .git/ .gitignore # Test files *.test.js coverage/ .nyc_output/ # Documentation README.md docs/ # IDE files .vscode/ .idea/ # Environment files .env .env.local *.key *.pem
Implement Multi-Stage Builds
Separate Build and Runtime Environments:
Use multi-stage builds to create minimal production images:
# Stage 1: Build environment with all dev dependencies
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build && \
npm run test
# Stage 2: Production environment with only runtime dependencies
FROM node:20-alpine AS production
WORKDIR /app
# Copy only necessary files from builder
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./
# Security: Run as non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001 && \
chown -R nodejs:nodejs /app
USER nodejs
EXPOSE 3000
CMD ["node", "dist/main.js"]
Create Specialized Build Targets:
Define multiple targets for different use cases:
# Development stage with hot reload
FROM node:20-alpine AS development
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000 9229
CMD ["npm", "run", "dev"]
# Testing stage with test dependencies
FROM development AS testing
RUN npm run lint && \
npm run test:unit && \
npm run test:integration
# Build stage
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage (minimal)
FROM node:20-alpine AS production
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
USER node
EXPOSE 3000
CMD ["node", "dist/main.js"]
Build specific stages:
# Development docker build --target development -t myapp:dev . # Testing docker build --target testing -t myapp:test . # Production docker build --target production -t myapp:prod .
Optimize Base Image Selection
Choose Minimal Base Images:
Select the smallest viable base image for your runtime:
# Python examples by size FROM python:3.11-alpine # ~50MB (best for simple apps) FROM python:3.11-slim # ~120MB (good compatibility) FROM python:3.11 # ~900MB (avoid in production) # Node.js examples by size FROM node:20-alpine # ~110MB (best for production) FROM node:20-slim # ~170MB (good compatibility) FROM node:20 # ~900MB (avoid in production) # Distroless images (Google) FROM gcr.io/distroless/nodejs20-debian12 # Minimal attack surface FROM gcr.io/distroless/python3-debian12 # No shell, no package manager
Use Distroless for Maximum Security:
Distroless images contain only your application and runtime dependencies:
# Build stage FROM node:20 AS builder WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY . . # Production: Distroless image FROM gcr.io/distroless/nodejs20-debian12 WORKDIR /app COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/dist ./dist USER nonroot:nonroot EXPOSE 3000 CMD ["dist/main.js"]
Pin Specific Versions:
Always use specific version tags for reproducibility:
# Bad: Version changes unexpectedly FROM node:20 # Good: Specific version pinned FROM node:20.11.1-alpine3.19 # Better: Use digest for immutability FROM node:20.11.1-alpine3.19@sha256:abc123...
Leverage Build Cache Effectively
Structure for Cache Reuse:
Place frequently changing instructions after stable ones:
FROM python:3.11-slim
# System packages (rarely change)
RUN apt-get update && \
apt-get install -y --no-install-recommends \
postgresql-client && \
apt-get clean
# Requirements (change occasionally)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Application code (changes frequently)
COPY . .
CMD ["python", "app.py"]
Use BuildKit Cache Mounts:
Enable BuildKit for advanced caching:
# syntax=docker/dockerfile:1.4
FROM python:3.11-slim
# Cache pip downloads across builds
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
# Cache apt packages
RUN --mount=type=cache,target=/var/cache/apt \
--mount=type=cache,target=/var/lib/apt \
apt-get update && \
apt-get install -y postgresql-client
Build with BuildKit:
DOCKER_BUILDKIT=1 docker build -t myapp:latest .
Implement Layer Caching in CI/CD:
Use registry cache for faster CI builds:
# GitHub Actions with BuildKit cache docker buildx build \ --cache-from type=registry,ref=ghcr.io/org/app:cache \ --cache-to type=registry,ref=ghcr.io/org/app:cache,mode=max \ --tag ghcr.io/org/app:latest \ --push .
Optimize Runtime Configuration
Configure Health Checks:
Define health checks for container orchestration:
FROM node:20-alpine WORKDIR /app COPY . . # HTTP health check HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD node healthcheck.js # Alternative: Using curl HEALTHCHECK --interval=30s --timeout=3s \ CMD curl -f http://localhost:3000/health || exit 1 EXPOSE 3000 CMD ["node", "server.js"]
Set Appropriate Working Directory:
Always use absolute paths for WORKDIR:
# Bad: Relative path WORKDIR app # Good: Absolute path WORKDIR /app # Better: Clear organization WORKDIR /opt/application
Use COPY Instead of ADD:
Prefer COPY for clarity unless you need ADD's special features:
# Bad: ADD has implicit behavior (extracts tars, fetches URLs) ADD archive.tar.gz /app/ # Good: COPY is explicit and predictable COPY src/ /app/src/ COPY package.json /app/ # OK: ADD when you need tar extraction ADD rootfs.tar.gz /
Docker Compose Best Practices
Structure Compose Files for Environments
Create Environment-Specific Overrides:
Use base compose file with environment overrides:
# docker-compose.yml (base)
version: '3.9'
services:
app:
build:
context: .
dockerfile: Dockerfile
environment:
NODE_ENV: production
restart: unless-stopped
db:
image: postgres:16-alpine
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_DB: ${DB_NAME}
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
postgres_data:
# docker-compose.override.yml (development)
version: '3.9'
services:
app:
build:
target: development
volumes:
- .:/app
- /app/node_modules
ports:
- "3000:3000"
- "9229:9229" # Debug port
environment:
NODE_ENV: development
db:
ports:
- "5432:5432"
# docker-compose.prod.yml (production)
version: '3.9'
services:
app:
build:
target: production
deploy:
replicas: 3
resources:
limits:
cpus: '0.5'
memory: 512M
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
Use with:
# Development (uses docker-compose.override.yml automatically) docker compose up # Production docker compose -f docker-compose.yml -f docker-compose.prod.yml up
Implement Dependency Management
Control Startup Order:
Use depends_on with health checks:
version: '3.9'
services:
app:
image: myapp:latest
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
db:
image: postgres:16-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
Configure Resource Limits
Set Memory and CPU Constraints:
Prevent resource exhaustion:
version: '3.9'
services:
app:
image: myapp:latest
deploy:
resources:
limits:
cpus: '1.0'
memory: 1G
reservations:
cpus: '0.5'
memory: 512M
mem_limit: 1g
cpus: 1.0
Build Optimization Strategies
Implement Build Arguments
Parameterize Builds:
Use ARG for build-time configuration:
ARG NODE_VERSION=20
FROM node:${NODE_VERSION}-alpine
ARG BUILD_DATE
ARG VERSION
ARG REVISION
LABEL org.opencontainers.image.created="${BUILD_DATE}" \
org.opencontainers.image.version="${VERSION}" \
org.opencontainers.image.revision="${REVISION}"
ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}
WORKDIR /app
COPY . .
RUN npm ci --only=${NODE_ENV}
CMD ["node", "server.js"]
Build with arguments:
docker build \ --build-arg NODE_VERSION=20 \ --build-arg VERSION=1.2.3 \ --build-arg BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ --build-arg REVISION=$(git rev-parse --short HEAD) \ -t myapp:1.2.3 .
Use Multi-Platform Builds
Build for Multiple Architectures:
Create images for AMD64 and ARM64:
# Create buildx builder docker buildx create --name multiarch --use # Build for multiple platforms docker buildx build \ --platform linux/amd64,linux/arm64 \ --tag ghcr.io/org/app:latest \ --push .
Platform-specific optimizations:
FROM --platform=$BUILDPLATFORM node:20-alpine AS builder ARG TARGETPLATFORM ARG BUILDPLATFORM RUN echo "Building on $BUILDPLATFORM for $TARGETPLATFORM" WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build FROM node:20-alpine COPY --from=builder /app/dist ./dist CMD ["node", "dist/main.js"]
Performance Optimization
Minimize Image Size
Remove Unnecessary Files:
Clean up in the same layer:
FROM ubuntu:22.04
RUN apt-get update && \
apt-get install -y --no-install-recommends \
build-essential \
python3 && \
# Build your app here
apt-get purge -y build-essential && \
apt-get autoremove -y && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
Analyze Image Layers:
Use dive to inspect layers:
# Install dive brew install dive # Analyze image dive myapp:latest
Implement Parallel Builds
Speed Up Multi-Stage Builds:
Use multiple FROM statements for parallel builds:
# These stages build in parallel FROM node:20 AS deps WORKDIR /app COPY package*.json ./ RUN npm ci FROM node:20 AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build FROM node:20 AS tester WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . RUN npm test # Final stage depends on builder and tester FROM node:20-alpine WORKDIR /app COPY --from=builder /app/dist ./dist COPY --from=deps /app/node_modules ./node_modules CMD ["node", "dist/main.js"]
Official References
- •Dockerfile Best Practices: https://docs.docker.com/develop/dev-best-practices/
- •Multi-Stage Builds: https://docs.docker.com/build/building/multi-stage/
- •BuildKit: https://docs.docker.com/build/buildkit/
- •Docker Compose Reference: https://docs.docker.com/compose/compose-file/
- •Build Cache: https://docs.docker.com/build/cache/
Related Skills
- •Container Security - Hardening and vulnerability management
- •Docker Skill - Container runtime operations
- •DevOps Practices - CI/CD integration and deployment