Flask’s built-in server prints a clear warning: “Do not use it in a production deployment.” The production pattern is Gunicorn running your app as several worker processes, supervised by systemd, behind Nginx. Here’s the complete setup on Ubuntu.

The app

Assume a project at /srv/flaskapp/app with an application object in app.py:

from flask import Flask

app = Flask(__name__)

@app.get("/")
def index():
    return "Hello from Flask"

If you use an application factory (create_app()), Gunicorn can call it: "app:create_app()".

1. User, code and virtualenv

sudo apt update
sudo apt install -y python3-venv nginx git
sudo adduser --system --group --home /srv/flaskapp flaskapp
sudo -u flaskapp git clone https://github.com/you/flaskapp.git /srv/flaskapp/app
sudo -u flaskapp python3 -m venv /srv/flaskapp/venv
sudo -u flaskapp /srv/flaskapp/venv/bin/pip install -r /srv/flaskapp/app/requirements.txt gunicorn

2. Configuration via environment

/srv/flaskapp/.env (owner flaskapp, mode 600):

FLASK_SECRET_KEY=a-long-random-string
DATABASE_URL=postgresql://flaskapp:[email protected]/flaskapp

Read them in your app: app.config["SECRET_KEY"] = os.environ["FLASK_SECRET_KEY"]. Never run with debug=True in production — the Werkzeug debugger allows code execution from the browser.

3. Test Gunicorn by hand

cd /srv/flaskapp/app
sudo -u flaskapp /srv/flaskapp/venv/bin/gunicorn -w 2 -b 127.0.0.1:8000 app:app
curl http://127.0.0.1:8000/

Stop it with Ctrl+C once it works.

4. systemd service

/etc/systemd/system/flaskapp.service:

[Unit]
Description=Flask app (Gunicorn)
After=network.target

[Service]
User=flaskapp
Group=flaskapp
WorkingDirectory=/srv/flaskapp/app
EnvironmentFile=/srv/flaskapp/.env
ExecStart=/srv/flaskapp/venv/bin/gunicorn \
    --workers 3 \
    --bind 127.0.0.1:8000 \
    --timeout 60 \
    --access-logfile - \
    app:app
ExecReload=/bin/kill -s HUP $MAINPID
Restart=on-failure

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now flaskapp
systemctl status flaskapp
journalctl -u flaskapp -f

Choosing workers

  • CPU-bound or typical apps: start with 2 × cores + 1 sync workers, then adjust to RAM (each worker loads the whole app).
  • I/O-heavy apps (many external API calls): use threads (--threads 4 --worker-class gthread) or async workers (gevent).
  • Long-running requests: raise --timeout, or better, move the work to a background queue.

5. Nginx

/etc/nginx/sites-available/flaskapp:

server {
    listen 80;
    server_name app.example.com;

    location /static/ {
        alias /srv/flaskapp/app/static/;
        expires 7d;
    }

    location / {
        proxy_pass http://127.0.0.1:8000;
        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;
    }
}
sudo ln -s /etc/nginx/sites-available/flaskapp /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

Make sure Nginx can read the static folder (chmod 755 on /srv/flaskapp and /srv/flaskapp/app).

Tell Flask about the proxy

Behind Nginx, Flask sees requests from 127.0.0.1 over HTTP. Use Werkzeug’s ProxyFix so request.remote_addr, url_for(..., _external=True) and HTTPS detection are correct:

from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1)

6. HTTPS

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d app.example.com

7. Deploy updates without downtime

cd /srv/flaskapp/app
sudo -u flaskapp git pull --ff-only
sudo -u flaskapp /srv/flaskapp/venv/bin/pip install -r requirements.txt
sudo systemctl reload flaskapp

reload sends HUP to Gunicorn, which starts new workers with the new code and gracefully stops the old ones — in-flight requests finish.

Unix socket instead of a TCP port (optional)

Bind Gunicorn to unix:/run/flaskapp/gunicorn.sock (add RuntimeDirectory=flaskapp to the service) and use proxy_pass http://unix:/run/flaskapp/gunicorn.sock; in Nginx. Slightly faster, and the app isn’t reachable via any TCP port at all. Ensure Nginx’s user can access the socket.

Troubleshooting

  • 502 Bad Gateway — Gunicorn isn’t running or is bound elsewhere; check journalctl -u flaskapp. See Nginx 502.
  • ModuleNotFoundError: No module named 'app' — wrong WorkingDirectory or module path in the app:app argument.
  • [CRITICAL] WORKER TIMEOUT — a request exceeded --timeout; find the slow code path.
  • URLs generated with http:// — add ProxyFix and the X-Forwarded-Proto header.

Related: Python virtual environments, Django with Gunicorn and Nginx, and FastAPI with Uvicorn. A small Ubuntu VPS runs a Flask API comfortably.

  • Flask
  • Python
  • Nginx