Deploying With Gunicorn and Nginx
Introduction
flask run uses Werkzeug’s development server—it is not built for production traffic. Gunicorn runs multiple worker processes that serve your WSGI app; Nginx terminates HTTPS, serves static files, and reverse-proxies to Gunicorn. This chapter deploys a Flask factory app on Linux following 12-factor config practices.
Prerequisites
- Blueprints and Application Factory
- Flask Security Best Practices
- Linux server access—see Linux track
Development vs Production Server
flask run / app.run() | Gunicorn | |
|---|---|---|
| Purpose | Local dev | Production WSGI |
| Auto reload | Yes | No (use deploy pipeline) |
| Concurrency | Single thread | Multiple workers |
| Security | Debugger risk | No debugger |
Warning
Never Expose flask run to the Internet
Use Gunicorn (Linux/macOS) or Waitress (Windows) behind a reverse proxy.
Install Gunicorn
pip install gunicornAdd to requirements.txt.
Run Gunicorn
From project root with wsgi.py:
from app import create_app
app = create_app("production")Start:
export FLASK_ENV=production
export SECRET_KEY="your-production-secret"
export DATABASE_URL="mysql+pymysql://user:pass@localhost/app?charset=utf8mb4"
gunicorn -w 4 -b 127.0.0.1:8000 wsgi:appFactory callable form:
gunicorn -w 4 -b 127.0.0.1:8000 'app:create_app()'Code explanation:
-w 4— four worker processes (often2 × CPU + 1as starting point)-b 127.0.0.1:8000— listen locally; Nginx faces the public
Bind Unix socket (optional):
gunicorn -w 4 --bind unix:/run/flask-app.sock wsgi:appWorker Tuning (Brief)
| Option | Meaning |
|---|---|
-w | Worker count |
--timeout 120 | Kill slow workers (seconds) |
--access-logfile - | Access log to stdout |
--error-logfile - | Error log to stdout |
For long uploads, align timeout with Nginx proxy_read_timeout.
Health Check Route
@main_bp.route("/health")
def health():
return {"status": "ok"}, 200Load balancers probe GET /health—no auth, no heavy DB unless you need deep checks.
Environment Variables (12-Factor)
Never bake secrets into code:
export SECRET_KEY="..."
export DATABASE_URL="..."
export FLASK_ENV="production"Systemd EnvironmentFile=/etc/flask-app/env or Docker env—see next chapter.
systemd Service
/etc/systemd/system/flask-app.service:
Enable:
sudo systemctl daemon-reload
sudo systemctl enable --now flask-app
sudo systemctl status flask-appRun migrations before first start:
flask db upgradeNginx Reverse Proxy
/etc/nginx/sites-available/flask-app:
Enable site and reload Nginx—details in Nginx reverse proxy basics and proxying Spring Boot and Node (same proxy_pass pattern).
HTTPS: add listen 443 ssl with certbot or manual certs—see HTTPS and Let's Encrypt.
Static Files Strategy
| Approach | When |
|---|---|
Nginx alias | Production default—fast |
Flask static/ | Dev only |
| WhiteNoise | Simple PaaS deploy without Nginx for static |
User uploads stay out of static/—serve via app or object storage.
Logging
Gunicorn logs to stdout; systemd journal:
journalctl -u flask-app -fApplication logs via app.logger to rotating files—see error handling and logging.
Windows Alternative: Waitress
pip install waitress
waitress-serve --port=8000 wsgi:appStill put IIS or Nginx in front for production TLS.
Deploy Checklist
-
DEBUG=False - Secrets in environment
-
flask db upgradeapplied - Gunicorn bound to localhost
- Nginx proxy headers set
- HTTPS enabled
-
/healthresponds 200
FAQ
502 Bad Gateway?
Gunicorn not running, wrong port, or socket permissions—check systemctl status.
Static 404 but app works?
Fix Nginx alias path or collect static to known directory.
Workers killed (WORKER TIMEOUT)?
Increase --timeout or optimize slow view; fix Nginx read timeout.
How many workers?
Start with 2 × CPU cores + 1; load test and adjust.
Run migrations on deploy?
Yes—automate flask db upgrade before restarting Gunicorn.
Multiple apps on one server?
Separate systemd units, ports, and Nginx server_name blocks.