Flask Nginx Production Setup: 9 Common Pitfalls and How to Fix Them
The most frequent Flask nginx production failures stem from mismatched proxy headers, missing ProxyFix middleware, and incorrect upstream binding between nginx and the WSGI server.
Deploying a Flask application behind nginx creates a robust reverse-proxy architecture where nginx handles TLS termination, static assets, and buffering while forwarding dynamic requests to a WSGI server like Gunicorn or uWSGI. However, according to the pallets/flask source code and official documentation, several configuration details in docs/deploying/nginx.rst and docs/deploying/proxy_fix.rst are frequent sources of 502 errors, broken URL generation, and security vulnerabilities in production environments.
The Reverse Proxy Architecture
In a standard Flask nginx production deployment, nginx sits between the internet and your application. Nginx receives external HTTP/HTTPS traffic, performs TLS termination, and serves static files directly from disk. For dynamic content, nginx acts as a reverse proxy, forwarding requests to a WSGI server running your Flask application on a local port or Unix socket. This separation of concerns improves performance and security, but requires precise configuration of headers, middleware, and network binding.
Critical Configuration Pitfalls
Mismatched Upstream URLs and Ports
The most immediate failure occurs when the proxy_pass directive in your nginx configuration points to a different address than where your WSGI server actually listens. A typo such as http://127.0.0.1:8000/ while Gunicorn binds to port 8001 results in a 502 Bad Gateway error.
Verify your WSGI server’s bind address exactly matches the proxy_pass URL. As shown in docs/deploying/nginx.rst (lines 48-62), the minimal configuration expects your application server to listen on http://127.0.0.1:8000/.
Improper X-Forwarded Header Forwarding
Without forwarding the X-Forwarded-* headers, Flask sees every request as originating from localhost (127.0.0.1), losing the original client IP, scheme, host, and path prefix. This breaks URL generation via url_for(), logging accuracy, and any IP-based security checks.
Add the four required proxy_set_header directives to your nginx server block as documented in docs/deploying/nginx.rst (lines 61-66):
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Prefix /;
Missing ProxyFix Middleware
Even with correctly forwarded headers, Flask (via Werkzeug) ignores them by default for security reasons. Without explicit configuration, request.remote_addr, request.url, and url_for() will return incorrect values based on the internal proxy connection rather than the original client request.
Wrap your WSGI application with werkzeug.middleware.proxy_fix.ProxyFix as implemented in docs/deploying/proxy_fix.rst (lines 23-30):
from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(
app.wsgi_app,
x_for=1, # Trust X-Forwarded-For
x_proto=1, # Trust X-Forwarded-Proto
x_host=1, # Trust X-Forwarded-Host
x_prefix=1 # Trust X-Forwarded-Prefix
)
Incorrect Proxy Trust Levels
Setting the x_for, x_proto, x_host, or x_prefix values too high trusts headers from untrusted clients, creating IP spoofing vulnerabilities. Setting them too low discards legitimate forwarded values, causing redirect loops or broken links.
Count the actual proxy hops between the client and your application. In a standard single-nginx setup, use 1 for each parameter. If you have additional load balancers or CDNs, increment accordingly.
Duplicate Static File Serving
Serving static assets from both nginx and Flask wastes bandwidth and can expose files unintentionally if Flask's static route has different access controls than nginx's filesystem permissions.
Configure a dedicated location /static/ block in nginx to serve files directly from disk with caching headers, and ensure your production deployment does not rely on Flask's app.static_folder handling for performance-critical assets.
Server Name and TLS Termination Issues
Using server_name _; (catch-all) is acceptable for testing, but in production you should specify actual domains to prevent host-header spoofing. Additionally, if you serve HTTPS from nginx but omit proxy_set_header X-Forwarded-Proto $scheme;, Flask will generate HTTP URLs, causing mixed-content warnings or insecure redirects.
Ensure the X-Forwarded-Proto header is set and that ProxyFix is configured with x_proto=1 to trust it.
Firewall and System Blocking
Even with perfect nginx and Flask configurations, operating system firewalls or SELinux policies may block traffic to the internal WSGI port (e.g., 8000). This manifests as connection timeouts or 502 errors despite services running.
Open the port locally using ufw allow 8000 or configure SELinux rules. Alternatively, bind the WSGI server to a Unix socket file with appropriate permissions, eliminating the need for an open TCP port between nginx and your application.
Complete Production Configuration Examples
Minimal Nginx Reverse Proxy Configuration
This configuration mirrors the official example from docs/deploying/nginx.rst (lines 53-66):
server {
listen 80;
server_name example.com; # Replace with your domain
location / {
proxy_pass http://127.0.0.1:8000/;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Prefix /;
}
location /static/ {
alias /path/to/your/app/static/;
expires 30d;
add_header Cache-Control "public, immutable";
}
}
Flask Application with ProxyFix
Implement the middleware as described in docs/deploying/proxy_fix.rst:
# app.py
from flask import Flask, request
from werkzeug.middleware.proxy_fix import ProxyFix
app = Flask(__name__)
# Trust one proxy (nginx) for all forwarded headers
app.wsgi_app = ProxyFix(
app.wsgi_app,
x_for=1,
x_proto=1,
x_host=1,
x_prefix=1
)
@app.route("/")
def hello():
# Now correctly reflects the real client IP
return f"Hello! Your IP is {request.remote_addr}"
Starting the WSGI Server
Bind Gunicorn to the exact address specified in your nginx proxy_pass directive:
gunicorn --bind 127.0.0.1:8000 wsgi:app
Ensure your wsgi.py file exposes the application instance:
from app import app as application
Apply configuration changes without dropping connections:
sudo systemctl reload nginx
Summary
- Match ports exactly: Ensure
proxy_passin nginx points to the same host:port where your WSGI server binds. - Forward headers: Include all four
X-Forwarded-*headers so Flask receives original client information. - Enable ProxyFix: Wrap your WSGI app with
werkzeug.middleware.proxy_fix.ProxyFixand set trust levels to1for a single nginx proxy. - Serve static files once: Let nginx handle
/static/directly and disable Flask's static file serving in production if possible. - Reload services: Run
systemctl reload nginxafter every configuration change to apply updates without downtime.
Frequently Asked Questions
Why does my Flask app return 502 Bad Gateway errors?
A 502 error indicates nginx cannot connect to the upstream WSGI server. Verify that your proxy_pass URL matches the bind address of Gunicorn or uWSGI exactly, including the port number. Check that the WSGI server is actually running and that no firewall rules block the connection between nginx and the application port.
Why is request.remote_addr showing 127.0.0.1 instead of the client IP?
This occurs when X-Forwarded-For headers are not forwarded by nginx or when ProxyFix is not applied to your Flask application. Ensure your nginx configuration includes proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; and that you have wrapped app.wsgi_app with ProxyFix with x_for set to at least 1.
Do I need ProxyFix if I'm not using HTTPS?
Yes. While ProxyFix is essential for correct scheme detection behind HTTPS, you also need it to correctly identify the original client IP, host, and path prefix in any reverse-proxy setup. Without it, request.remote_addr will always show the nginx server's local IP, breaking logging and IP-based rate limiting regardless of the protocol.
Should I use TCP ports or Unix sockets for the nginx-to-WSGI connection?
Both work, but Unix sockets often provide better performance and security for local communication. They eliminate TCP overhead and avoid the need to open firewall ports for internal traffic. If using sockets, set proxy_pass http://unix:/path/to/app.sock; in nginx and bind your WSGI server to the same socket file path with appropriate filesystem permissions.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →