Docker in Production Environment

Module 28: DevOps & Deployment - Tuesday, Lecture 1

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.

flowchart LR A[Development Docker] --> B[Production Docker] A --> A1[Focus: Developer Experience] A --> A2[Debug Tools Included] A --> A3[Development Dependencies] A --> A4[Verbose Logging] A --> A5[Fast Iteration] B --> B1[Focus: Reliability & Security] B --> B2[Minimal Attack Surface] B --> B3[Production Dependencies Only] B --> B4[Optimized Performance] B --> B5[Resource Efficiency]

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.

graph TD A[Docker Host] --> B[Container 1: Web] A --> C[Container 2: API] A --> D[Container 3: Database] A --> E[Container 4: Cache] F[Load Balancer] --> A B -- "Volume" --> G[Persistent Storage] D -- "Volume" --> G

Pros:

Cons:

Clustered Deployment

For production applications at scale, container orchestration platforms like Kubernetes or Docker Swarm manage containers across multiple hosts.

graph TD A[Load Balancer] --> B[Node 1] A --> C[Node 2] A --> D[Node 3] B --> B1[Container: Web] B --> B2[Container: API] C --> C1[Container: Web] C --> C2[Container: API] D --> D1[Container: Web] D --> D2[Container: API] B -.-> E[Shared Storage] C -.-> E D -.-> E F[Database Cluster] --- B F --- C F --- D

Pros:

Cons:

Hybrid Architecture: Stateless vs. Stateful

A common pattern separates stateless and stateful components:

graph TD A[Load Balancer] --> B[Container Cluster] B --> B1[Stateless Service 1] B --> B2[Stateless Service 2] B --> B3[Stateless Service 3] B1 & B2 & B3 --> C[Managed Database Service] B1 & B2 & B3 --> D[Managed Cache Service] B1 & B2 & B3 --> E[Managed Message Queue] F[Object Storage] --- B1 & B2 & B3

This architecture leverages containers for stateless services (web servers, APIs) while using managed services for stateful components (databases, caches, message queues).

Benefits:

Production Docker Best Practices

Container Security

# 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

# 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

graph LR A[Containers] --logs--> B[Log Agent] A --metrics--> C[Metrics Agent] B --> D[Log Storage] C --> E[Time Series DB] D --> F[Log Analysis] E --> G[Metrics Dashboards] F & G --> H[Alerting System]

Container Lifecycle Management

// 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:

Layer Caching Strategy

Docker builds images layer by layer. Optimize your Dockerfile to leverage this for faster builds:

  1. Order layers from least likely to most likely to change
  2. Place dependencies before application code
  3. Separate code that changes frequently into its own layers
graph TD A[Base Image Layer] --> B[System Dependencies Layer] B --> C[Application Dependencies Layer] C --> D[Application Code Layer] D --> E[Configuration Layer] style A fill:#d4f1f9 style B fill:#d4f1f9 style C fill:#ffe6cc style D fill:#ffcccc style E fill:#ffcccc classDef rarely fill:#d4f1f9; classDef sometimes fill:#ffe6cc; classDef frequently fill:#ffcccc;

Explanation:

Image Versioning and Tagging

Proper tagging strategies ensure traceability and deployment reliability:

# 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:

graph TD subgraph Frontend Network A[Web Container] B[Frontend API Container] end subgraph Backend Network C[Backend API Container] D[Worker Container] end subgraph Data Network E[Database Container] F[Cache Container] end A --> B B --> C C --> D C --> E C --> F D --> E D --> F

Persistence and Data Management

Data persistence is critical for production applications:

flowchart LR A[Container] -- "Ephemeral" --> B[Container Filesystem] A -- "Persistent" --> C[Volume] A -- "Persistent" --> D[Bind Mount] A -- "Persistent" --> E[tmpfs] C --> F[Docker Managed Volume] C --> G[Host Path Volume] C --> H[Network Volume] H --> I[NFS] H --> J[Cloud Storage] H --> K[SAN/NAS]

Best practices for data persistence:

# 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:

graph TD A[CDN] --> B[Load Balancer] B --> C[Web Containers] C --> D[API Gateway] D --> E[Product Service] D --> F[Cart Service] D --> G[User Service] D --> H[Order Service] E & F & G & H --> I[Message Queue] I --> J[Payment Processing] I --> K[Inventory Management] I --> L[Shipping Service] E --> M[Product Database] F --> N[Cart Database] G --> O[User Database] H --> P[Order Database] subgraph "Frontend Tier" A B C end subgraph "API Tier" D E F G H end subgraph "Worker Tier" I J K L end subgraph "Data Tier" M N O P end

Implementation Details

Lessons Learned

Key takeaways from this implementation:

Security Considerations for Production Docker

Container Security Layers

Securing containerized applications requires a multi-layered approach:

graph TD A[Container Security] --> B[Image Security] A --> C[Runtime Security] A --> D[Host Security] A --> E[Network Security] A --> F[Data Security] B --> B1[Trusted base images] B --> B2[Vulnerability scanning] B --> B3[Signed images] C --> C1[Non-root users] C --> C2[Resource limits] C --> C3[Read-only filesystems] C --> C4[Security profiles] D --> D1[Host hardening] D --> D2[Security updates] D --> D3[Minimal access] E --> E1[Network segmentation] E --> E2[Encrypted communication] E --> E3[Network policies] F --> F1[Encryption at rest] F --> F2[Secure secrets management] F --> F3[Access controls]

Security Best Practices Checklist

  1. Image Security
    • Use minimal base images (Alpine, distroless)
    • Scan images for vulnerabilities
    • Implement image signing and verification
    • Never embed secrets in images
  2. 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
  3. Host and Infrastructure Security
    • Keep host systems updated
    • Implement least privilege access
    • Use dedicated hosts for containers
    • Secure the Docker daemon
  4. Network Security
    • Implement network segmentation
    • Use TLS for container communication
    • Apply network policies
    • Restrict exposed ports
  5. 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:

  1. Uses a minimal base image
  2. Runs the application as a non-root user
  3. Implements proper security practices
  4. Sets resource limits
  5. Configures health checks
  6. Separates environment-specific configuration
  7. 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:

  1. Implement a production-grade logging configuration
  2. Set up a monitoring solution (e.g., Prometheus and Grafana)
  3. Configure backup and restore procedures for persistent data
  4. Implement network segmentation for the application components
  5. 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:

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.

Additional Resources