nginx: Serviranje statičkog sadržaja
Ciljevi lekcije
Nakon ove lekcije trebalo bi da:
- razumeš razliku između
rootialiasi znaš kada koji koristiš - vladaš direktivom
try_filesu svim njenim oblicima - umeš da podesiš
index,autoindexi MIME tipove - postaviš prilagođene stranice grešaka
- dijagnostikuješ 403 i 404 sistematski, umesto pogađanjem
1. root — spajanje putanje
root određuje osnovni direktorijum. nginx sastavlja putanju do fajla prostim nadovezivanjem:
putanja_do_fajla = root + uri
server {
root /var/www/example.com/html;
location /slike/ {
# root se nasleđuje
}
}
| Zahtev | Fajl na disku |
|---|---|
/ |
/var/www/example.com/html/ → traži se index |
/o-nama.html |
/var/www/example.com/html/o-nama.html |
/slike/logo.png |
/var/www/example.com/html/slike/logo.png |
Primeti da se prefiks iz location bloka zadržava u putanji. /slike/ iz zahteva postaje slike/ na disku.
root možeš da nadjačaš u pojedinačnom location bloku:
server {
root /var/www/example.com/html;
location /preuzimanja/ {
root /srv/podaci; # fajl: /srv/podaci/preuzimanja/...
}
}
Obrati pažnju: putanja je /srv/podaci/preuzimanja/, a ne /srv/podaci/. root uvek dodaje ceo URI.
2. alias — zamena prefiksa
alias radi drugačije. On zamenjuje deo URI-ja koji se poklopio sa location prefiksom:
putanja_do_fajla = alias + (uri bez location prefiksa)
location /preuzimanja/ {
alias /srv/podaci/;
}
| Zahtev | Fajl na disku |
|---|---|
/preuzimanja/spisak.pdf |
/srv/podaci/spisak.pdf |
Prefiks /preuzimanja/ je nestao iz putanje. To je cela razlika: root dodaje, alias zamenjuje.
Pravilo kose crte
Ovo je najčešća greška sa alias-om i vredi je zapamtiti kao mehaničko pravilo:
Ako
locationzavršava kosom crtom,aliasmora da završava kosom crtom.
Netačno:
location /static/ {
alias /var/www/assets; # ← nedostaje kosa crta
}
Zahtev /static/css/app.css daje /var/www/assets + css/app.css = /var/www/assetscss/app.css. Fajl očigledno ne postoji, dobijaš 404, a putanja u error logu izgleda kao nešto što nikad nisi napisao.
Tačno:
location /static/ {
alias /var/www/assets/;
}
Sigurnosna zamka: location bez kose crte uz alias sa kosom crtom
Ova kombinacija nije samo greška — ona je poznata ranjivost:
location /files {
alias /var/www/files/;
}
Zahtev /files../etc/passwd daje /var/www/files/ + ../etc/passwd = /var/www/files/../etc/passwd = /var/www/etc/passwd. Napadač je izašao jedan nivo iznad predviđenog direktorijuma, a normalizacija URI-ja to nije sprečila jer se .. nalazi posle tačke spajanja.
Pravilo je jednostavno: kod alias-a obe strane imaju kosu crtu, ili nijedna. U praksi — uvek obe.
Kada koristiti šta
Ako je struktura na disku ista kao u URL-u, koristi root. Ako se razlikuje, koristi alias.
Ova dva bloka rade isto:
location /static/ {
alias /var/www/example.com/static/;
}
location /static/ {
root /var/www/example.com;
}
U tom slučaju root je bolji izbor: kraći je, nema zamku sa kosom crtom, i nema problema sa try_files.
alias i try_files se ne slažu najbolje. Kombinacija ima poznate probleme sa razrešavanjem putanje i nije preporučena. Ako ti treba try_files, prilagodi strukturu direktorijuma tako da možeš koristiti root.
alias u regex bloku zahteva da eksplicitno navedeš šta se zamenjuje, kroz zahvaćene grupe:
location ~ ^/slicice/(.+\.(?:jpg|png))$ {
alias /var/cache/slicice/$1;
}
3. try_files u detalje
try_files proverava listu putanja redom i koristi prvu koja postoji.
location / {
try_files $uri $uri/ =404;
}
Redom: postoji li fajl sa tim imenom → postoji li direktorijum sa tim imenom → ako ništa, vrati 404.
Poslednji argument je poseban. On nije provera nego rezervni ishod, i može biti:
| Oblik | Ponašanje |
|---|---|
=404 |
vrati statusni kod |
/index.html |
interno preusmeri na taj URI |
@ime |
skoči na imenovani location |
Kada poslednji argument nije statusni kod, dolazi do internog preusmeravanja — nginx uzima taj URI i ponovo prolazi kroz izbor location bloka. To nije HTTP redirect; klijent ništa ne vidi.
Tri obrasca koja ćeš koristiti stalno
Statički sajt:
location / {
try_files $uri $uri/ =404;
}
Single-page aplikacija (React, Vue, Angular) — ruting se dešava u browseru, pa sve nepostojeće putanje treba da vrate index.html:
location / {
try_files $uri $uri/ /index.html;
}
Aplikacija iza proxy-ja — statiku serviraj sa diska, ostalo prosledi:
location / {
try_files $uri $uri/ @app;
}
location @app {
proxy_pass http://127.0.0.1:3000;
}
Zamka sa beskonačnom petljom
location / {
try_files $uri $uri/ /index.html;
}
Ako index.html ne postoji, interno preusmeravanje na /index.html ponovo pada u isti location, koji ponovo preusmerava... nginx prekida posle deset ciklusa i vraća 500, uz rewrite or internal redirection cycle u error logu. Kada vidiš tu poruku, znaš gde da gledaš.
$uri/ i zašto je bitna kosa crta
Kada try_files uspešno nađe $uri/ kao direktorijum, dalje se primenjuje index direktiva. Ako izostaviš $uri/ iz liste, zahtev za /blog/ neće naći index.html unutar tog direktorijuma.
4. index i autoindex
index
index index.html index.htm;
Lista fajlova koji se traže kada URI pokazuje na direktorijum. Prvi pronađeni se servira. Ako nijedan ne postoji, rezultat je 403 Forbidden — ne 404. Ta razlika je važna za dijagnostiku: 403 na putanji koja se završava kosom crtom skoro uvek znači „direktorijum postoji, ali nema index fajla".
Poslednji element može biti apsolutni URI:
index index.html /pocetna.php;
autoindex
Kada želiš da nginx generiše spisak fajlova umesto index stranice:
location /preuzimanja/ {
autoindex on;
autoindex_exact_size off; # "1.2M" umesto "1258291"
autoindex_localtime on; # lokalno vreme umesto UTC
}
Korisno za interne repozitorijume fajlova, mirror-e i slično.
Budi svestan šta radiš: autoindex on izlaže kompletan spisak svega u direktorijumu, uključujući stvari koje si zaboravio da si ostavio tamo. Nikada ga ne uključuj globalno, samo na konkretnim putanjama, i najbolje uz neko ograničenje pristupa.
5. MIME tipovi
nginx određuje Content-Type zaglavlje na osnovu ekstenzije fajla, koristeći mapu iz /etc/nginx/mime.types.
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
}
default_type je ono što se šalje kada ekstenzija nije u mapi. Vrednost application/octet-stream znači „binarni podaci" i browser će takav fajl ponuditi na preuzimanje umesto da ga prikaže.
Kada browser preuzima CSS umesto da ga primeni, ili prikazuje JavaScript kao tekst, uzrok je gotovo uvek ovde. Proveri šta server zapravo šalje:
curl -sI http://localhost/css/app.css | grep -i content-type
Dodavanje sopstvenog tipa, bez diranja mime.types:
http {
include /etc/nginx/mime.types;
types {
application/manifest+json webmanifest;
font/woff2 woff2;
}
}
Blok types na ovom mestu dopunjuje ono što je učitano iz mime.types. Prvo proveri da li ti tip uopšte nedostaje:
grep -i woff2 /etc/nginx/mime.types
Kodni raspored
Za tekstualne tipove vredi eksplicitno navesti kodiranje:
charset utf-8;
Ovo dodaje ; charset=utf-8 na Content-Type tekstualnih odgovora. Bez toga, stranice bez <meta charset> oznake mogu da se prikažu sa pokvarenim slovima š, đ, č, ć, ž.
6. Prilagođene stranice grešaka
server {
root /var/www/example.com/html;
error_page 404 /greske/404.html;
error_page 500 502 503 504 /greske/50x.html;
location ^~ /greske/ {
internal;
}
}
Direktiva internal znači da se toj putanji ne može pristupiti direktno spolja — samo kroz interno preusmeravanje. Bez nje bi korisnik mogao da otvori /greske/404.html i dobije stranicu greške sa statusom 200, što zbunjuje i korisnike i pretraživače.
Primeti ^~ — bez njega bi neki regex blok mogao da presretne ovu putanju. To je pravilo iz prethodne lekcije u praksi.
Ako želiš da promeniš i statusni kod:
error_page 404 =200 /greske/404.html; # vraća 200 umesto 404
To se ponekad radi za SPA aplikacije, ali oprezno — pretraživači tada indeksiraju stranice grešaka kao valjan sadržaj.
7. Optimizacija serviranja fajlova
Nekoliko direktiva koje se tiču statike (tuningu se detaljno vraćamo kasnije):
http {
sendfile on; # kopiranje fajl→socket u kernelu, bez prolaska kroz nginx
tcp_nopush on; # slanje punih paketa, ima smisla samo uz sendfile
tcp_nodelay on; # bez odlaganja za keep-alive konekcije
}
Keš otvorenih fajlova — nginx pamti deskriptore i rezultate stat() poziva, pa ne pita kernel za svaki zahtev:
http {
open_file_cache max=10000 inactive=60s;
open_file_cache_valid 60s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
}
Ovo osetno pomaže sajtovima sa mnogo malih fajlova. Cena je što izmene na disku postaju vidljive tek nakon isteka open_file_cache_valid — na razvojnoj mašini ovo isključi, inače ćeš se pitati zašto ne vidiš svoje izmene.
8. Zaštita skrivenih fajlova
Podrazumevano, nginx servira sve što nađe — uključujući .git, .env, .htaccess i rezervne kopije koje je urednik ostavio.
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
location ~ ~$ {
deny all;
}
Prvi blok blokira sve putanje koje sadrže segment koji počinje tačkom. Drugi hvata fajlove sa tildom na kraju (config.php~), koje ostavljaju neki editori.
Proveri odmah da li ti je .git izložen — to je jedan od najčešćih načina da izvorni kod procuri:
curl -I http://localhost/.git/config
Ako ovo vraća 200, imaš problem koji treba rešiti pre bilo čega drugog.
9. Sistematska dijagnostika 403 i 404
Kada nešto ne radi, umesto probanja nasumičnih izmena, prati redosled.
Korak 1 — pogledaj šta piše u error logu
sudo tail -20 /var/log/nginx/example.com.error.log
nginx u log upisuje tačnu putanju do fajla koji je pokušao da otvori. To je najvredniji podatak koji imaš:
2026/08/29 14:22:11 [error] 1234#1234: *5 open() "/var/www/assetscss/app.css"
failed (2: No such file or directory), client: 192.0.2.5, ...
Ako putanja izgleda čudno — kao ova gore — problem je u root ili alias konfiguraciji, a ne u fajlu.
Korak 2 — razlikuj uzroke po kodu
| Kod | Tipičan uzrok |
|---|---|
| 404 | fajl ne postoji na putanji koju je nginx sastavio |
403 uz Permission denied u logu |
dozvole |
403 na putanji sa / na kraju |
direktorijum postoji, ali nema index fajla i autoindex je isključen |
403 uz directory index of ... is forbidden |
isto kao gore, eksplicitna poruka |
500 uz rewrite or internal redirection cycle |
petlja u try_files ili rewrite |
Korak 3 — proveri putanju iz loga
ls -la /putanja/iz/loga
namei -l /putanja/iz/loga
namei -l prikazuje dozvole za svaki nivo putanje. Podsetnik iz lekcije 4: www-data mora imati x nad svakim direktorijumom do fajla, i r nad samim fajlom.
Korak 4 — proveri iz ugla samog nginx-a
sudo -u www-data cat /var/www/example.com/html/index.html
Ako ovo ne prolazi, ni nginx ne može. Ako prolazi, a nginx i dalje vraća 403, sledeći osumnjičeni je AppArmor:
sudo dmesg | grep -i apparmor | tail
Korak 5 — proveri da li je uopšte taj blok aktivan
Vrati se na tehniku iz prethodne lekcije:
add_header X-Match "ime-bloka" always;
Nije retko da se ispostavi kako zahtev uopšte ne stiže tamo gde misliš.
Praktična vežba
1. Demonstriraj razliku root i alias. Napravi strukturu i konfiguraciju:
sudo mkdir -p /var/www/test/{html,assets}
echo "iz html" | sudo tee /var/www/test/html/index.html
echo "iz assets" | sudo tee /var/www/test/assets/proba.txt
server {
listen 80;
server_name test.local;
root /var/www/test/html;
location /r/ {
root /var/www/test;
}
location /a/ {
alias /var/www/test/assets/;
}
}
Predvidi, pa proveri:
curl -H "Host: test.local" http://localhost/a/proba.txt
curl -H "Host: test.local" http://localhost/r/proba.txt
sudo tail -3 /var/log/nginx/error.log
Iz error loga pročitaj koju je putanju nginx sastavio u drugom slučaju i objasni zašto.
2. Reprodukuj grešku sa kosom crtom. Izbaci kosu crtu iz alias /var/www/test/assets; i pošalji isti zahtev. Pronađi u error logu spojenu putanju — trebalo bi da izgleda besmisleno. To je oblik greške koji ćeš prepoznati kad ti se desi na pravom serveru.
3. Testiraj try_files obrasce. Napravi blok sa try_files $uri $uri/ /index.html;, pa zatraži nepostojeću putanju:
curl -H "Host: test.local" http://localhost/ne-postoji/nigde
Zatim obriši index.html i ponovi zahtev. Pročitaj šta piše u error logu — trebalo bi da vidiš poruku o ciklusu preusmeravanja.
4. Podesi autoindex i pogledaj šta izlaže:
location /a/ {
alias /var/www/test/assets/;
autoindex on;
autoindex_exact_size off;
}
curl -H "Host: test.local" http://localhost/a/
5. Proveri MIME tipove. Napravi fajl sa nepoznatom ekstenzijom i vidi šta server šalje:
echo "podaci" | sudo tee /var/www/test/html/proba.nesto
curl -sI -H "Host: test.local" http://localhost/proba.nesto | grep -i content-type
Zatim dodaj tip kroz types { } blok, primeni i proveri ponovo.
6. Napravi prilagođenu stranicu za 404 sa internal direktivom. Uveri se da:
curl -s -H "Host: test.local" http://localhost/ne-postoji # tvoja stranica
curl -sI -H "Host: test.local" http://localhost/greske/404.html # 404, ne 200
Drugi zahtev mora da vrati grešku — to znači da internal radi.
7. Proveri da skriveni fajlovi nisu izloženi:
sudo mkdir -p /var/www/test/html/.git
echo "tajna" | sudo tee /var/www/test/html/.git/config
curl -sI -H "Host: test.local" http://localhost/.git/config
Ako dobiješ 200, dodaj blok location ~ /\. { deny all; } i proveri ponovo. Obriši testni direktorijum kad završiš.
Rezime
rootdodaje URI na putanju;aliaszamenjuje poklopljeni prefiks.- Kod
alias-a: ili obe strane imaju kosu crtu na kraju, ili nijedna. Mešanje daje ili pokvarene putanje ili ranjivost na izlazak iz direktorijuma. - Kad god možeš, koristi
root— nema zamki i lepše se slaže satry_files. try_filesproverava putanje redom; poslednji argument je rezervni ishod, ne provera.- Nedostatak
indexfajla daje 403, ne 404. - MIME tipovi se određuju po ekstenziji;
default_type application/octet-streamznači da će browser fajl preuzeti umesto prikazati. error_pageuzinternalsprečava direktan pristup stranicama grešaka.- Dijagnostika ide istim redom svaki put: error log → putanja koju je nginx sastavio →
namei -l→ test kaowww-data. - Blokiraj skrivene fajlove i proveri da
.gitnije izložen.
Pitanja za proveru
location /docs/ { root /srv/files; }— gde nginx traži fajl za zahtev/docs/uputstvo.pdf?- Isto pitanje, ali sa
alias /srv/files;. U čemu je razlika? - Zašto je
location /files { alias /var/www/files/; }sigurnosni problem? - Šta radi poslednji argument u
try_files $uri $uri/ @app;i po čemu se razlikuje od prethodnih? - Sajt vraća 403 na
/blog/, a direktorijum postoji i dozvole su ispravne. Šta je najverovatniji uzrok? - Browser preuzima
.cssfajl umesto da ga primeni. Odakle počinješ dijagnostiku? - Šta znači
internaluzlocationblok za stranice grešaka i šta se dešava ako je izostaviš? - U error logu vidiš
rewrite or internal redirection cycle. Šta je uzrok?
Sledeća lekcija: reverse proxy — proxy_pass, prosleđivanje zaglavlja, X-Forwarded-* i podrška za WebSocket.
Comments
Post a Comment