How to Set Up Nginx for a Flask Application

How to Set Up Nginx for a Flask Application

How to Set Up Nginx for a Flask Application

I remember running flask run on a production server once, early in my career, and wondering why the app kept falling over under any real traffic. It turns out Flask’s built-in development server is exactly that — a development server. It’s single-threaded by default, not designed for concurrent production load, and Flask’s own documentation tells you not to use it in production. What you actually need is a proper WSGI server like Gunicorn or uWSGI, with Nginx sitting in front of it. That’s what I’m going to walk through here.

Why You Need Nginx (and a WSGI Server) for Flask

Flask apps speak WSGI (Web Server Gateway Interface), not raw HTTP. Nginx doesn’t natively speak WSGI, so you need a middleman — a WSGI server like Gunicorn — to translate between Nginx and your Flask application. The architecture looks like this:

Client → Nginx (port 80/443) → Gunicorn (port 8000, localhost) → Flask app

Nginx handles TLS, serves static files, and manages client connections efficiently. Gunicorn manages a pool of worker processes that actually run your Python code. This split is important: Nginx is written in C and handles thousands of concurrent connections with minimal memory, while your Python workers stay focused purely on executing application logic.

Requirements

Set up your virtual environment and install dependencies:

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

Confirm your app has a proper WSGI entry point. If your Flask app object is defined in app.py as app = Flask(__name__), Gunicorn can find it directly. Test Gunicorn manually first before touching Nginx:

gunicorn --bind 127.0.0.1:8000 app:app

Visit http://127.0.0.1:8000 locally (or curl it) to confirm the app responds. Don’t move to the Nginx step until this works — I’ve wasted time debugging Nginx configs when the real issue was Gunicorn never starting.

Step 1: Create a Systemd Service for Gunicorn

Running Gunicorn manually in a terminal isn’t sustainable — I use systemd to keep it running and restart it automatically if it crashes.

sudo nano /etc/systemd/system/myflaskapp.service
[Unit]
Description=Gunicorn 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/gunicorn --workers 3 --bind unix:myflaskapp.sock -m 007 app:app

[Install]
WantedBy=multi-user.target

A few notes on this file:

Enable and start it:

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

Step 2: Create the Nginx Server Block

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

    access_log /var/log/nginx/myflaskapp.access.log;
    error_log /var/log/nginx/myflaskapp.error.log;

    location / {
        include proxy_params;
        proxy_pass http://unix:/var/www/myflaskapp/myflaskapp.sock;
    }

    location /static/ {
        alias /var/www/myflaskapp/static/;
        expires 30d;
    }

    client_max_body_size 10M;
}

A couple of things worth explaining:

Step 3: Enable the Site

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

Visit http://myflaskapp.example.com — you should see your Flask app served through the full stack now.

Step 4: Add HTTPS

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

Certbot handles the certificate and updates the server block for you, redirecting HTTP to HTTPS automatically.

Handling Environment Variables and Configuration

Flask apps typically need environment-specific config (secret keys, database URLs). I set these in the systemd service file rather than hardcoding them:

Environment="FLASK_ENV=production"
Environment="SECRET_KEY=your-secret-key-here"
Environment="DATABASE_URL=postgresql://user:pass@localhost/dbname"

After editing the service file, always run:

sudo systemctl daemon-reload
sudo systemctl restart myflaskapp

Testing Your Setup

  1. sudo nginx -t to check syntax
  2. sudo systemctl status myflaskapp to confirm Gunicorn is active
  3. curl -I http://myflaskapp.example.com
  4. Check that static assets load correctly (open browser dev tools and confirm no 404s on CSS/JS)
  5. Check logs in real time while testing: sudo tail -f /var/log/nginx/myflaskapp.error.log alongside sudo journalctl -u myflaskapp -f

Troubleshooting Common Issues

502 Bad Gateway — Almost always means Gunicorn isn’t running or the socket path doesn’t match between the systemd file and the Nginx config. Check sudo systemctl status myflaskapp and confirm the socket file exists: ls -l /var/www/myflaskapp/myflaskapp.sock.

Permission denied on socket — Make sure the socket’s group ownership allows Nginx’s user to access it; the -m 007 flag in the Gunicorn command handles this, but double check the User/Group in the systemd file match what Nginx runs as.

Static files 404ing — Confirm the alias path in the Nginx config matches your actual Flask static folder exactly, including trailing slashes — Nginx is picky about this.

App changes not appearing — Gunicorn caches your app in memory; you need to restart it after every deploy: sudo systemctl restart myflaskapp.

Worker timeouts on slow requests — Increase Gunicorn’s --timeout flag (default is 30 seconds) for endpoints doing heavy processing, or better, move slow work to a background task queue like Celery.

Security Considerations

Performance Tips

Real-World Use Case

I ran a data-processing API built in Flask that received webhook payloads and had to respond quickly while doing the heavy lifting asynchronously. The setup was Nginx terminating TLS, proxying to Gunicorn with gthread workers (since the app spent most of its time waiting on I/O), and offloading actual processing to a Celery task queue backed by Redis. This kept response times under 100ms even though the real work sometimes took several seconds — Nginx and Gunicorn never had to wait around for it.

Best Practices Recap

Once you understand that Flask, Gunicorn, and Nginx are three distinct layers each doing one job well, the whole setup stops feeling mysterious. Nginx faces the internet, Gunicorn manages your Python processes, and Flask just focuses on being a web framework — exactly what it was designed to do.

Exit mobile version