Deploying to production

Flask · Advanced · 14 min

The dev server is not built for real traffic; production needs the right server, proxy setup and settings.

In production, a WSGI server runs your app with multiple workers:

gunicorn -w 4 -b 0.0.0.0:8000 "myapp:create_app()"

Gunicorn runs on Unix; Waitress is a pure-Python option that also works on Windows. Gunicorn suggests (2 x CPU cores) + 1 sync workers as a starting point; add --threads for I/O-bound apps, or use gevent workers for many slow connections.

Put a reverse proxy (Nginx, Caddy or a cloud load balancer) in front. It terminates TLS, buffers slow clients, serves /static directly and forwards the rest to Gunicorn.

Behind a proxy, Flask sees the proxy's address and plain HTTP unless you trust the X-Forwarded-* headers. Wrap the app with ProxyFix, configured with how many proxies set each header:

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

Only do this when a proxy really sets them; otherwise clients can spoof their IP and scheme.

Checklist:

  • debug off; SECRET_KEY and credentials from the environment
  • secure cookie flags and MAX_CONTENT_LENGTH
  • logging to stdout/stderr via app.logger and the server's access log, collected by your platform
  • database migrations run as a separate deploy step, not in create_app
  • a health-check route the load balancer can poll
  • containerize with a small image and run as a non-root user

Platforms like Render, Fly.io, Heroku-style buildpacks or a Docker container on any cloud all follow this same Gunicorn-plus-proxy shape.

Example

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

def create_app():
    app = Flask(__name__)
    app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1)

    @app.get("/healthz")
    def health():
        return {"status": "ok"}

    @app.get("/whoami")
    def whoami():
        return {"ip": request.remote_addr, "scheme": request.scheme, "host": request.host}

    return app

# Production: gunicorn -w 4 -b 0.0.0.0:8000 "myapp:create_app()"
app = create_app()
r = app.test_client().get("/whoami", headers={
    "X-Forwarded-For": "203.0.113.9", "X-Forwarded-Proto": "https", "X-Forwarded-Host": "example.com"})
print(r.json)   # {'host': 'example.com', 'ip': '203.0.113.9', 'scheme': 'https'}

Remember

  • Use Gunicorn or Waitress, never flask run, in production
  • A reverse proxy handles TLS and static files
  • ProxyFix with the right counts restores client IP and scheme behind a proxy

Watch out for

  • Enabling ProxyFix without a proxy, letting clients spoof X-Forwarded-For.

Check yourself

  1. Behind Nginx, url_for(..., _external=True) produces http:// links although users use HTTPS. What fixes it?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: flask.palletsprojects.com/en/stable/deploying/