Skip to content

Latest commit

 

History

History
328 lines (236 loc) · 7.98 KB

File metadata and controls

328 lines (236 loc) · 7.98 KB

Lab: Using Nginx as a Reverse Proxy for a Flask Web App

Overview

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

What is a Proxy?

A proxy is an intermediary that sits between a client and a server.

Forward Proxy

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

Reverse Proxy

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.


Why Use a Reverse Proxy?

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.


Architecture for This Lab

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.

Step 1: Create a Simple Flask App with Jinja2

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.py

Test directly:


Step 2: Install and Run Nginx

Install Nginx if needed:

RHEL:

sudo dnf update
sudo dnf install -y nginx

Start Nginx:

sudo systemctl start nginx

Step 3: Configure Nginx as a Reverse Proxy

Create a new Nginx config file, for example:

sudo vi /etc/nginx/conf.d/flask_proxy.conf

Update 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 nginx

Step 4: Access the App Through the Proxy

Now 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.


Step 5: Observe the Proxy in Action

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.

Running Flask with WSGI (Gunicorn)

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 gunicorn

Run your Flask app with Gunicorn:

gunicorn -w 4 -b 127.0.0.1:5000 app:app
  • -w 4 starts 4 worker processes
  • -b 127.0.0.1:5000 binds to localhost only (for Nginx to proxy)
  • app:app means app.py file, app Flask object

This makes your app production-ready and lets Nginx proxy to it securely.


What About CORS?

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:

That would normally require CORS headers.

Why CORS Is Not an Issue Here

In this setup:

From the browser’s perspective:

  • Everything is coming from the same origin (localhost:8000).

So:

  • ✅ No CORS errors
  • ✅ No browser complaints

If They Were on Different Domains

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.


Why This Setup Is Common

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.

🧩 Your Task

  • 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.

⭐ Challenge

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"))

Key Takeaways

  • 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.