Deployment

The development server started by flask run is single-process and unhardened — it is for development only. Production requires a proper WSGI server behind a reverse proxy.

Architecture Overview

Browser → Nginx (TLS, static files, rate limiting) → Gunicorn (multiple workers) → Flask app

Choosing Components

  • Gunicorn (first choice on Linux): mature and stable; -w multi-process plus optional gevent/gthread concurrency models
  • uWSGI: feature-rich, more complex configuration
  • Waitress: pure Python, cross-platform — the main option on Windows
  • Reverse proxy: Nginx / Caddy (automatic HTTPS)

Gunicorn

pip install gunicorn

# 4 worker processes bound to local port 8000
gunicorn -w 4 -b 127.0.0.1:8000 "app:create_app()"

# IO-heavy apps can switch to the gthread model
gunicorn -w 4 --threads 8 -k gthread -b 127.0.0.1:8000 "app:create_app()"

A good starting worker count is CPU cores × 2 + 1. Bind to 127.0.0.1 rather than 0.0.0.0 so only the local Nginx can reach it.

Nginx Reverse Proxy

server {
    listen 443 ssl;
    server_name example.com;

    location /static/ {
        alias /srv/myapp/app/static/;
        expires 30d;
    }

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Behind a proxy, Flask must trust the forwarded headers — otherwise url_for(..., _external=True) generates http links:

from werkzeug.middleware.proxy_fix import ProxyFix

app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1)

systemd Service

# /etc/systemd/system/myapp.service
[Unit]
Description=My Flask App
After=network.target

[Service]
User=www-data
WorkingDirectory=/srv/myapp
Environment="SECRET_KEY=..." "DATABASE_URL=..."
ExecStart=/srv/myapp/.venv/bin/gunicorn -w 4 -b 127.0.0.1:8000 "app:create_app()"
Restart=always

[Install]
WantedBy=multi-user.target
sudo systemctl enable --now myapp

Containerization

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8000", "app:create_app()"]

Copying requirements.txt first and installing dependencies separately makes good use of Docker layer caching.

Production Checklist

  • DEBUG=False; SECRET_KEY comes from an environment variable and is sufficiently random
  • Database URLs and third-party secrets all injected via environment variables
  • ProxyFix configured and forwarding headers set in Nginx
  • Static files served by Nginx/CDN
  • Logs to stdout (containers) or rotating files (see the Logging chapter)
  • A health-check endpoint (e.g. GET /healthz returning 200)
  • HTTPS enforced (HSTS); session cookies set Secure