Gunicorn Configuration

Difficulty: Advanced

Gunicorn (Green Unicorn) is the most popular WSGI HTTP server for running Flask applications in production. Flask's built-in development server is single-threaded, not optimized for concurrent requests, and has the debug mode security vulnerability. Gunicorn provides multiple worker processes, proper signal handling, and production-grade performance.

Gunicorn uses a pre-fork worker model. The master process manages worker processes, and each worker handles requests independently. If a worker crashes, the master restarts it. This provides fault tolerance and process isolation.

The number of workers determines how many concurrent requests your application can handle. The common formula is workers = 2 * CPU_cores + 1. More workers handle more concurrent requests but use more memory. Each worker is an independent process with its own memory space.

Gunicorn supports different worker types. The default sync worker handles one request at a time per worker. The gthread worker uses threads within each worker for concurrent request handling. The gevent and eventlet workers use green threads for high concurrency with low memory. Use gthread for I/O-bound apps and the default sync for CPU-bound apps.

The bind option sets the address and port Gunicorn listens on. For direct access, use 0.0.0.0:8000. Behind a reverse proxy (Nginx, Caddy), bind to a Unix socket (unix:/tmp/gunicorn.sock) or localhost (127.0.0.1:8000) for security.

Gunicorn's timeout setting kills workers that do not respond within the specified seconds. The default is 30 seconds. Increase it for long-running requests. The graceful_timeout is how long to wait for workers to finish existing requests during a restart.

In production, Gunicorn typically runs behind a reverse proxy like Nginx. Nginx handles static files, SSL termination, request buffering, and load balancing. Gunicorn handles only the Python application. This separation improves security and performance.

Code examples

Basic Gunicorn Commands

# Install Gunicorn
# pip install gunicorn

# Run with default settings (1 worker)
# gunicorn app:app

# Run with factory pattern
# gunicorn 'app:create_app()'

# Specify workers and bind address
# gunicorn -w 4 -b 0.0.0.0:8000 app:app

# With threads
# gunicorn -w 4 --threads 2 -b 0.0.0.0:8000 app:app

# With Unix socket (behind Nginx)
# gunicorn -w 4 -b unix:/tmp/gunicorn.sock app:app

# With timeout and graceful timeout
# gunicorn -w 4 --timeout 120 --graceful-timeout 30 app:app

# With access logging
# gunicorn -w 4 --access-logfile access.log --error-logfile error.log app:app

Gunicorn takes the module:app format. Use -w for workers (2 * CPUs + 1). Use -b for binding. For the factory pattern, wrap the call in quotes. Unix sockets are preferred behind a reverse proxy.

Gunicorn Configuration File

# gunicorn.conf.py
import multiprocessing

# Server socket
bind = '0.0.0.0:8000'
backlog = 2048

# Workers
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = 'sync'  # or 'gthread', 'gevent'
threads = 1
worker_connections = 1000
timeout = 120
keepalive = 5

# Restart workers after this many requests (prevent memory leaks)
max_requests = 1000
max_requests_jitter = 50

# Logging
accesslog = '-'  # stdout
errorlog = '-'   # stderr
loglevel = 'info'
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'

# Process naming
proc_name = 'my_flask_app'

# Run with: gunicorn -c gunicorn.conf.py app:app

A config file keeps Gunicorn settings organized. workers scales with CPU count. max_requests restarts workers periodically to prevent memory leaks. Logging to stdout is standard for Docker.

Nginx as Reverse Proxy

# /etc/nginx/sites-available/myapp
server {
    listen 80;
    server_name example.com;

    # Redirect HTTP to HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl;
    server_name example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # Serve static files directly
    location /static/ {
        alias /var/www/myapp/static/;
        expires 30d;
    }

    # Proxy to Gunicorn
    location / {
        proxy_pass http://unix:/tmp/gunicorn.sock;
        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;
    }
}

Nginx handles SSL, static files, and proxies dynamic requests to Gunicorn. proxy_set_header forwards the real client IP and protocol. Static files are served directly by Nginx (much faster than Flask).

ProxyFix Middleware

from flask import Flask
from werkzeug.middleware.proxy_fix import ProxyFix

def create_app():
    app = Flask(__name__)

    # Tell Flask it's behind a reverse proxy
    app.wsgi_app = ProxyFix(
        app.wsgi_app,
        x_for=1,        # Trust X-Forwarded-For
        x_proto=1,      # Trust X-Forwarded-Proto
        x_host=1,       # Trust X-Forwarded-Host
        x_prefix=1      # Trust X-Forwarded-Prefix
    )

    @app.route('/')
    def index():
        # Now request.remote_addr gives the real client IP
        # request.is_secure is True for HTTPS
        # url_for generates HTTPS URLs
        return 'OK'

    return app

Behind a reverse proxy, Flask sees the proxy's IP, not the client's. ProxyFix reads the X-Forwarded-* headers to restore the real client information. Set x_for=1 for one proxy hop.

Key points

Concepts covered

Gunicorn, Workers, Threads, Binding, Production Server, Reverse Proxy