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

Development vs Production Server

flask run / app.run()Gunicorn
PurposeLocal devProduction WSGI
Auto reloadYesNo (use deploy pipeline)
ConcurrencySingle threadMultiple workers
SecurityDebugger riskNo debugger

Warning

Never Expose flask run to the Internet

Use Gunicorn (Linux/macOS) or Waitress (Windows) behind a reverse proxy.

Install Gunicorn

bash
pip install gunicorn

Add to requirements.txt.

Run Gunicorn

From project root with wsgi.py:

python
from app import create_app
 
app = create_app("production")

Start:

bash
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:app

Factory callable form:

bash
gunicorn -w 4 -b 127.0.0.1:8000 'app:create_app()'

Code explanation:

  • -w 4 — four worker processes (often 2 × CPU + 1 as starting point)
  • -b 127.0.0.1:8000 — listen locally; Nginx faces the public

Bind Unix socket (optional):

bash
gunicorn -w 4 --bind unix:/run/flask-app.sock wsgi:app

Worker Tuning (Brief)

OptionMeaning
-wWorker count
--timeout 120Kill 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

python
@main_bp.route("/health")
def health():
    return {"status": "ok"}, 200

Load balancers probe GET /health—no auth, no heavy DB unless you need deep checks.

Environment Variables (12-Factor)

Never bake secrets into code:

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

bash
sudo systemctl daemon-reload
sudo systemctl enable --now flask-app
sudo systemctl status flask-app

Run migrations before first start:

bash
flask db upgrade

Nginx 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

ApproachWhen
Nginx aliasProduction default—fast
Flask static/Dev only
WhiteNoiseSimple 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:

bash
journalctl -u flask-app -f

Application logs via app.logger to rotating files—see error handling and logging.

Windows Alternative: Waitress

bash
pip install waitress
waitress-serve --port=8000 wsgi:app

Still put IIS or Nginx in front for production TLS.

Deploy Checklist

  • DEBUG=False
  • Secrets in environment
  • flask db upgrade applied
  • Gunicorn bound to localhost
  • Nginx proxy headers set
  • HTTPS enabled
  • /health responds 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.