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 + 1sync 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'— wrongWorkingDirectoryor module path in theapp:appargument.[CRITICAL] WORKER TIMEOUT— a request exceeded--timeout; find the slow code path.- URLs generated with
http://— addProxyFixand theX-Forwarded-Protoheader.
Related: Python virtual environments, Django with Gunicorn and Nginx, and FastAPI with Uvicorn. A small Ubuntu VPS runs a Flask API comfortably.