How to Set Up Nginx with Flask and uWSGI

How to Set Up Nginx with Flask and uWSGI

Flask’s built-in development server prints a friendly warning every time you start it: “This is a development server. Do not use it in a production deployment.” A lot of people ignore that warning until something breaks under real traffic. I’ve been there. The fix is a well-worn, reliable combination: uWSGI to run your Flask app as a proper application server, and Nginx in front of it to handle the internet-facing side of things.

This guide walks through the whole stack, from a bare server to a Flask app running behind Nginx and uWSGI with a proper systemd service, SSL, and production-grade configuration.

Why Flask Needs Help in Production

Flask is a WSGI application — it implements the interface, but it doesn’t include a production-grade server to run that interface at scale. Flask’s dev server is single-threaded by default, doesn’t handle concurrent connections well, and has no process management (no automatic restarts on crash, no worker pooling). uWSGI solves all of that: it’s a mature, battle-tested application server that speaks WSGI natively, manages multiple worker processes, and integrates tightly with Nginx via its own efficient binary protocol.

Requirements

  • Ubuntu 22.04/24.04 server with sudo access
  • Python 3.8+ installed
  • A Flask application, structured with an application factory or a simple app = Flask(__name__) object exposed at module level
  • pip and venv

I’ll assume your project lives at /var/www/myflaskapp with the main Flask object accessible as app inside wsgi.py.

Step 1: Install System Packages

sudo apt update
sudo apt install python3-pip python3-venv nginx -y

Step 2: Set Up a Virtual Environment and Install Dependencies

cd /var/www/myflaskapp
python3 -m venv venv
source venv/bin/activate
pip install flask uwsgi

If your app has a requirements.txt, install from that instead:

pip install -r requirements.txt
pip install uwsgi

Step 3: Create the WSGI Entry Point

Create wsgi.py in your project root if it doesn’t already exist:

# wsgi.py
from myapp import create_app

app = create_app()

if __name__ == "__main__":
    app.run()

If your app is a simple single-file Flask app rather than a factory pattern, this can be as simple as:

# wsgi.py
from app import app

if __name__ == "__main__":
    app.run()

This file is the bridge uWSGI uses to find your actual Flask app object.

Step 4: Test uWSGI Directly

Before wiring in Nginx, confirm uWSGI can serve the app on its own:

uwsgi --socket 0.0.0.0:8000 --protocol=http -w wsgi:app

Visit http://your-server-ip:8000 in a browser. If your Flask app responds, uWSGI is correctly finding and running it. Stop it with Ctrl+C once confirmed — we don’t want it bound to a public port long-term.

Step 5: Create a uWSGI Configuration File

Rather than passing flags on the command line, use an .ini file for anything beyond a quick test:

nano /var/www/myflaskapp/myapp.ini
[uwsgi]
module = wsgi:app

master = true
processes = 4
threads = 2

socket = /var/www/myflaskapp/myapp.sock
chmod-socket = 660
vacuum = true

die-on-term = true
harakiri = 30
max-requests = 5000

buffer-size = 32768

A quick explanation of the less obvious settings:

  • master = true enables the uWSGI master process, which manages worker processes and lets you reload gracefully.
  • processes / threads control concurrency. A common starting formula is (2 x CPU cores) + 1 processes, adjusted based on whether your app is more I/O-bound (favor threads) or CPU-bound (favor processes).
  • socket — this uses a Unix socket rather than a TCP port, similar to the Ruby/Puma setup; Nginx talks to uWSGI over this socket using the native uwsgi protocol, which is more efficient than proxying HTTP.
  • chmod-socket = 660 and vacuum = true control socket permissions and ensure the socket file is cleaned up when uWSGI stops.
  • harakiri = 30 kills any worker that takes longer than 30 seconds to respond — a safety net against hung requests.
  • max-requests = 5000 recycles each worker after 5000 requests, which helps guard against slow memory leaks in long-running Python processes.

Step 6: Run uWSGI as a systemd Service

sudo nano /etc/systemd/system/myflaskapp.service
[Unit]
Description=uWSGI instance for myflaskapp
After=network.target

[Service]
User=www-data
Group=www-data
WorkingDirectory=/var/www/myflaskapp
Environment="PATH=/var/www/myflaskapp/venv/bin"
ExecStart=/var/www/myflaskapp/venv/bin/uwsgi --ini myapp.ini

Restart=always
KillSignal=SIGQUIT
Type=notify
NotifyAccess=all

[Install]
WantedBy=multi-user.target

Enable and start it:

sudo systemctl daemon-reload
sudo systemctl enable myflaskapp
sudo systemctl start myflaskapp
sudo systemctl status myflaskapp

Check the socket file was created:

ls -la /var/www/myflaskapp/myapp.sock

Step 7: Configure Nginx

sudo nano /etc/nginx/sites-available/myflaskapp
server {
    listen 80;
    server_name example.com www.example.com;

    client_max_body_size 10M;

    location /static/ {
        alias /var/www/myflaskapp/static/;
        expires 30d;
        add_header Cache-Control "public";
    }

    location / {
        include uwsgi_params;
        uwsgi_pass unix:/var/www/myflaskapp/myapp.sock;

        uwsgi_read_timeout 60s;
        uwsgi_send_timeout 60s;
    }

    error_page 502 503 /maintenance.html;
    location = /maintenance.html {
        root /var/www/myflaskapp/static;
    }
}

Notice uwsgi_pass instead of proxy_pass — that’s the key difference from a typical HTTP reverse proxy setup. Nginx has a dedicated module for speaking the uwsgi binary protocol directly to the socket, and include uwsgi_params; pulls in a standard set of header mappings (Nginx ships this file at /etc/nginx/uwsgi_params by default) so uWSGI receives proper REMOTE_ADDR, HTTP_HOST, etc.

