Introduction to Production Docker
In development environments, Docker helps us create consistent, reproducible environments for writing and testing code. But when we transition to production, our priorities shift from developer convenience to reliability, security, performance, and scalability.
Think of the difference as similar to a home kitchen versus a professional restaurant kitchen. While both are for cooking, the professional kitchen is designed for efficiency, consistency, and volume—with strict standards for food safety, quality control, and workflow optimization.
In this lecture, we'll explore key considerations for running Docker in production environments, including architecture patterns, security best practices, monitoring, and resource optimization.
Production Docker Architecture Patterns
Single-Host Deployment
The simplest production setup runs containers on a single host using Docker Compose.
Pros:
- Simple to set up and manage
- Lower operational complexity
- Suitable for small applications or MVPs
Cons:
- Single point of failure
- Limited scalability
- Resource constraints
Clustered Deployment
For production applications at scale, container orchestration platforms like Kubernetes or Docker Swarm manage containers across multiple hosts.
Pros:
- High availability through redundancy
- Horizontal scalability
- Resilience against node failures
- Better resource utilization
Cons:
- Increased operational complexity
- Networking challenges
- Requires orchestration knowledge
Hybrid Architecture: Stateless vs. Stateful
A common pattern separates stateless and stateful components:
This architecture leverages containers for stateless services (web servers, APIs) while using managed services for stateful components (databases, caches, message queues).
Benefits:
- Simpler container lifecycle management
- Built-in redundancy for stateful services
- Reduced operational burden
- Easier scaling for stateless components
Production Docker Best Practices
Container Security
- Use minimal base images: Alpine or distroless images reduce attack surface
- Run as non-root: Create and use non-privileged users in your containers
- Read-only file systems: Mount specific directories as writable only when needed
- Scan for vulnerabilities: Regularly scan images for security issues
- Use secrets management: Never bake secrets into images
# Example: Running as non-root user
FROM node:18-alpine
# Create app directory
WORKDIR /app
# Create non-root user
RUN addgroup -g 1001 appuser && \
adduser -u 1001 -G appuser -s /bin/sh -D appuser
# Copy application code
COPY --chown=appuser:appuser . .
# Install dependencies
RUN npm ci --production
# Switch to non-root user
USER appuser
# Run the application
CMD ["node", "server.js"]
Resource Management
- Set resource limits: Define CPU and memory constraints for containers
- Monitor resource usage: Implement tools to track container resource consumption
- Handle out-of-memory gracefully: Design your applications to fail gracefully
- Implement health checks: Enable automatic restart of unhealthy containers
# Example: Resource limits in docker-compose.yml
services:
api:
image: api-service:1.0.0
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
Logging and Monitoring
- Centralized logging: Forward logs to a collection system
- Structure your logs: Use JSON or other parseable formats
- Include correlation IDs: Trace requests across services
- Monitor container metrics: Track CPU, memory, network, and disk usage
- Application performance monitoring: Track response times, error rates, etc.
Container Lifecycle Management
- Implement graceful shutdowns: Handle SIGTERM properly
- Use rolling updates: Update containers without downtime
- Version your images: Tag images with meaningful, traceable identifiers
- Implement blue-green or canary deployments: Reduce deployment risk
// Example: Node.js graceful shutdown
const server = app.listen(port, () => {
console.log(`Server listening on port ${port}`);
});
// Handle graceful shutdown
process.on('SIGTERM', () => {
console.log('SIGTERM received, shutting down gracefully');
server.close(() => {
console.log('HTTP server closed');
// Close database connections
mongoose.connection.close(false, () => {
console.log('Database connections closed');
process.exit(0);
});
});
// Force shutdown after timeout
setTimeout(() => {
console.error('Forced shutdown after timeout');
process.exit(1);
}, 30000);
});
Production-Ready Docker Images
Image Size Optimization
Smaller images lead to faster deployments, reduced storage costs, and smaller attack surfaces.
# BEFORE: Large, inefficient image
FROM ubuntu:20.04
RUN apt-get update && apt-get install -y curl python3 python3-pip
COPY . /app
WORKDIR /app
RUN pip3 install -r requirements.txt
CMD ["python3", "app.py"]
# AFTER: Optimized image
FROM python:3.9-slim
WORKDIR /app
# Copy and install dependencies first (better layer caching)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY app.py .
CMD ["python", "app.py"]
Optimization techniques:
- Use smaller base images (Alpine, slim variants)
- Combine RUN commands to reduce layers
- Remove unnecessary files in the same layer they're created
- Use .dockerignore to exclude unnecessary files
- Consider multi-stage builds (covered in Lecture 2)
Layer Caching Strategy
Docker builds images layer by layer. Optimize your Dockerfile to leverage this for faster builds:
- Order layers from least likely to most likely to change
- Place dependencies before application code
- Separate code that changes frequently into its own layers
Explanation:
- Blue layers: Rarely change (base image, system packages)
- Orange layers: Sometimes change (dependencies)
- Red layers: Frequently change (application code, config)
Image Versioning and Tagging
Proper tagging strategies ensure traceability and deployment reliability:
- Never use 'latest': It's ambiguous and leads to deployment inconsistencies
- Include semantic versioning: Major.Minor.Patch (e.g., 1.2.3)
- Consider including build information: Git commit, build number
- Tag for environments: prod, staging, etc.
# Example tagging strategy
# Format: [registry]/[app]:[semantic-version]-[build-info]
# Good examples:
docker tag myapp gcr.io/my-project/api:1.2.3-b42
docker tag myapp registry.example.com/billing-service:2.0.1-8f731a
# Bad examples:
docker tag myapp myapp:latest # Ambiguous
docker tag myapp myapp:new # Not descriptive
docker tag myapp myapp:$(date +%s) # Not meaningful
Networking and Persistence in Production
Container Networking
Production networking requires careful consideration:
- Use overlay networks for multi-host communication
- Implement network segmentation for security
- Define explicit ports rather than using random assignments
- Consider network policies to restrict traffic flow
- Monitor network performance for bottlenecks
Persistence and Data Management
Data persistence is critical for production applications:
Best practices for data persistence:
- Separate data from application: Use volumes for persistent data
- Consider storage drivers: Different drivers have different performance characteristics
- Implement backup strategies: Regular backups of volume data
- Use appropriate volume types: Local for performance, network for reliability
- Monitor storage metrics: Disk usage, I/O operations, latency
# Example: Docker Compose with volume configuration
services:
database:
image: postgres:14
volumes:
# Named volume for database data
- db-data:/var/lib/postgresql/data
# Mount for custom configuration
- ./postgres.conf:/etc/postgresql/postgresql.conf
command: postgres -c 'config_file=/etc/postgresql/postgresql.conf'
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
volumes:
db-data:
driver: local
driver_opts:
type: 'none'
o: 'bind'
device: '/mnt/data/postgres'
secrets:
db_password:
file: ./secrets/db_password.txt
Real-World Production Docker Example
E-Commerce Platform Case Study
Let's look at how an e-commerce company might implement Docker in production:
Implementation Details
- Infrastructure: Kubernetes cluster with multiple worker nodes
- Scaling: Horizontal Pod Autoscaler based on CPU and request rate
- Deployments: Blue-green deployment pattern for zero-downtime updates
- Monitoring: Prometheus for metrics, ELK stack for logs, Grafana for dashboards
- Data Persistence: StatefulSets for databases with persistent volumes
- Security: Network policies, RBAC, encrypted secrets management
- CI/CD: GitLab CI/CD pipeline with automated testing and deployment
Lessons Learned
Key takeaways from this implementation:
- Start simple: Begin with core services before containerizing everything
- Plan for data: Data persistence requires careful planning
- Monitoring is crucial: Invest in observability from day one
- Automate everything: Manual processes lead to errors
- Document thoroughly: Document architecture, decisions, and operations
Security Considerations for Production Docker
Container Security Layers
Securing containerized applications requires a multi-layered approach:
Security Best Practices Checklist
- Image Security
- Use minimal base images (Alpine, distroless)
- Scan images for vulnerabilities
- Implement image signing and verification
- Never embed secrets in images
- Runtime Security
- Run containers as non-root users
- Use read-only filesystems where possible
- Implement resource limits
- Apply security profiles (AppArmor, SELinux)
- Use --no-new-privileges flag
- Host and Infrastructure Security
- Keep host systems updated
- Implement least privilege access
- Use dedicated hosts for containers
- Secure the Docker daemon
- Network Security
- Implement network segmentation
- Use TLS for container communication
- Apply network policies
- Restrict exposed ports
- Secrets Management
- Use a dedicated secrets management solution
- Implement proper access controls
- Rotate secrets regularly
- Monitor for unusual access patterns
Practical Exercise: Converting Development Docker to Production
Exercise Brief
In this exercise, you'll convert a development-focused Docker setup to a production-ready configuration.
Starting Point: Development Docker Configuration
# Development Dockerfile
FROM node:18
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev"]
# Development docker-compose.yml
services:
app:
build: .
ports:
- "3000:3000"
volumes:
- .:/app
- /app/node_modules
environment:
- NODE_ENV=development
- DEBUG=app:*
command: npm run dev
db:
image: postgres:latest
environment:
POSTGRES_USER: devuser
POSTGRES_PASSWORD: devpassword
POSTGRES_DB: devdb
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
Your Task
Convert the development configuration to a production-ready setup that:
- Uses a minimal base image
- Runs the application as a non-root user
- Implements proper security practices
- Sets resource limits
- Configures health checks
- Separates environment-specific configuration
- Uses a proper tagging strategy
Solution Outline
Here's how you might approach the solution:
# Production Dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
# Copy package files and install dependencies
COPY package*.json ./
RUN npm ci
# Copy source code and build application
COPY . .
RUN npm run build
# Production image
FROM node:18-alpine
# Create non-root user
RUN addgroup -g 1001 appuser && \
adduser -u 1001 -G appuser -s /bin/sh -D appuser
# Set working directory and ownership
WORKDIR /app
RUN chown -R appuser:appuser /app
# Copy from builder stage
COPY --from=builder --chown=appuser:appuser /app/package*.json ./
COPY --from=builder --chown=appuser:appuser /app/node_modules ./node_modules
COPY --from=builder --chown=appuser:appuser /app/dist ./dist
# Switch to non-root user
USER appuser
# Set environment variables
ENV NODE_ENV=production
# Expose application port
EXPOSE 3000
# Healthcheck
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
# Run the application
CMD ["node", "dist/server.js"]
# Production docker-compose.yml
services:
app:
image: ${REGISTRY_URL}/myapp:${VERSION}-${BUILD_NUMBER}
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M
restart_policy:
condition: on-failure
max_attempts: 3
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=${DB_URL}
secrets:
- app_secret
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 5s
db:
image: postgres:14-alpine
deploy:
resources:
limits:
cpus: '1.0'
memory: 1G
environment:
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
POSTGRES_DB: ${DB_NAME}
volumes:
- pgdata:/var/lib/postgresql/data
secrets:
- db_password
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
volumes:
pgdata:
driver: local
driver_opts:
type: 'none'
o: 'bind'
device: '/mnt/data/postgres'
secrets:
app_secret:
file: ./secrets/app_secret.txt
db_password:
file: ./secrets/db_password.txt
Challenge Extension
Once you've completed the basic task, extend your solution to:
- Implement a production-grade logging configuration
- Set up a monitoring solution (e.g., Prometheus and Grafana)
- Configure backup and restore procedures for persistent data
- Implement network segmentation for the application components
- Create a CI/CD pipeline configuration for automated deployment
Conclusion
Transitioning Docker from development to production involves much more than just changing a few configuration options. It requires a shift in mindset—from developer convenience to operational excellence.
Key takeaways from this lecture include:
- Production-First Design: Consider production requirements from the start
- Security is Paramount: Implement defense in depth for containerized applications
- Optimize for Reliability: Design for high availability and resilience
- Monitor Everything: Comprehensive observability is essential
- Automate Operations: Reduce human error through automation
In our next lecture, we'll explore multi-stage Docker builds—a powerful technique for creating optimized production images while maintaining a developer-friendly build process.