Welcome back to "Getting Started with NGINX Web Server"! You're making great progress on your NGINX journey. In the previous lesson, we set up a basic web server that serves static files. Now, in this second lesson, we'll expand NGINX's capabilities by configuring it as a reverse proxy.
A reverse proxy is one of NGINX's most powerful features, allowing you to route requests to backend applications seamlessly. By the end of this lesson, you'll configure NGINX to forward API requests to a Flask backend while continuing to serve static files. This pattern is fundamental to modern web architecture, where frontend and backend services work together through a unified entry point.
We'll explore the proxy_pass directive, learn about essential proxy headers, and see how NGINX acts as an intermediary between clients and your application server.
Before we dive into configuration, let's understand what a reverse proxy actually does. A reverse proxy sits between clients and backend servers, forwarding client requests to the appropriate backend and returning the backend's response to the client.
Think of it like a receptionist at a large office building. When visitors arrive, they don't go directly to individual offices. Instead, they speak to the receptionist, who directs them to the right department or person. The visitors don't need to know the internal layout or where each person sits; the receptionist handles all the routing.
In web terms, clients make requests to NGINX (the receptionist), and NGINX forwards those requests to backend applications like Flask, Node.js, or Java servers. The client only sees NGINX; they don't need to know about the backend infrastructure.
NGINX as a reverse proxy offers several important advantages for modern web applications:
- Unified entry point: Clients connect to a single server on a standard port, while backends can run on any port.
- Security: Backend servers aren't directly exposed to the internet; NGINX acts as a protective layer.
- Load distribution: NGINX can distribute requests across multiple backend servers (we'll explore this in later courses).
- SSL termination: NGINX can handle HTTPS encryption, letting backends focus on application logic.
- Static file optimization: NGINX excels at serving static files, freeing up application servers for dynamic content.
This architecture separates concerns cleanly, with NGINX handling web traffic efficiently while your application focuses on business logic.
As you may recall from the previous lesson, our NGINX configuration starts with worker processes and the HTTP block. For our reverse proxy setup, we'll use a similar foundation:
We're keeping the same basic structure: one worker process, 1,024 maximum connections, and MIME type support. Our server listens on port 3000 and serves static files from the html directory. This ensures our static content continues to work as before.
Now we'll add a new location block specifically for API requests. This block will intercept requests starting with /api/ and forward them to our backend:
The location /api/ directive creates a path prefix match. Any request starting with /api/ will be handled by this block instead of trying to serve a static file. We'll expand this configuration in the following sections.
The proxy_pass directive is the heart of reverse proxy configuration. It tells NGINX where to forward requests:
Let's break down what's happening here:
http://127.0.0.1:5000is the backend server address (Flask will run on port 5000).- The trailing slash is important: it means NGINX will strip the
/api/prefix before forwarding. - When a client requests
/api/users, NGINX forwards it tohttp://127.0.0.1:5000/users.
Without the trailing slash, /api/ would be preserved in the forwarded request. This flexibility lets us design clean external APIs while keeping backend routes simple.
When NGINX forwards requests, we need to pass along information about the original client. We do this using proxy_set_header directives:
Each directive sets an HTTP header that the backend server will receive. These headers preserve important information that would otherwise be lost when NGINX acts as an intermediary. Let's examine each one:
-
Host: The
Hostheader tells the backend server which domain name the client requested. The$hostvariable contains the hostname from the request line or theHostheader from the client. This is crucial when a single backend serves multiple domains or when the backend needs to generate URLs that match the original request. -
X-Real-IP: This header preserves the client's IP address. Without it, the backend would only see NGINX's IP address (127.0.0.1) as the client. The
$remote_addrvariable contains the actual client's IP address. Backends use this for logging, security decisions, or geolocation features. -
X-Forwarded-For: This header tracks the request's journey through proxies. The special variable
$proxy_add_x_forwarded_forappends the client's IP to any existingX-Forwarded-Forchain. If the request passes through multiple proxies, this header maintains the complete path, which is valuable for security auditing and troubleshooting. -
X-Forwarded-Proto: This header indicates the original protocol. The
$schemevariable contains either or , indicating how the client connected to NGINX. Even if NGINX communicates with the backend over plain HTTP, the backend knows whether the original client connection was secure. This helps backends generate correct redirect URLs and make security decisions.
Let's add back the static file handling to complete our server block:
This location block handles all requests that don't match /api/. The try_files directive attempts to serve the requested file, then a directory index, and finally falls back to index.html. This pattern is common for single-page applications where the frontend handles routing.
Notice how the /api/ location is checked first due to the more specific prefix match, while / catches everything else.
Now we need a backend application for NGINX to proxy to. We'll create a minimal Flask app that listens on port 5000. Save this as app.py:
This simple backend provides two endpoints: a root path that shows the headers it receives and a /users endpoint that returns sample data. The root endpoint is particularly useful for verifying that our proxy headers are being passed correctly.
To test our reverse proxy setup, we need both servers running. Start Flask first in one terminal:
You should see output indicating Flask is running:
Then, in another terminal, validate and start NGINX:
Now we have Flask listening on port 5000 (backend) and NGINX listening on port 3000 (frontend/proxy).
Before testing the proxy, let's verify Flask works directly. Use curl to make a request directly to port 5000:
This bypasses NGINX completely and talks directly to Flask. You should see:
This confirms our Flask backend is working correctly and ready to receive proxied requests.
Now let's access the same endpoint through NGINX's reverse proxy. Make a request to port 3000 with the /api/ prefix:
Even though we're connecting to port 3000, NGINX forwards the request to Flask on port 5000, strips the /api/ prefix, and returns the response:
From the client's perspective, everything appears to come from the NGINX server on port 3000. The backend infrastructure is completely hidden. This is the power of a reverse proxy: a unified interface that seamlessly coordinates multiple services.
To see the proxy headers in action, let's request the root endpoint through the proxy:
The Flask app returns the headers it received:
Notice how Flask sees Host: localhost:3000 (the original request host), and the various X-Forwarded-* headers preserve information about the client connection. This is exactly what we configured with our proxy_set_header directives.
Excellent work! You've successfully configured NGINX as a reverse proxy, bridging static file serving with a dynamic backend application. You've learned how proxy_pass forwards requests, why proxy headers are essential for preserving client information, and how to set up a complete proxied architecture.
This configuration pattern is foundational for modern web applications. Whether you're building a microservices architecture, implementing API gateways, or simply separating frontend and backend concerns, reverse proxying with NGINX provides a robust, performant solution.
In the upcoming practice exercises, you'll apply these concepts hands-on, configuring your own reverse proxy setups and exploring different routing patterns. Time to put your newfound skills into action!
