Docker Best Practices: Building Production-Ready Containers
Docker has revolutionized application deployment, but building production-ready containers requires following best practices. Here’s a comprehensive guide to creating secure, efficient Docker images.
Dockerfile Optimization
Use Multi-Stage Builds
Reduce image size and improve security:
# Build stage
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./
EXPOSE 3000
CMD ["node", "dist/index.js"]
Minimize Layers
Each instruction creates a layer—combine commands when possible:
# ❌ Bad - multiple layers
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y git
# ✅ Good - single layer
RUN apt-get update && \
apt-get install -y curl git && \
rm -rf /var/lib/apt/lists/*
Use .dockerignore
Exclude unnecessary files:
# .dockerignore
node_modules
npm-debug.log
.git
.env
*.md
.vscode
coverage
.DS_Store
Base Image Selection
Use Official Images
# ✅ Official image
FROM node:18-alpine
# ❌ Unknown source
FROM some-random-user/node
Choose Minimal Base Images
# Full image: 900MB
FROM node:18
# Slim image: 200MB
FROM node:18-slim
# Alpine image: 120MB
FROM node:18-alpine
Pin Specific Versions
# ❌ Bad - latest changes unexpectedly
FROM node:latest
# ✅ Good - reproducible builds
FROM node:18.17.0-alpine3.18
Security Best Practices
Don’t Run as Root
FROM node:18-alpine
# Create non-root user
RUN addgroup -g 1001 appgroup && \
adduser -D -u 1001 -G appgroup appuser
WORKDIR /app
COPY --chown=appuser:appgroup . .
# Switch to non-root user
USER appuser
CMD ["node", "index.js"]
Scan for Vulnerabilities
# Use Docker Scout
docker scout cves my-image:latest
# Or Trivy
trivy image my-image:latest
Use Secret Management
# ❌ Bad - secrets in image
ENV API_KEY=secret123
# ✅ Good - secrets at runtime
# docker run -e API_KEY=$API_KEY my-image
Minimize Attack Surface
# Remove unnecessary packages
RUN apk add --no-cache python3 && \
apk del apk-tools
# Don't include development tools in production
Performance Optimization
Layer Caching Strategy
Order instructions from least to most frequently changed:
# 1. Base image (rarely changes)
FROM node:18-alpine
# 2. System dependencies (rarely changes)
RUN apk add --no-cache python3 make g++
# 3. Application dependencies (changes occasionally)
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# 4. Application code (changes frequently)
COPY . .
CMD ["node", "index.js"]
Use BuildKit
Enable advanced caching and parallel builds:
# Enable BuildKit
export DOCKER_BUILDKIT=1
docker build -t my-app .
# Or in docker-compose
COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose build
Optimize Node.js Images
FROM node:18-alpine
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production && \
npm cache clean --force
# Copy source
COPY . .
# Use production mode
ENV NODE_ENV=production
CMD ["node", "index.js"]
Resource Management
Set Resource Limits
# docker-compose.yml
services:
app:
image: my-app
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M
Health Checks
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node healthcheck.js || exit 1
Logging and Monitoring
Log to STDOUT/STDERR
// Don't write logs to files inside container
console.log('Application started');
console.error('Error occurred');
Add Labels
LABEL maintainer="team@example.com"
LABEL version="1.0"
LABEL description="Production web application"
LABEL org.opencontainers.image.source="https://github.com/user/repo"
Development vs Production
Use Different Dockerfiles
# Dockerfile.dev
FROM node:18
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["npm", "run", "dev"]
# Dockerfile.prod
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
CMD ["node", "index.js"]
Docker Compose for Development
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile.dev
volumes:
- .:/app
- /app/node_modules
ports:
- "3000:3000"
environment:
- NODE_ENV=development
CI/CD Integration
GitHub Actions Example
name: Docker Build and Push
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: user/app:latest
cache-from: type=gha
cache-to: type=gha,mode=max
Common Mistakes to Avoid
- ❌ Running containers as root
- ❌ Using
latesttag in production - ❌ Not using
.dockerignore - ❌ Installing unnecessary packages
- ❌ Storing secrets in images
- ❌ Not setting resource limits
- ❌ Ignoring security scans
- ❌ Poor layer caching strategy
Quick Checklist
- ✅ Use official, minimal base images
- ✅ Pin specific versions
- ✅ Use multi-stage builds
- ✅ Run as non-root user
- ✅ Add
.dockerignore - ✅ Scan for vulnerabilities
- ✅ Set health checks
- ✅ Add resource limits
- ✅ Use BuildKit
- ✅ Optimize layer caching
Conclusion
Building production-ready Docker images requires attention to security, performance, and maintainability. Follow these best practices to create containers that are secure, efficient, and easy to manage in production environments.