Reverse proxying
Scope: proxy_pass — the directive most nginx
configs exist for — the headers that make it honest,
and what failure looks like.
One directive
location /app/ {
proxy_pass http://127.0.0.1:9001/;
}
Whatever arrives under /app/ is forwarded to the
program on port 9001, and the response relayed back.
The trailing slash on both sides matters more than it
looks: with it, /app/users reaches the backend as
/users (the location prefix is replaced); without
matching slashes the URI passes through whole, and
your backend sees paths you didn't intend.
What the backend receives
The lab's backend echoes exactly what arrives — method, path, headers — so nothing here is assumed. First, the control: talking to the backend directly, with no proxy at all:
$ curl -s http://127.0.0.1:9001/
backend port: 9001
request: GET /
client address: 127.0.0.1:44326
headers received:
Host: 127.0.0.1:9001
User-Agent: curl/8.5.0
Accept: */*
Now through nginx, first with proxy_pass alone:
$ curl -s http://127.0.0.1:8080/app-raw/
headers received:
Host: 127.0.0.1:9001
Connection: close
User-Agent: curl/8.5.0
Accept: */*
The backend has no idea the visitor existed: the
connection came from nginx (127.0.0.1), and the Host
header is the upstream's address — not the name the
visitor asked for. Its own log is honest about the only
address it saw:
127.0.0.1 - "GET /" xff=-
Every request the app serves looks like one very busy localhost. Logs, rate limits, IP-based rules — all see nginx. The fix is a header block inside the location (set it in the location itself; remember chapter 3's wholesale-replacement rule):
location /app/ {
proxy_pass http://127.0.0.1:9001/;
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;
}
$ curl -s http://127.0.0.1:8080/app/
headers received:
Host: 127.0.0.1
X-Real-IP: 127.0.0.1
X-Forwarded-For: 127.0.0.1
X-Forwarded-Proto: http
...
127.0.0.1 - "GET /" xff=127.0.0.1
Host— the name the visitor used ($host; note nginx drops the port here —127.0.0.1, not127.0.0.1:8080). Without it, the app builds wrong links and redirects.X-Forwarded-For— the visitor's IP, appended through each proxy hop ($proxy_add_x_forwarded_forkeeps any existing chain instead of overwriting it).X-Real-IP— the same address, one step, for simple apps.X-Forwarded-Proto—httporhttpsas the visitor saw it, so the app can answer "you arrived at https://…" even though the nginx→app hop is plain http on localhost.
This is a convention, not a standard: the app must trust these headers. Most frameworks have an explicit setting for it ("trust proxy" in most of them), and it is off by default — deliberately, because a header is just text, and trusting text from strangers is how apps get fooled. Point nginx at an app, see wrong URLs or useless IPs in its logs, and you know exactly which setting to look for.
When the app is down: 502
Stop the backend — or point at a port where nothing listens — and the visitor gets:
$ curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8080/dead/
502
502 is nginx saying "I'm fine; the app isn't answering." It assigns blame correctly, and the error log names the cause in full:
[error] ... connect() failed (111: Connection refused) while connecting to upstream, client: 127.0.0.1, server: localhost, request: "GET /dead/ HTTP/1.1", upstream: "http://127.0.0.1:9999/", host: "127.0.0.1:8080"
connect() failed (111: Connection refused) while
connecting to upstream, naming the upstream address. A
502 is never "your nginx config is wrong" — it means
the config is right and the thing behind it needs
attention. (A 404 or a garbled page means the app
answered — then nginx is innocent.) Chapter 13
walks the common failures one by one.
Check: proxy an app you run, hit it, and read one access line for the proxy and one line from the app's own log. The two lines should be telling the same story — and now you know why they don't unless the headers are set.