Enable the site:

sudo ln -s /etc/nginx/sites-available/myflaskapp /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx

Step 8: Fix the www-data Permission Chain

A common snag: uWSGI runs as www-data and creates the socket owned by www-data, and Nginx’s worker processes also run as www-data by default (check /etc/nginx/nginx.conf for the user directive), so this usually just works. If you changed the systemd User= to something else (like deploy), make sure Nginx can still read/write the socket — either match the users, or add www-data to the app user’s group.

Step 9: Add HTTPS

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

Certbot updates your server block automatically with the SSL certificate paths and an HTTP-to-HTTPS redirect.

Testing Your Setup

curl -I https://example.com

Expect a 200 (or your app’s actual root response). Test a dynamic route that hits your database or does real work, not just the homepage, to make sure the full request path — Nginx → socket → uWSGI → Flask → your logic — is functioning.

You can also watch uWSGI’s own logs live while testing:

sudo journalctl -u myflaskapp -f

And Nginx’s error log in another terminal:

sudo tail -f /var/log/nginx/error.log

Troubleshooting Common Issues

502 Bad Gateway — Check that the uWSGI service is actually running and the socket file exists with correct permissions. Also confirm the socket path in your Nginx config exactly matches the one in myapp.ini.

“ModuleNotFoundError” in uWSGI logs — Usually means uWSGI isn’t using your virtualenv’s Python. Add this to myapp.ini if needed:

virtualenv = /var/www/myflaskapp/venv

Static files 404 — Double check the alias path in the /static/ location block matches your actual static folder, and that file permissions allow the Nginx user to read them.

Long-running requests time out — Adjust both harakiri in myapp.ini and uwsgi_read_timeout in Nginx; they need to agree, or one will kill the request before the other expects it to end.

App changes not reflected after deploy — uWSGI’s master process needs a reload, not just a restart of Nginx:

sudo systemctl restart myflaskapp

Or, for zero-downtime reloads once you’re comfortable, use uWSGI’s touch-reload feature:

touch-reload = /var/www/myflaskapp/wsgi.py

Then a simple touch wsgi.py after a deploy triggers a graceful worker reload.

Security Considerations

  • Never bind uWSGI’s socket to a public TCP port in production. Unix sockets, scoped to the local filesystem, are the safer default.
  • Set server_tokens off; in nginx.conf to avoid leaking version info.
  • Add standard security headers:
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
  • Use Flask’s SECRET_KEY and session cookie settings correctlySESSION_COOKIE_SECURE = True and SESSION_COOKIE_HTTPONLY = True in your Flask config, so cookies aren’t sent over plain HTTP or exposed to JavaScript.
  • Rate limit sensitive endpoints (login, password reset, API) at the Nginx layer:
limit_req_zone $binary_remote_addr zone=flasklogin:10m rate=5r/m;

location /login {
    limit_req zone=flasklogin burst=5 nodelay;
    include uwsgi_params;
    uwsgi_pass unix:/var/www/myflaskapp/myapp.sock;
}
  • Never run Flask with debug=True in production. It exposes an interactive debugger that allows arbitrary code execution if reached by an attacker. Confirm this is off in your app config, independent of anything Nginx does.

Performance Tips

  • Tune processes and threads based on real load testing, not guesswork. Use a tool like wrk or locust to simulate traffic and watch CPU/memory while adjusting.
  • Enable gzip for JSON/text responses:
gzip on;
gzip_types text/plain application/json application/javascript text/css;
gzip_min_length 512;
  • Cache static assets aggressively with long expires headers, and use cache-busting filenames (hash-based) if your build process supports it.
  • Consider uwsgi_cache for GET-heavy endpoints that don’t change often — Nginx can cache uWSGI responses directly, skipping Python entirely for repeat requests within a TTL window.
  • Watch memory per worker. Flask apps with large in-memory caches or ML models loaded at startup can balloon memory usage; multiply per-worker RAM by processes to sanity check total server memory needs.

Real-World Use Cases

  • An internal analytics dashboard built in Flask, serving a small team — uWSGI with 2 processes was more than enough, fronted by Nginx purely for TLS termination and basic auth at the proxy layer.
  • A public-facing REST API for a mobile app, where Nginx handled rate limiting per API key (via a custom header check) before requests ever reached the Flask/uWSGI layer.
  • A machine learning inference service where each uWSGI worker loaded a model into memory at startup (lazy-apps = true to avoid loading the model multiple times unnecessarily during preload), with Nginx queuing and load-balancing requests across a modest worker pool sized to available GPU/CPU resources.

Best Practices Recap

  • Use Unix sockets between Nginx and uWSGI, never a public TCP port for uWSGI.
  • Manage uWSGI through systemd so it survives crashes and reboots.
  • Keep uwsgi_params included in your location block — don’t hand-roll header forwarding.
  • Set harakiri and Nginx timeouts to agree with each other.
  • Recycle workers periodically with max-requests to guard against memory creep.
  • Always terminate SSL at Nginx and redirect HTTP to HTTPS.
  • Load test before finalizing processes/threads counts — the right numbers depend entirely on your app’s actual workload.

With this setup in place, your Flask app gets everything it was missing from the dev server: real concurrency, process supervision, and a fast, secure front door courtesy of Nginx.

Total
1
Shares

Leave a Reply

Previous Post
How to Enable HTTP/3 in Nginx

How to Enable HTTP/3 in Nginx

Next Post
How to Use Nginx as a WebSocket Load Balancer

How to Use Nginx as a WebSocket Load Balancer

Related Posts