How to Set Up Nginx with Flask and uWSGI

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

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:

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

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;
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;
}

Performance Tips

gzip on;
gzip_types text/plain application/json application/javascript text/css;
gzip_min_length 512;

Real-World Use Cases

Best Practices Recap

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.

Exit mobile version