In this lab, you will deploy a simple Flask web application that renders HTML using Jinja2 and exposes a basic JSON API endpoint. Then, you will place Nginx in front of it as a reverse proxy.
By the end of this lab, you will understand:
- What a reverse proxy is
- How it differs from a regular (forward) proxy
- Why reverse proxies are commonly used in front of web apps and APIs
- How this setup helps with things like CORS, security, and scalability
A proxy is an intermediary that sits between a client and a server.
A forward proxy sits in front of clients:
Client → Proxy → Internet
- Used to control or monitor outbound traffic
- Common in corporate networks or VPNs
- Example: browser configured to use a proxy to access websites
A reverse proxy sits in front of servers:
Client → Reverse Proxy → Backend Server
- Clients talk to the proxy, not the backend directly
- The proxy forwards requests to one or more backend services
In this lab, Nginx will act as a reverse proxy in front of a Flask app.
Reverse proxies are used in real systems to:
- Hide backend services from direct internet access
- Provide a single entry point for multiple services
- Handle TLS (HTTPS) in one place
- Add headers, logging, and rate limiting
- Improve security and flexibility
- Match how cloud load balancers, API gateways, and Kubernetes ingress work
In production, your Flask app almost never faces the internet directly — something like Nginx, an ALB, or an API gateway does.
Before:
Browser → Flask (localhost:5000)
After:
Browser → Nginx (localhost:443) → Flask (localhost:5000)
- The browser will only talk to Nginx.
- Flask will become an internal service.
Create a new file called app.py:
If your API was previously running with HTTPS, make sure to run it with plain HTTP instead (as shown below). The reverse proxy, Nginx, will handle HTTPS for you.
from flask import Flask, render_template, jsonify
app = Flask(__name__)
@app.route("/")
def index():
return render_template("index.html", message="Hello from Flask behind Nginx!")
@app.route("/api/data")
def data():
return jsonify({
"service": "flask-backend",
"status": "ok",
"message": "Hello from the API"
})
if __name__ == "__main__":
# runs without https (ssl)
app.run(host="127.0.0.1", port=5000, debug=True)Create a folder called templates/ and inside it create index.html:
<!DOCTYPE html>
<html>
<head>
<title>Flask + Nginx Proxy Lab</title>
</head>
<body>
<h1>{{ message }}</h1>
<p>This page is rendered using Jinja2.</p>
<p>API response:</p>
<pre id="api"></pre>
<script>
fetch("/api/data")
.then(r => r.json())
.then(d => {
document.getElementById("api").innerText = JSON.stringify(d, null, 2);
});
</script>
</body>
</html>Run the app:
python app.pyTest directly:
Install Nginx if needed:
RHEL:
sudo dnf update
sudo dnf install -y nginxStart Nginx:
sudo systemctl start nginxCreate a new Nginx config file, for example:
sudo vi /etc/nginx/conf.d/flask_proxy.confUpdate ssl_certificate and ssl_certificate_key to the path of the keys you created for the https lab.
Add:
server {
listen 443 ssl;
server_name localhost;
# Use the cert and key you generated in the HTTPS lab (see 2.3-python-api-https.md)
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://127.0.0.1:5000; # backend Flask app
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Reload Nginx:
sudo nginx -t
sudo systemctl reload nginxNow open:
You should see the same Flask app — but now through Nginx, using HTTPS.
Your Flask app is still running on 127.0.0.1:5000, but users should only access it via https://localhost/ (port 443).
Reminder: Use the self-signed certificate and key you generated in the HTTPS lab (see Scripting-APIS/2.3-python-api-https.md). If you haven't done that lab, follow the instructions there to create cert.pem and key.pem with OpenSSL.
In Flask, temporarily add this to one route:
from flask import request
print("Client IP:", request.headers.get("X-Forwarded-For"))Refresh the page and observe that Flask sees the client IP forwarded by Nginx.
This shows:
- The backend now trusts headers set by the proxy.
In production, Flask should not be run with its built-in server. Instead, use a WSGI server like Gunicorn to handle requests efficiently and securely.
Install Gunicorn in your virtual environment:
pip install gunicornRun your Flask app with Gunicorn:
gunicorn -w 4 -b 127.0.0.1:5000 app:app-w 4starts 4 worker processes-b 127.0.0.1:5000binds to localhost only (for Nginx to proxy)app:appmeansapp.pyfile,appFlask object
This makes your app production-ready and lets Nginx proxy to it securely.
CORS (Cross-Origin Resource Sharing) is a browser security feature that blocks JavaScript from calling APIs on a different origin (domain, scheme, or port).
Example of different origins:
- Frontend: http://localhost:8000
- API: http://localhost:5000
That would normally require CORS headers.
In this setup:
- Browser calls http://localhost:8000/api/data
- Nginx proxies internally to http://127.0.0.1:5000
From the browser’s perspective:
- Everything is coming from the same origin (localhost:8000).
So:
- ✅ No CORS errors
- ✅ No browser complaints
If your frontend were on one domain and the API on another, you would:
- Configure the API to return CORS headers like:
Access-Control-Allow-Origin: https://frontend.example.com
The browser enforces CORS, but the API decides what is allowed.
This pattern is widely used because:
- Backends stay private
- One entry point for web + APIs
- Easier TLS and cert management
- Central place for:
- logging
- rate limiting
- auth
- routing
- Matches how cloud load balancers and API gateways work
- Makes scaling easier later
- In Kubernetes, this role is played by an Ingress controller.
- Get the Flask app running on 127.0.0.1:5000.
- Configure Nginx to proxy traffic from http://localhost:8000 to Flask.
- Verify:
- HTML page loads through Nginx
- /api/data works through Nginx
- Confirm you are no longer directly using :5000 in your browser.
Extend this setup to your existing project:
- Put Nginx in front of your current Flask app.
- Make Flask listen only on localhost wth WSGI.
- Access:
- your Jinja2 pages
- your API endpoints only through Nginx.
- (Optional) Add a custom header in Nginx and print it in Flask.
Example:
proxy_set_header X-Proxy-Lab true;
Then in Flask:
print(request.headers.get("X-Proxy-Lab"))- A reverse proxy sits in front of servers, not clients.
- Clients talk to the proxy, which forwards requests internally.
- Reverse proxies:
- hide backend services
- simplify security
- avoid CORS issues when using same origin
- This is how most real web apps and APIs are deployed.