nginx: Serviranje statičkog sadržaja

Ciljevi lekcije

Nakon ove lekcije trebalo bi da:

  • razumeš razliku između root i alias i znaš kada koji koristiš
  • vladaš direktivom try_files u svim njenim oblicima
  • umeš da podesiš index, autoindex i 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 location završava kosom crtom, alias mora 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

  • root dodaje URI na putanju; alias zamenjuje 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 sa try_files.
  • try_files proverava putanje redom; poslednji argument je rezervni ishod, ne provera.
  • Nedostatak index fajla daje 403, ne 404.
  • MIME tipovi se određuju po ekstenziji; default_type application/octet-stream znači da će browser fajl preuzeti umesto prikazati.
  • error_page uz internal sprečava direktan pristup stranicama grešaka.
  • Dijagnostika ide istim redom svaki put: error log → putanja koju je nginx sastavio → namei -l → test kao www-data.
  • Blokiraj skrivene fajlove i proveri da .git nije izložen.

Pitanja za proveru

  1. location /docs/ { root /srv/files; } — gde nginx traži fajl za zahtev /docs/uputstvo.pdf?
  2. Isto pitanje, ali sa alias /srv/files;. U čemu je razlika?
  3. Zašto je location /files { alias /var/www/files/; } sigurnosni problem?
  4. Šta radi poslednji argument u try_files $uri $uri/ @app; i po čemu se razlikuje od prethodnih?
  5. Sajt vraća 403 na /blog/, a direktorijum postoji i dozvole su ispravne. Šta je najverovatniji uzrok?
  6. Browser preuzima .css fajl umesto da ga primeni. Odakle počinješ dijagnostiku?
  7. Šta znači internal uz location blok za stranice grešaka i šta se dešava ako je izostaviš?
  8. 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

Popular posts from this blog

Početak u Linuxu — šta je i zašto se uči

Konverzija tipova podataka u Pythonu

groupadd