Reference Guide Detailed deployment notes with production context and concrete examples.

Nginx Reverse Proxy for Flask Explained

If you're trying to understand how Nginx sits in front of Flask or need a working reverse proxy setup, this guide shows you how to configure it step-by-step. The outcome is a Flask app served through Gunicorn with Nginx handling client traffic, headers, and static delivery.

Quick Fix / Quick Setup

Use this minimal Nginx server block if Gunicorn is already listening on 127.0.0.1:8000:

bash
sudo tee /etc/nginx/sites-available/flaskapp >/dev/null <<'EOF'
server {
    listen 80;
    server_name example.com www.example.com;

    location /static/ {
        alias /var/www/flaskapp/static/;
    }

    location / {
        include proxy_params;
        proxy_pass http://127.0.0.1:8000;
        proxy_redirect off;
    }
}
EOF

sudo ln -sf /etc/nginx/sites-available/flaskapp /etc/nginx/sites-enabled/flaskapp
sudo nginx -t
sudo systemctl reload nginx

# Test Gunicorn first
curl -I http://127.0.0.1:8000

# Test through Nginx
curl -I http://example.com

If Gunicorn uses a Unix socket, replace:

nginx
proxy_pass http://127.0.0.1:8000;

with:

nginx
proxy_pass http://unix:/run/gunicorn.sock;

This assumes static files exist at /var/www/flaskapp/static/.

What’s Happening

Nginx accepts incoming client requests and acts as the public web server. It forwards dynamic requests to Gunicorn, which runs the Flask WSGI app, and it can serve static files directly without involving Flask. Most failures happen when the upstream target, socket permissions, headers, or static file paths do not match the running app.

Step-by-Step Guide

  1. Start or verify Gunicorn first
    Confirm your Flask app is running behind Gunicorn before configuring Nginx.
    Example:
    bash
    gunicorn --bind 127.0.0.1:8000 wsgi:app
    

    If using systemd, verify the service:
    bash
    sudo systemctl status gunicorn --no-pager
    
  2. Test Gunicorn directly on the local machine
    Do not add Nginx until the upstream works.
    bash
    curl -I http://127.0.0.1:8000
    

    Expected result: 200, 301, 302, or your expected app response.
  3. Create an Nginx server block
    Create /etc/nginx/sites-available/flaskapp:
    nginx
    server {
        listen 80;
        server_name example.com www.example.com;
    
        location /static/ {
            alias /var/www/flaskapp/static/;
        }
    
        location / {
            include proxy_params;
            proxy_pass http://127.0.0.1:8000;
            proxy_redirect off;
        }
    }
    

    Replace:
    • example.com www.example.com with your real domain
    • /var/www/flaskapp/static/ with your actual static directory
  4. Enable the site
    bash
    sudo ln -sf /etc/nginx/sites-available/flaskapp /etc/nginx/sites-enabled/flaskapp
    

    If the default site conflicts, disable it:
    bash
    sudo rm -f /etc/nginx/sites-enabled/default
    
  5. Validate Nginx configuration
    bash
    sudo nginx -t
    

    Fix any reported syntax errors before reloading.
  6. Reload Nginx
    bash
    sudo systemctl reload nginx
    
  7. Test the public Nginx path
    bash
    curl -I http://example.com
    

    Also test in a browser.
  8. Optional: use a Unix socket instead of TCP
    If Gunicorn is bound to a socket:
    bash
    gunicorn --bind unix:/run/gunicorn.sock wsgi:app
    

    Update the Nginx server block:
    nginx
    server {
        listen 80;
        server_name example.com www.example.com;
    
        location /static/ {
            alias /var/www/flaskapp/static/;
        }
    
        location / {
            include proxy_params;
            proxy_pass http://unix:/run/gunicorn.sock;
            proxy_redirect off;
        }
    }
    

    Then verify the socket exists:
    bash
    sudo ls -l /run/gunicorn.sock
    

    Nginx must be able to read the socket.
  9. Verify forwarded headers are included
    This line is important:
    nginx
    include proxy_params;
    

    It forwards standard proxy headers such as Host and client IP metadata. Without it, Flask may generate incorrect URLs or mis-handle scheme and host information when additional proxy layers or HTTPS are added.
  10. Serve static files directly from Nginx
    Use alias for static assets:
    nginx
    location /static/ {
        alias /var/www/flaskapp/static/;
    }
    

    Confirm the files exist:
    bash
    ls -lah /var/www/flaskapp/static/
    
  11. Harden the deployment baseline
    Keep Gunicorn private to the local machine unless you explicitly need external access.
    Recommended:
    • bind Gunicorn to 127.0.0.1:8000 or /run/gunicorn.sock
    • remove the default Nginx site
    • add HTTPS after HTTP proxying is confirmed working
  12. Validate end-to-end behavior
    Check all layers:
    bash
    curl -I http://127.0.0.1:8000
    curl -I http://example.com
    sudo nginx -t
    sudo systemctl status nginx --no-pager
    sudo systemctl status gunicorn --no-pager
    

Minimal Nginx Server Block Example

nginx
server {
    listen 80;
    server_name example.com www.example.com;

    location /static/ {
        alias /var/www/flaskapp/static/;
    }

    location / {
        include proxy_params;
        proxy_pass http://127.0.0.1:8000;
        proxy_redirect off;
    }
}

