Skip to main content
โšก Calmops

Deploying Web Applications with Docker, Gunicorn, and Nginx: A Production-Ready Guide

Deploying Web Applications with Docker, Gunicorn, and Nginx: A Production-Ready Guide

Deploying web applications to production requires more than just running your code on a server. You need proper isolation, scalability, and a robust architecture that can handle real-world traffic. The combination of Docker, Gunicorn, and Nginx has become the industry standard for deploying Python web applications, and for good reason.

In this comprehensive guide, we’ll explore how these three technologies work together to create a production-ready deployment stack, and I’ll walk you through implementing it step by step.


Understanding the Stack

The Three Components

Docker: Containerization platform that packages your application with all its dependencies into a portable, isolated container.

Gunicorn: WSGI HTTP server that runs your Python web application (Flask, Django, etc.) and handles multiple concurrent requests.

Nginx: High-performance reverse proxy and web server that sits in front of Gunicorn, handling SSL/TLS, load balancing, and static file serving.

How They Work Together

Client Request
    โ†“
Nginx (Port 80/443)
    โ†“
Gunicorn (Port 8000)
    โ†“
Python Application
    โ†“
Database/External Services
  1. Client makes a request to your domain
  2. Nginx receives the request on port 80/443
  3. Nginx forwards the request to Gunicorn on port 8000
  4. Gunicorn runs your Python application
  5. Application processes the request and returns a response
  6. Nginx sends the response back to the client

Why This Stack?

  • Isolation: Docker ensures your app runs the same everywhere
  • Performance: Gunicorn efficiently handles concurrent requests
  • Scalability: Nginx can load balance across multiple Gunicorn instances
  • Security: Nginx provides SSL/TLS termination and acts as a security layer
  • Simplicity: Each component has a single responsibility

Part 1: Creating a Docker Container

Step 1: Create a Sample Flask Application

# app.py
from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/')
def hello():
    return jsonify({'message': 'Hello from Flask!'})

@app.route('/health')
def health():
    return jsonify({'status': 'healthy'})

if __name__ == '__main__':
    app.run()

Step 2: Create a Requirements File

# requirements.txt
Flask==2.3.0
Gunicorn==20.1.0

Step 3: Write a Dockerfile

# Dockerfile
FROM python:3.11-slim

# Set working directory
WORKDIR /app

