A proxy server is fundamentally a middleman, but its true power lies in how it can be instructed to act like the client, the server, or both, all while observing and manipulating the traffic in between.
Let’s see this in action. Imagine a simple web server and a client wanting to access it.
[Client] <---- HTTP GET /index.html ----> [Web Server]
Now, let’s introduce a forward proxy. The client is configured to send its requests to the forward proxy.
[Client] <---- HTTP GET /index.html ----> [Forward Proxy] <---- HTTP GET http://your-website.com/index.html ----> [Web Server]
Notice how the forward proxy rewrites the request. It’s no longer just asking for /index.html; it’s asking for http://your-website.com/index.html. The forward proxy is acting on behalf of the client.
Here’s a typical Nginx configuration for a forward proxy:
# /etc/nginx/nginx.conf or a conf.d file
http {
# ... other http settings ...
resolver 8.8.8.8 8.8.4.4 valid=30s; # DNS resolver for upstream lookups
server {
listen 8080; # The port the proxy listens on
server_name proxy.your-domain.com;
location / {
proxy_pass http://$http_host$request_uri; # Passes request to the resolved upstream host
proxy_set_header Host $http_host; # Forwards the original Host header
proxy_set_header X-Real-IP $remote_addr; # Forwards the client's real IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # Appends to the list of IPs
proxy_set_header X-Forwarded-Proto $scheme; # Forwards the original scheme (http/https)
# For basic authentication if needed
# auth_basic "Restricted Content";
# auth_basic_user_file /etc/nginx/htpasswd.proxy;
}
}
}
In this setup, clients on your internal network would configure their browsers to use proxy.your-domain.com:8080. When a client requests http://internal-app.local/data, the Nginx proxy receives it. The proxy_pass http://$http_host$request_uri; directive takes the $http_host (which is internal-app.local in this case, from the client’s Host header) and the $request_uri (/data) to construct the full upstream URL. The resolver directive is crucial here; it tells Nginx how to dynamically resolve internal-app.local to an IP address. The proxy_set_header directives ensure that the backend server knows who the original client was and what host they intended to reach.
Now, for a reverse proxy, the setup is inverted. The reverse proxy sits in front of one or more web servers, and clients access the reverse proxy directly, unaware of the backend servers.
[Client] <---- HTTP GET / ----> [Reverse Proxy] <----+---- HTTP GET / ----> [Web Server 1]
|
+---- HTTP GET / ----> [Web Server 2]
Here, the reverse proxy acts on behalf of the server. It receives requests from external clients and decides which internal server should handle them.
A common Nginx reverse proxy configuration looks like this:
# /etc/nginx/nginx.conf or a conf.d file
http {
# ... other http settings ...
upstream backend_servers {
server 192.168.1.100:80; # Web Server 1
server 192.168.1.101:80; # Web Server 2
# Least_conn; # Example load balancing method
}
server {
listen 80; # The port the reverse proxy listens on for clients
server_name your-website.com;
location / {
proxy_pass http://backend_servers; # Passes request to the upstream group
proxy_set_header Host $host; # Forwards the original Host header
proxy_set_header X-Real-IP $remote_addr; # Forwards the client's real IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}
In this scenario, clients access your-website.com on port 80. The Nginx reverse proxy receives the request. The upstream backend_servers block defines a pool of internal servers. proxy_pass http://backend_servers; tells Nginx to forward the request to one of the servers in that pool, using a load balancing algorithm (default is round-robin). The proxy_set_header Host $host; is critical here; it ensures that the backend server receives the Host header that the client originally sent (e.g., your-website.com), which is necessary for virtual hosting on the backend.
The key difference in how proxy_set_header Host behaves is revealing. For a forward proxy, you often want to forward the Host header as received from the client to the upstream, because the upstream might be an internal hostname. For a reverse proxy, you typically want to forward the Host header as received from the client to the backend servers so they know which virtual host is being requested.
When using proxy_pass with a named upstream like http://backend_servers, Nginx uses the IP addresses defined in the upstream block directly. However, if you proxy_pass to a domain name (e.g., proxy_pass http://app-service.internal;), Nginx will perform DNS resolution for app-service.internal using the configured resolver directive, much like a forward proxy does. This is how a reverse proxy can dynamically discover and route to backend services.
The magic of proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; is that it appends the current client’s IP address to any existing X-Forwarded-For header. If the request has already been proxied through multiple layers, this header becomes a comma-separated list of IP addresses, allowing the final backend server to trace the original client through the entire chain of proxies.
The choice between forward and reverse proxies often comes down to who the proxy is serving. A forward proxy serves the client, acting as an intermediary for outbound requests. A reverse proxy serves the server, acting as a gateway for inbound requests.
The most surprising thing is that Nginx, when configured as a reverse proxy, can dynamically discover backend services using DNS resolution if you proxy_pass to a hostname rather than a static IP address, effectively turning it into a rudimentary service registry.
The next step is understanding how to leverage these proxy capabilities for more advanced patterns like caching, SSL termination, and request modification.