Why Docker Best Practices Matter
Docker has transformed how we build and deploy applications, but poorly constructed containers can lead to security vulnerabilities, bloated images, and operational headaches. Following best practices from the start saves countless hours in production.
This guide covers essential techniques for building containers that are secure, efficient, and maintainable.
Choosing the Right Base Image
Your base image choice impacts security, size, and compatibility:
- Alpine Linux - Minimal size (~5MB), ideal for production when dependencies allow
- Distroless images - Google's minimal images contain only your app and runtime
- Official language images - Python, Node.js, Go with known configurations
- Debian/Ubuntu slim variants - Good balance of compatibility and size
Always specify exact version tags (e.g., python:3.11-slim-bookworm) rather than latest for reproducible builds.
Optimizing Dockerfile Layers
Docker uses layer caching for faster builds. Optimize by:
Order Instructions Strategically
# Bad: Copies all files before installing dependencies
COPY . .
RUN pip install -r requirements.txt
# Good: Install dependencies first (cached until requirements change)
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
Combine RUN Commands
# Bad: Creates multiple layers
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get clean
# Good: Single layer, with cleanup
RUN apt-get update && \
apt-get install -y --no-install-recommends curl && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
Multi-Stage Builds
Multi-stage builds create lean production images by separating build and runtime:
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM node:20-alpine AS production
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/index.js"]
The final image contains only what's needed to run, excluding build tools and source code.
Security Best Practices
Never Run as Root
# Create non-root user
RUN addgroup -g 1001 appgroup && \
adduser -u 1001 -G appgroup -D appuser
USER appuser
Scan Images for Vulnerabilities
Integrate security scanning into your CI pipeline using tools like Trivy, Snyk, or Docker Scout.
Don't Store Secrets in Images
Never include passwords, API keys, or certificates in your Dockerfile. Use:
- Environment variables at runtime
- Docker secrets for Swarm
- Kubernetes secrets
- External secret management (HashiCorp Vault)
Use .dockerignore
Exclude unnecessary files from the build context:
.git
.gitignore
node_modules
*.md
.env
tests/
Health Checks
Define health checks so orchestrators know when your container is ready:
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
Logging Best Practices
Containerized applications should log to stdout/stderr, not files:
- Enables log aggregation by orchestration platforms
- Avoids filling container storage
- Simplifies log rotation and management
Resource Limits
Always set resource limits in production to prevent runaway containers:
docker run -d --memory=512m --cpus=1 myapp:latest
Image Tagging Strategy
Implement a consistent tagging strategy:
myapp:1.2.3- Semantic versioningmyapp:1.2.3-abc123- Version + git commitmyapp:latest- Current stable (use cautiously)myapp:dev- Development builds
Conclusion
Building production-ready containers requires attention to detail in security, efficiency, and maintainability. These practices will serve you well whether you're deploying a single container or managing thousands across a Kubernetes cluster.
Ready to master Docker and containerization? Explore our comprehensive Docker eBooks for hands-on exercises and advanced techniques.