When to Use TCP vs Unix Socket

  • Use 127.0.0.1:8000 for a simpler first deployment and easier curl testing.
  • Use a Unix socket such as /run/gunicorn.sock for local-only communication and tighter process coupling.
  • If using a socket, permission and ownership issues are a common failure point.
  • If using TCP, do not expose Gunicorn publicly on 0.0.0.0 unless that is intentional and firewalled.

Common Causes

  • Gunicorn not running → Nginx has no upstream to connect to → start or restart Gunicorn and retest locally.
    bash
    sudo systemctl restart gunicorn
    curl -I http://127.0.0.1:8000
    
  • Wrong proxy_pass target → Nginx points to the wrong port or socket → match Nginx to the actual Gunicorn bind.
    bash
    ss -ltnp | grep 8000
    ss -lx | grep gunicorn
    
  • Socket permission mismatch → Nginx cannot access /run/gunicorn.sock → fix service user, group, and socket permissions.
    bash
    sudo ls -l /run/gunicorn.sock
    
  • Missing include proxy_params → forwarded headers are incomplete → add the standard proxy parameters include.
  • Incorrect server_name → requests hit the wrong server block → update the domain list and reload Nginx.
  • Bad static alias path → app works but static files return 404 → point alias to the real static directory.
  • Nginx syntax error → reload fails or old config remains active → run nginx -t and fix the reported line.
  • Gunicorn bound to 0.0.0.0 unexpectedly → direct exposure bypasses Nginx and weakens the setup → bind to 127.0.0.1 or a Unix socket.

Debugging Section

Run these checks in order.

1. Validate Nginx configuration

bash
sudo nginx -t

Look for:

  • syntax is ok
  • test is successful

2. Check Nginx service status

bash
sudo systemctl status nginx --no-pager

Look for:

  • service is active (running)
  • no recent config or bind errors

3. Check Gunicorn service status

bash
sudo systemctl status gunicorn --no-pager

Look for:

  • service is running
  • no import errors
  • no worker boot failures

4. Confirm Gunicorn is listening

For TCP:

bash
ss -ltnp | grep 8000

For Unix sockets:

bash
ss -lx | grep gunicorn
sudo ls -l /run/gunicorn.sock

Look for:

  • expected port or socket exists
  • correct ownership and permissions

5. Test the upstream directly

bash
curl -I http://127.0.0.1:8000

Look for:

  • direct success before testing through Nginx

6. Test through Nginx

bash
curl -I http://your-domain

Look for:

  • expected response code
  • if direct Gunicorn works but Nginx fails, the issue is in Nginx configuration or routing

7. Inspect Nginx logs

bash
sudo tail -n 100 /var/log/nginx/error.log
sudo tail -n 100 /var/log/nginx/access.log

Look for:

  • connect() failed
  • permission denied
  • no such file or directory
  • requests hitting the expected server block

8. Inspect journald logs

bash
sudo journalctl -u nginx -n 100 --no-pager
sudo journalctl -u gunicorn -n 100 --no-pager

Look for:

  • startup failures
  • path errors
  • Python import or dependency failures

9. Dump active Nginx configuration

bash
sudo nginx -T | sed -n '/server_name example.com/,/}/p'

Use this to confirm:

  • the active server_name
  • the active proxy_pass
  • no conflicting config is overriding your site

If requests fail with 502, continue with Fix Flask 502 Bad Gateway (Step-by-Step Guide). If static assets fail, continue with Flask Static Files Not Loading in Production.

Checklist

  • Gunicorn is running and bound to localhost TCP or a valid Unix socket
  • Nginx server block points to the correct upstream
  • server_name matches the domain or server IP being tested
  • Static file alias path exists if Nginx serves static assets
  • nginx -t passes without errors
  • Nginx reloaded successfully
  • Direct Gunicorn test works before Nginx test
  • Domain request reaches the Flask app through Nginx

FAQ

Q: What is the reverse proxy layer doing in a Flask deployment?
A: It accepts client requests, handles web-server concerns, and forwards application traffic to Gunicorn.

Q: Does Nginx run Flask directly?
A: No. Nginx proxies requests to Gunicorn or another WSGI server running the Flask app.

Q: Should I start with a port or a Unix socket?
A: Start with 127.0.0.1:8000 for easier testing, then switch to a socket if needed.

Q: Should proxy_pass point to localhost or a socket?
A: Either works. TCP is simpler to debug; Unix sockets are common in production.

Q: Can Nginx serve static files without Flask?
A: Yes. Use a location block with alias to serve static assets directly.

Q: Why serve static files from Nginx instead of Flask?
A: Nginx is more efficient for static file delivery and keeps Gunicorn focused on dynamic requests.

Q: Why do I get 502 Bad Gateway after adding Nginx?
A: The upstream Gunicorn service is usually down, misbound, or inaccessible due to socket or permission issues.

Q: Is this enough for HTTPS?
A: No. Add TLS certificates and an HTTPS server block or use Certbot after the proxy is working.

Final Takeaway

An Nginx reverse proxy for Flask means Nginx handles client traffic and forwards app requests to Gunicorn. In practice, most setup issues come from an incorrect upstream target, missing proxy headers, broken static paths, or socket permission problems. Start with a simple localhost Gunicorn bind, verify it directly, then add Nginx, static files, and HTTPS in stages.