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

  • A Linux server with Python 3 and pip installed
  • Your Flask application code deployed to the server
  • Nginx installed
  • Gunicorn installed inside your project’s virtual environment

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:

  • --workers 3 — a common starting formula is (2 x CPU cores) + 1, but I always benchmark rather than trust the formula blindly.
  • --bind unix:myflaskapp.sock — I prefer a Unix socket over a TCP port for local communication between Nginx and Gunicorn; it’s marginally faster and avoids taking up a TCP port unnecessarily.
  • -m 007 — sets socket permissions so Nginx (running as www-data or similar) can read/write it.

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:

  • include proxy_params; — Nginx ships with a proxy_params file (in /etc/nginx/) that sets the standard Host, X-Real-IP, and X-Forwarded-For headers for you, so you don’t have to repeat them manually.
  • location /static/ { alias ... } — I always let Nginx serve Flask’s static files directly rather than routing them through Gunicorn and Python. It’s significantly faster and takes load off your app workers.

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

  • Never run Flask’s built-in dev server (app.run()) in production
  • Set debug=False in your Flask app — leaving debug mode on exposes an interactive debugger that can execute arbitrary code if reached by an attacker
  • Store secrets as environment variables, never committed to source control
  • Restrict the Gunicorn socket so only Nginx can access it (the -m 007 permission mode handles this)
  • Add rate limiting in Nginx for login or API endpoints using limit_req_zone
  • Keep Python, Flask, and all dependencies patched — run pip list --outdated periodically

Performance Tips

  • Tune Gunicorn worker count based on actual load testing, not just the CPU-count formula
  • Use gevent or gthread worker classes for I/O-bound Flask apps (lots of external API calls or DB queries) instead of the default sync workers
  • Let Nginx handle all static file serving — never proxy static assets through Python
  • Enable gzip in Nginx for JSON and HTML responses
  • Cache expensive endpoints with Flask-Caching backed by Redis if you have read-heavy routes
  • Use connection pooling for your database (e.g., SQLAlchemy’s built-in pool) so Gunicorn workers aren’t opening new DB connections per request

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

  • Always use Gunicorn (or uWSGI) in production, never Flask’s dev server
  • Manage Gunicorn with systemd for automatic restarts
  • Use a Unix socket for local Nginx-to-Gunicorn communication
  • Let Nginx serve static files directly
  • Store config as environment variables
  • Restart Gunicorn after every deployment
  • Terminate TLS at Nginx with Certbot

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.

Total
1
Shares

Leave a Reply

Previous Post
How to Set Up Nginx for a Node.js Application

How to Set Up Nginx for a Node.js Application

Next Post
How to Set Up Nginx for a Laravel Application

How to Set Up Nginx for a Laravel Application

Related Posts