nginx: Reverse proxy
Ciljevi lekcije
Nakon ove lekcije trebalo bi da:
- razumeš razliku između
proxy_passsa URI-jem i bez njega — to je najvažnija stvar u ovoj lekciji - znaš koja zaglavlja moraš da prosleđuješ i zašto
- podesiš tajmaute i baferisanje prema potrebama aplikacije
- omogućiš WebSocket saobraćaj kroz nginx
- prepoznaš uzrok grešaka 502, 504 i 413
1. Šta radi reverse proxy
Forward proxy stoji ispred klijenta i izlazi na internet u njegovo ime. Reverse proxy stoji ispred servera i prima saobraćaj u njegovo ime.
klijent ──▶ nginx :443 ──▶ aplikacija :3000
(javno) (127.0.0.1, nedostupna spolja)
Zašto ne pustiti aplikaciju direktno na port 443? Zato što nginx radi niz stvari koje aplikacija ne bi trebalo da radi:
- TLS terminacija — aplikacija govori običan HTTP, ne mora da zna ništa o sertifikatima
- statika sa diska — CSS i slike ne prolaze kroz aplikacioni proces
- spori klijenti — nginx prihvata zahtev u komadima i prosleđuje ga backend-u tek kad je kompletan, pa aplikacioni worker nije blokiran
- rate limiting, keširanje, kompresija, logovanje — na jednom mestu, van koda
- više aplikacija na jednoj IP adresi i portu
Zadnja tri argumenta važe čak i kada imaš samo jednu aplikaciju.
2. proxy_pass — i zamka sa kosom crtom
Najjednostavniji oblik:
location / {
proxy_pass http://127.0.0.1:3000;
}
Sada dolazi pravilo koje mora da ti uđe u refleks:
Ako
proxy_passsadrži putanju (makar i samo/), prefiks izlocationbloka se zamenjuje tom putanjom. Ako ne sadrži putanju, originalni URI se prosleđuje nepromenjen.
Pogledaj razliku:
location /api/ {
proxy_pass http://127.0.0.1:3000; # BEZ putanje
}
Zahtev /api/korisnici → backend prima /api/korisnici
location /api/ {
proxy_pass http://127.0.0.1:3000/; # SA putanjom (kosa crta)
}
Zahtev /api/korisnici → backend prima /korisnici
Jedan znak menja ponašanje. Prvi oblik koristiš kada aplikacija sama zna za prefiks /api. Drugi kada aplikacija sluša na korenu, a ti je montiraš pod prefiks.
Može i drugačija putanja:
location /stari-api/ {
proxy_pass http://127.0.0.1:3000/v2/;
}
Zahtev /stari-api/korisnici → backend prima /v2/korisnici.
Kada nešto stiže na pogrešnu putanju kod backend-a, ovo je prvo mesto koje proveravaš.
Ograničenje: oblik sa putanjom ne može da se koristi u location bloku definisanom regularnim izrazom, ni unutar imenovanog location-a. Tamo ide samo oblik bez putanje.
Unix socket
Kada backend sluša na socket fajlu umesto na TCP portu:
proxy_pass http://unix:/run/app.sock:/;
Sintaksa je nezgodna: unix: + putanja + : + URI deo. Ta poslednja kosa crta je URI deo i podleže pravilu odozgo.
proxy_pass sa promenljivom
resolver 127.0.0.53 valid=30s;
location /servis/ {
set $backend "http://interni-servis.local:8080";
proxy_pass $backend;
}
Kada proxy_pass sadrži promenljivu, nginx razrešava ime u vreme svakog zahteva, a ne pri startu. To je korisno u kontejnerskim okruženjima gde se IP adrese menjaju, ali zahteva resolver direktivu i malo je sporije. Bez promenljive, nginx razreši ime jednom pri startu i drži ga zauvek — što znači da ti restart backend kontejnera može tiho oboriti proxy.
3. Zaglavlja — šta nginx radi podrazumevano
Ovde je najčešći izvor problema u produkciji. Podrazumevano nginx prema backend-u šalje:
Host: 127.0.0.1:3000 ← adresa backend-a, ne originalni domen!
Connection: close
Posledica: aplikacija ne zna na koji je domen zahtev zaista došao, ne zna IP adresu pravog klijenta, i ne zna da li je originalni zahtev bio HTTPS. Redirekcije koje aplikacija generiše vodiće na 127.0.0.1:3000, korisnici u logovima će svi imati istu IP adresu, a aplikacija koja proverava HTTPS može upasti u petlju preusmeravanja.
Zato uvek prosleđuješ ova zaglavlja:
location / {
proxy_pass http://127.0.0.1:3000;
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;
proxy_set_header X-Forwarded-Host $host;
}
Šta koje radi:
| Zaglavlje | Sadržaj |
|---|---|
Host |
originalni domen iz zahteva klijenta |
X-Real-IP |
IP adresa klijenta |
X-Forwarded-For |
lanac IP adresa; $proxy_add_x_forwarded_for dodaje trenutnog klijenta na postojeći lanac |
X-Forwarded-Proto |
http ili https — po ovome aplikacija zna da je iza TLS terminacije |
X-Forwarded-Host |
originalni domen, za aplikacije koje traže baš ovo ime |
Ubuntu paket ovo već nudi kao snippet:
location / {
proxy_pass http://127.0.0.1:3000;
include proxy_params;
}
Pogledaj šta je unutra sa cat /etc/nginx/proxy_params.
Podsetnik na zamku iz lekcije 3
proxy_set_header se ne spaja kroz nivoe. Ako definišeš zaglavlja u server bloku, a onda u jednom location bloku dodaš samo jedno — sva ostala nestaju za tu putanju:
server {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
location /api/ {
proxy_set_header X-Api-Key "tajna"; # sada Host i X-Real-IP NE idu!
proxy_pass http://backend;
}
}
Rešenje je include proxy_params; na oba mesta.
Prava IP adresa iza CDN-a
Ako je ispred nginx-a još jedan sloj (Cloudflare, load balancer, CDN), $remote_addr je adresa tog sloja, a ne korisnika. Modul realip to ispravlja:
set_real_ip_from 10.0.0.0/8; # opseg u kome je poverljivi proxy
real_ip_header X-Forwarded-For;
real_ip_recursive on;
Nakon ovoga $remote_addr sadrži pravu adresu klijenta i logovi, rate limiting i sve ostalo rade ispravno.
Kritično: set_real_ip_from navodi opsege kojima veruješ. Ako tu upišeš 0.0.0.0/0, bilo ko može da lažira X-Forwarded-For i zaobiđe ti sve što se oslanja na IP adresu. Navodi isključivo konkretne opsege svog infrastrukturnog sloja.
4. Baferisanje
Podrazumevano je proxy_buffering on. nginx prima odgovor od backend-a u svoje bafere, oslobađa backend čim je odgovor preuzet, pa ga zatim isporučuje klijentu svojim tempom.
proxy_buffering on;
proxy_buffer_size 8k; # bafer za zaglavlja odgovora
proxy_buffers 8 16k; # broj i veličina bafera za telo
proxy_busy_buffers_size 32k;
Ovo je gotovo uvek ono što želiš, jer zaštita od sporih klijenata i jeste glavni razlog što reverse proxy postoji.
Kada isključiti: kod odgovora koji teku u realnom vremenu — Server-Sent Events, streaming API-ji, logovi uživo:
location /events {
proxy_pass http://127.0.0.1:3000;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 24h;
}
Bez ovoga nginx zadržava podatke u baferu i klijent ne vidi ništa dok se bafer ne napuni.
Greška koju ćeš sresti: upstream sent too big header while reading response header from upstream u error logu. Znači da zaglavlja odgovora ne staju u proxy_buffer_size — tipično kod aplikacija koje šalju velike cookie-je ili mnogo zaglavlja. Rešenje je povećanje:
proxy_buffer_size 16k;
proxy_buffers 8 16k;
5. Tajmauti i veličina zahteva
proxy_connect_timeout 5s; # koliko čekamo da se konekcija uspostavi
proxy_send_timeout 60s; # pauza između dva upisa ka backend-u
proxy_read_timeout 60s; # pauza između dva čitanja od backend-a
Podrazumevano je 60 sekundi za sve tri. Napomena koja se često pogrešno razume: proxy_read_timeout nije ukupno vreme odgovora, nego maksimalna pauza između dva primljena komada. Odgovor koji stiže u delovima može trajati satima i neće isteći.
proxy_connect_timeout spusti na 3–5 sekundi. Ako se TCP konekcija ka lokalnom backend-u ne uspostavi za pet sekundi, neće ni za šezdeset — bolje da brzo dobiješ grešku nego da gomilaš konekcije koje čekaju.
Za dugotrajne operacije podigni čitanje na konkretnoj putanji, ne globalno:
location /izvestaji/generisi {
proxy_pass http://127.0.0.1:3000;
proxy_read_timeout 300s;
}
client_max_body_size i greška 413
Podrazumevana vrednost je 1 megabajt. Svaki upload veći od toga vraća 413 Request Entity Too Large — i to nginx odbija zahtev, aplikacija ga nikada ne vidi.
client_max_body_size 50m;
Kada korisnik prijavi da ne može da otpremi fajl, ovo je prvi osumnjičeni. Postavi je u server blok ili na konkretnu putanju za upload. Ne postavljaj je preterano visoko globalno — to je i mera zaštite.
Ne zaboravi da i backend ima svoje ograničenje (PHP upload_max_filesize i post_max_size, client_max_body_size ekvivalenti u drugim okruženjima). Oba moraju biti podešena.
6. WebSocket
WebSocket počinje kao običan HTTP zahtev koji traži nadogradnju protokola:
GET /ws HTTP/1.1
Upgrade: websocket
Connection: Upgrade
nginx podrazumevano ne prosleđuje ta zaglavlja, pa nadogradnja ne uspeva. Standardno rešenje koristi map blok:
# u http kontekstu, van server bloka
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
location /ws/ {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
}
Zašto baš ovako:
proxy_http_version 1.1je obavezno — nadogradnja ne postoji u HTTP/1.0mappostavljaConnection: upgradesamo kada klijent traži nadogradnju, aConnection: closeinače. Ako bezuslovno napišešConnection "upgrade", pokvarićeš obične HTTP zahteve na istoj putanji.- WebSocket veze su dugotrajne i tihe, pa podrazumevanih 60 sekundi znači da ti nginx prekida vezu svakog minuta. Zato produženi tajmauti.
Ako aplikacija ima ugrađen ping/pong mehanizam sa intervalom manjim od 60 sekundi, tajmaute ne moraš da diraš.
7. Ostale korisne direktive
Sakrivanje zaglavlja backend-a:
proxy_hide_header X-Powered-By;
proxy_hide_header Server;
Nema razloga da svetu javljaš verziju svog aplikacionog framework-a.
Ispravljanje redirekcija: ako backend vraća Location zaglavlje sa svojom internom adresom:
proxy_redirect http://127.0.0.1:3000/ /;
Podrazumevano proxy_redirect default već pokriva prost slučaj, ali kod složenijih montiranja putanja moraš eksplicitno.
Keep-alive prema backend-u — vidi sledeću lekciju, jer se konfiguriše kroz upstream blok.
8. Dijagnostika
| Greška | Značenje | Gde tražiti |
|---|---|---|
| 502 Bad Gateway | nginx ne može da se poveže ili je dobio neispravan odgovor | backend ne radi, pogrešan port/socket, dozvole nad socket fajlom |
| 504 Gateway Timeout | backend nije odgovorio u roku | proxy_read_timeout, spor upit, zaglavljen backend |
| 413 | telo zahteva preveliko | client_max_body_size |
400 uz upstream sent too big header |
zaglavlja ne staju u bafer | proxy_buffer_size |
Kod 502 uvek prvo proveri da li backend uopšte odgovara, zaobilazeći nginx:
curl -v http://127.0.0.1:3000/
Ako ovo ne radi, problem nije u nginx-u. Ako radi, pogledaj error log:
sudo tail -20 /var/log/nginx/error.log
connect() failed (111: Connection refused) while connecting to upstream
Backend ne sluša na toj adresi.
connect() to unix:/run/app.sock failed (13: Permission denied)
Socket postoji, ali www-data nema pristup. Proveri ls -l /run/app.sock i dozvole nad direktorijumom.
Dodaj vreme odgovora backend-a u log — jednom podešeno, štedi sate:
log_format proxy '$remote_addr - $status "$request" '
'ukupno=$request_time backend=$upstream_response_time '
'upstream=$upstream_addr status=$upstream_status';
access_log /var/log/nginx/proxy.log proxy;
Razlika između $request_time i $upstream_response_time odmah kaže da li usporava aplikacija ili mreža ka klijentu.
Praktična vežba
1. Pokreni prosti backend koji vraća putanju i zaglavlja koja je primio:
mkdir -p ~/test-backend && cd ~/test-backend
cat > server.js <<'EOF'
const http = require('http');
http.createServer((req, res) => {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end(`PUTANJA: ${req.url}\nHOST: ${req.headers.host}\n` +
`X-Real-IP: ${req.headers['x-real-ip'] || '(nema)'}\n` +
`X-Forwarded-Proto: ${req.headers['x-forwarded-proto'] || '(nema)'}\n`);
}).listen(3000, '127.0.0.1');
EOF
node server.js &
Ako nemaš Node: sudo apt install nodejs. Alternativa je python3 -m http.server 3000, ali neće prikazivati zaglavlja.
2. Demonstriraj zamku sa kosom crtom. Napravi dva bloka i uporedi:
server {
listen 80;
server_name proxy.local;
location /bez/ {
proxy_pass http://127.0.0.1:3000;
}
location /sa/ {
proxy_pass http://127.0.0.1:3000/;
}
}
curl -H "Host: proxy.local" http://localhost/bez/korisnici
curl -H "Host: proxy.local" http://localhost/sa/korisnici
Uporedi vrednost PUTANJA u oba odgovora.
3. Vidi šta backend dobija bez prosleđenih zaglavlja. Zahtev ka bilo kom bloku pokazuje HOST: 127.0.0.1:3000 i X-Real-IP: (nema). Sada dodaj include proxy_params; u blok, primeni i ponovi — vrednosti se menjaju.
4. Izazovi 502 i pročitaj log:
kill %1 # ugasi backend
curl -H "Host: proxy.local" http://localhost/bez/
sudo tail -3 /var/log/nginx/error.log
Pronađi poruku Connection refused i tačnu adresu koju je nginx pokušao.
5. Izazovi 413:
node server.js &
dd if=/dev/zero of=/tmp/veliki.bin bs=1M count=5
curl -H "Host: proxy.local" -F "fajl=@/tmp/veliki.bin" http://localhost/bez/upload
Dodaj client_max_body_size 10m; u server blok, primeni i probaj ponovo.
6. Podesi log sa vremenima odgovora kao u primeru iz teksta i pošalji nekoliko zahteva. Uporedi ukupno i backend kolone.
Rezime
proxy_pass http://host;prosleđuje URI nepromenjen.proxy_pass http://host/;zamenjujelocationprefiks. Jedan znak, potpuno drugačije ponašanje.- Bez
proxy_set_header, backend vidiHost: 127.0.0.1:3000i nema pojma ko je klijent ni da li je zahtev bio HTTPS. include proxy_params;je najkraći put do ispravnog skupa zaglavlja — i mora se ponoviti u svakomlocationbloku koji ima sopstveniproxy_set_header.set_real_ip_fromnavodi samo opsege kojima zaista veruješ.- Baferisanje drži uključeno, osim za streaming odgovore.
client_max_body_sizeje 1 MB podrazumevano — to je uzrok većine neuspelih uploada.- WebSocket traži
proxy_http_version 1.1,mapzaConnectionzaglavlje i duže tajmaute. - Kod 502 prvo proveri backend direktno sa
curl, pa tek onda gledaj nginx.
Pitanja za proveru
location /api/ { proxy_pass http://backend:3000/v1/; }— koju putanju backend prima za zahtev/api/korisnici/5?- Aplikacija generiše redirekciju na
http://127.0.0.1:3000/prijava. Šta si propustio da podesiš? - U
serverbloku imaš četiriproxy_set_headerdirektive, a u jednomlocationbloku jednu dodatnu. Šta backend dobija na toj putanji? - Zašto je
map $http_upgrade $connection_upgradebolji odproxy_set_header Connection "upgrade";? - Server-Sent Events stižu klijentu tek na kraju, u jednom komadu. Šta popravljaš?
- Kada je opasno napisati
set_real_ip_from 0.0.0.0/0;? - Kako iz logova razlikuješ sporu aplikaciju od spore mreže ka klijentu?
Sledeća lekcija: load balancing — upstream blok, metodi raspodele, keep-alive konekcije i pasivne provere zdravlja.
Comments
Post a Comment