# Install system dependencies
RUN apt-get update && apt-get install -y \
    gcc \
    && rm -rf /var/lib/apt/lists/*

# Copy requirements and install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy application code
COPY app.py .

# Expose port (for documentation, Gunicorn will listen on this)
EXPOSE 8000

# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
    CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"

# Run Gunicorn
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "4", "--worker-class", "sync", "--timeout", "60", "app:app"]

Dockerfile Explanation:

  • FROM python:3.11-slim: Use lightweight Python image
  • WORKDIR /app: Set working directory inside container
  • RUN apt-get update: Install system dependencies
  • COPY requirements.txt: Copy dependencies file
  • RUN pip install: Install Python packages
  • COPY app.py: Copy application code
  • EXPOSE 8000: Document that app listens on port 8000
  • HEALTHCHECK: Define health check for container orchestration
  • CMD: Command to run when container starts

Step 4: Build and Test the Docker Image

# Build the image
docker build -t my-flask-app:1.0 .

# Run the container
docker run -p 8000:8000 my-flask-app:1.0

# Test the application
curl http://localhost:8000/

Part 2: Configuring Nginx

Nginx Configuration

# nginx.conf
upstream gunicorn {
    # Define the Gunicorn server(s)
    server gunicorn:8000;
    # For load balancing across multiple instances:
    # server gunicorn1:8000;
    # server gunicorn2:8000;
    # server gunicorn3:8000;
}

server {
    listen 80;
    server_name _;
    client_max_body_size 20M;

    # Logging
    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "no-referrer-when-downgrade" always;

    # Serve static files directly (if applicable)
    location /static/ {
        alias /app/static/;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    # Proxy requests to Gunicorn
    location / {
        proxy_pass http://gunicorn;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        
        # Timeouts
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
        
        # Buffering
        proxy_buffering on;
        proxy_buffer_size 4k;
        proxy_buffers 8 4k;
    }

    # Health check endpoint
    location /health {
        proxy_pass http://gunicorn;
        access_log off;
    }
}

Key Nginx Directives:

  • upstream: Defines backend servers
  • proxy_pass: Forwards requests to Gunicorn
  • proxy_set_header: Passes important headers to backend
  • add_header: Adds security headers to responses
  • location: Defines URL patterns and their handling

Dockerfile for Nginx

# Dockerfile.nginx
FROM nginx:alpine

# Copy custom Nginx configuration
COPY nginx.conf /etc/nginx/conf.d/default.conf

# Expose port
EXPOSE 80

# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
    CMD wget --quiet --tries=1 --spider http://localhost/health || exit 1

# Run Nginx
CMD ["nginx", "-g", "daemon off;"]

Part 3: Docker Compose Orchestration

Docker Compose Configuration

# docker-compose.yml
version: '3.8'

services:
  # Gunicorn application server
  gunicorn:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: my-app-gunicorn
    environment:
      - FLASK_ENV=production
      - PYTHONUNBUFFERED=1
    volumes:
      - ./app.py:/app/app.py  # For development; remove in production
    expose:
      - 8000
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 5s
    networks:
      - app-network
    # Resource limits
    deploy:
      resources:
        limits:
          cpus: '1'
          memory: 512M
        reservations:
          cpus: '0.5'
          memory: 256M

  # Nginx reverse proxy
  nginx:
    build:
      context: .
      dockerfile: Dockerfile.nginx
    container_name: my-app-nginx
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - ./ssl:/etc/nginx/ssl:ro  # For SSL certificates
    depends_on:
      - gunicorn
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 5s
    networks:
      - app-network

networks:
  app-network:
    driver: bridge

Running with Docker Compose

# Start all services
docker-compose up -d

# View logs
docker-compose logs -f

# Stop services
docker-compose down

# Rebuild images
docker-compose up -d --build

# Scale Gunicorn instances
docker-compose up -d --scale gunicorn=3

Part 4: Production Considerations

SSL/TLS Configuration

# nginx.conf with SSL
server {
    listen 80;
    server_name example.com;
    
    # Redirect HTTP to HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name example.com;
    
    # SSL certificates
    ssl_certificate /etc/nginx/ssl/cert.pem;
    ssl_certificate_key /etc/nginx/ssl/key.pem;
    
    # SSL configuration
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;
    
    # HSTS header
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    
    # Rest of configuration...
}

Environment Variables

# docker-compose.yml with environment variables
services:
  gunicorn:
    environment:
      - FLASK_ENV=production
      - DATABASE_URL=postgresql://user:pass@db:5432/myapp
      - SECRET_KEY=${SECRET_KEY}
      - DEBUG=False

Logging and Monitoring

# Dockerfile with logging
FROM python:3.11-slim

WORKDIR /app

# Install logging dependencies
RUN pip install python-json-logger

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app.py .

# Configure logging
ENV PYTHONUNBUFFERED=1

CMD ["gunicorn", \
     "--bind", "0.0.0.0:8000", \
     "--workers", "4", \
     "--access-logfile", "-", \
     "--error-logfile", "-", \
     "--log-level", "info", \
     "app:app"]

Resource Limits and Optimization

# docker-compose.yml with optimization
services:
  gunicorn:
    deploy:
      resources:
        limits:
          cpus: '2'
          memory: 1G
        reservations:
          cpus: '1'
          memory: 512M
    environment:
      - GUNICORN_WORKERS=4
      - GUNICORN_THREADS=2
      - GUNICORN_WORKER_CLASS=gthread

Best Practices

1. Use Multi-Stage Builds

# Multi-stage Dockerfile for smaller images
FROM python:3.11 as builder

WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

FROM python:3.11-slim

WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY app.py .

ENV PATH=/root/.local/bin:$PATH

CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]

2. Security Best Practices

# Run as non-root user
FROM python:3.11-slim

RUN useradd -m -u 1000 appuser

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app.py .
RUN chown -R appuser:appuser /app

USER appuser

CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]

3. Health Checks

# docker-compose.yml with comprehensive health checks
services:
  gunicorn:
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 10s

4. Graceful Shutdown

# Handle signals properly
FROM python:3.11-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app.py .

# Use exec form to ensure signals are passed to Gunicorn
CMD ["gunicorn", \
     "--bind", "0.0.0.0:8000", \
     "--workers", "4", \
     "--timeout", "60", \
     "--graceful-timeout", "30", \
     "app:app"]

Common Pitfalls and Troubleshooting

Pitfall 1: Gunicorn Not Responding

# Check if Gunicorn is running
docker-compose ps

# View Gunicorn logs
docker-compose logs gunicorn

# Test connection
docker-compose exec nginx curl http://gunicorn:8000/

Pitfall 2: Nginx 502 Bad Gateway

Causes:

  • Gunicorn not running
  • Gunicorn crashed
  • Network connectivity issue

Solution:

# Check Gunicorn health
docker-compose exec gunicorn curl http://localhost:8000/health

# Restart services
docker-compose restart gunicorn nginx

# Check logs
docker-compose logs --tail=50

Pitfall 3: Static Files Not Serving

# Ensure static files are configured correctly
location /static/ {
    alias /app/static/;
    expires 30d;
}

Pitfall 4: High Memory Usage

# Limit workers and threads
CMD ["gunicorn", \
     "--bind", "0.0.0.0:8000", \
     "--workers", "2", \
     "--worker-class", "sync", \
     "--max-requests", "1000", \
     "--max-requests-jitter", "100", \
     "app:app"]

Deployment Checklist

  • Dockerfile builds successfully
  • Application runs in container
  • Nginx configuration is valid
  • Docker Compose file is correct
  • Health checks are configured
  • SSL/TLS certificates are in place
  • Environment variables are set
  • Resource limits are defined
  • Logging is configured
  • Security headers are added
  • Non-root user is used
  • Graceful shutdown is implemented
  • Monitoring and alerting are set up

Conclusion

The Docker, Gunicorn, and Nginx stack provides a robust, scalable foundation for deploying Python web applications. By understanding how each component works and following best practices, you can build production-ready deployments that are secure, performant, and maintainable.

Key Takeaways

  • Docker provides isolation and consistency across environments
  • Gunicorn efficiently runs your Python application with multiple workers
  • Nginx acts as a reverse proxy, handling SSL/TLS and load balancing
  • Docker Compose orchestrates multiple containers as a single application
  • Health checks ensure your services are running correctly
  • Security should be built in from the start (SSL/TLS, security headers, non-root user)
  • Monitoring and logging are essential for production systems
  • Resource limits prevent runaway containers from consuming all system resources

Start with this foundation, monitor your application in production, and iterate based on real-world performance and requirements. Happy deploying!

Comments