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:
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:
proxy_pass http://127.0.0.1:8000;
with:
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
- Start or verify Gunicorn first
Confirm your Flask app is running behind Gunicorn before configuring Nginx.
Example:bashgunicorn --bind 127.0.0.1:8000 wsgi:app
If using systemd, verify the service:bashsudo systemctl status gunicorn --no-pager - Test Gunicorn directly on the local machine
Do not add Nginx until the upstream works.bashcurl -I http://127.0.0.1:8000
Expected result:200,301,302, or your expected app response. - Create an Nginx server block
Create/etc/nginx/sites-available/flaskapp:nginxserver { 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.comwith your real domain/var/www/flaskapp/static/with your actual static directory
- Enable the sitebash
sudo ln -sf /etc/nginx/sites-available/flaskapp /etc/nginx/sites-enabled/flaskapp
If the default site conflicts, disable it:bashsudo rm -f /etc/nginx/sites-enabled/default - Validate Nginx configurationbash
sudo nginx -t
Fix any reported syntax errors before reloading. - Reload Nginxbash
sudo systemctl reload nginx - Test the public Nginx pathbash
curl -I http://example.com
Also test in a browser. - Optional: use a Unix socket instead of TCP
If Gunicorn is bound to a socket:bashgunicorn --bind unix:/run/gunicorn.sock wsgi:app
Update the Nginx server block:nginxserver { 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:bashsudo ls -l /run/gunicorn.sock
Nginx must be able to read the socket. - Verify forwarded headers are included
This line is important:nginxinclude proxy_params;
It forwards standard proxy headers such asHostand 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. - Serve static files directly from Nginx
Usealiasfor static assets:nginxlocation /static/ { alias /var/www/flaskapp/static/; }
Confirm the files exist:bashls -lah /var/www/flaskapp/static/ - 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:8000or/run/gunicorn.sock - remove the default Nginx site
- add HTTPS after HTTP proxying is confirmed working
- bind Gunicorn to
- Validate end-to-end behavior
Check all layers:bashcurl -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
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:8000for a simpler first deployment and easiercurltesting. - Use a Unix socket such as
/run/gunicorn.sockfor 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.0unless 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_passtarget → Nginx points to the wrong port or socket → match Nginx to the actual Gunicorn bind.bashss -ltnp | grep 8000 ss -lx | grep gunicorn - Socket permission mismatch → Nginx cannot access
/run/gunicorn.sock→ fix service user, group, and socket permissions.bashsudo 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→ pointaliasto the real static directory. - Nginx syntax error → reload fails or old config remains active → run
nginx -tand fix the reported line. - Gunicorn bound to
0.0.0.0unexpectedly → direct exposure bypasses Nginx and weakens the setup → bind to127.0.0.1or a Unix socket.
Debugging Section
Run these checks in order.
1. Validate Nginx configuration
sudo nginx -t
Look for:
syntax is oktest is successful
2. Check Nginx service status
sudo systemctl status nginx --no-pager
Look for:
- service is
active (running) - no recent config or bind errors
3. Check Gunicorn service status
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:
ss -ltnp | grep 8000
For Unix sockets:
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
curl -I http://127.0.0.1:8000
Look for:
- direct success before testing through Nginx
6. Test through Nginx
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
sudo tail -n 100 /var/log/nginx/error.log
sudo tail -n 100 /var/log/nginx/access.log
Look for:
connect() failedpermission deniedno such file or directory- requests hitting the expected server block
8. Inspect journald logs
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
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_namematches the domain or server IP being tested - Static file alias path exists if Nginx serves static assets
-
nginx -tpasses without errors - Nginx reloaded successfully
- Direct Gunicorn test works before Nginx test
- Domain request reaches the Flask app through Nginx
Related Guides
- Deploy Flask with Nginx + Gunicorn (Step-by-Step Guide)
- Fix Flask 502 Bad Gateway (Step-by-Step Guide)
- Flask Static Files Not Loading in Production
- Flask Production Checklist (Everything You Must Do)
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.