Nginx reverse proxy i HTTPS ispred lokalnog LLM-a
Do sada je sve slušalo na 127.0.0.1 i to je bila ispravna odluka. Kad servis treba da bude dostupan spolja, između njega i mreže ide proxy koji radi ono što ni Ollama ni vLLM ne umeju: TLS, kontrolu pristupa i ograničavanje broja zahteva.
Ovaj tekst pokriva prvi deo — postavljanje proxy-ja i sertifikata. Autentifikacija i ključevi su tema sledećeg teksta; ova dva idu zajedno i nijedan sam nije dovoljan.
Šta proxy rešava
| Bez proxy-ja | Sa proxy-jem |
|---|---|
| HTTP, sve u čitljivom obliku | TLS sa važećim sertifikatom |
| Nema kontrole pristupa | Ključevi, IP liste, lozinke |
| Nema ograničenja broja zahteva | Rate limiting |
| Nema zapisa ko je šta tražio | Pristupni log |
| Svaki servis na svom portu | Više servisa na jednom imenu |
Postavka izgleda ovako:
Internet → Nginx :443 (TLS) → Ollama :11434 (lokalno)
→ Open WebUI :3000 (lokalno)
→ vLLM :8000 (lokalno)
Priprema
$ sudo apt install -y nginx
$ sudo dnf install -y nginx $ sudo systemctl enable --now nginx
Proveri da servisi ispod stvarno slušaju samo lokalno:
$ sudo ss -tulpn | grep -E '11434|8000|3000' tcp LISTEN 0 4096 127.0.0.1:11434 0.0.0.0:* tcp LISTEN 0 4096 127.0.0.1:3000 0.0.0.0:*
Ako neki od njih stoji na 0.0.0.0, proxy nije zaštita — svako i dalje može zaobići ga i otići pravo na port. Vrati ih na lokalnu adresu pre nego što nastaviš, kako je opisano u tekstu o systemd jedinici.
Za Docker postavke isto važi za mapiranje porta — -p 127.0.0.1:11434:11434, ne -p 11434:11434.
Firewall propušta samo veb portove:
$ sudo ufw allow 80/tcp $ sudo ufw allow 443/tcp
$ sudo firewall-cmd --permanent --add-service=http --add-service=https $ sudo firewall-cmd --reload
Sertifikat
Za javno dostupan server sa imenom domena:
$ sudo apt install -y certbot python3-certbot-nginx $ sudo certbot --nginx -d ai.primer.rs
Certbot sam upisuje TLS deo u konfiguraciju i postavlja obnavljanje. Provera da obnavljanje radi:
$ sudo certbot renew --dry-run $ systemctl list-timers | grep certbot
Za server u lokalnoj mreži, gde Let's Encrypt ne može da potvrdi ime, ide sopstveni sertifikat:
$ sudo openssl req -x509 -nodes -days 730 -newkey rsa:2048 \
-keyout /etc/ssl/private/ai.key \
-out /etc/ssl/certs/ai.crt \
-subj "/CN=ai.lokalno"
Pregledači će prijavljivati upozorenje, a curl traži oznaku -k. Za internu upotrebu je prihvatljivo; bolje rešenje je sopstveni CA čiji sertifikat postaviš na klijentske mašine.
Konfiguracija
$ sudo nano /etc/nginx/conf.d/ai.conf
server {
listen 80;
server_name ai.primer.rs;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
http2 on;
server_name ai.primer.rs;
ssl_certificate /etc/letsencrypt/live/ai.primer.rs/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/ai.primer.rs/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
access_log /var/log/nginx/ai-access.log;
error_log /var/log/nginx/ai-error.log;
client_max_body_size 50M;
location / {
proxy_pass http://127.0.0.1:11434;
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_buffering off;
proxy_cache off;
proxy_http_version 1.1;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
}
}
$ sudo nginx -t $ sudo systemctl reload nginx
Dva reda koja se najčešće izostave
proxy_buffering off; — bez ovoga Nginx skuplja ceo odgovor pre nego što ga prosledi. Streaming prestaje da radi: korisnik gleda u prazan ekran pola minuta, pa dobije sve odjednom. Ovo je najčešća pritužba posle postavljanja proxy-ja.
proxy_read_timeout 600s; — podrazumevanih šezdeset sekundi je premalo. Veći model sa dugim odgovorom prelazi taj rok, veza se prekida, a u logu stoji greška koja izgleda kao pad servisa.
Vrednost client_max_body_size podigni ako korisnici prilažu dokumente kroz Open WebUI — podrazumevani jedan megabajt je premali za PDF.
Više servisa iza jednog imena
server {
listen 443 ssl;
http2 on;
server_name ai.primer.rs;
ssl_certificate /etc/letsencrypt/live/ai.primer.rs/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/ai.primer.rs/privkey.pem;
client_max_body_size 50M;
# Veb interfejs na korenu
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;
# WebSocket, obavezno za Open WebUI
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
proxy_read_timeout 600s;
}
# API na zasebnoj putanji
location /api/ {
proxy_pass http://127.0.0.1:11434/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_buffering off;
proxy_http_version 1.1;
proxy_read_timeout 600s;
}
}
Kosa crta na kraju proxy_pass adrese menja značenje. Sa njom se /api/ odseca pre prosleđivanja, pa zahtev na /api/v1/chat/completions stiže do Ollame kao /v1/chat/completions. Bez kose crte putanja ostaje cela i ništa ne radi — to je klasična greška koja košta pola sata.
$ curl https://ai.primer.rs/api/v1/models
Zasebna imena umesto putanja
Čistije, ako imaš kontrolu nad DNS zapisima:
server {
listen 443 ssl;
server_name chat.primer.rs;
# ... sertifikat ...
location / { proxy_pass http://127.0.0.1:3000; ... }
}
server {
listen 443 ssl;
server_name api.primer.rs;
# ... sertifikat ...
location / { proxy_pass http://127.0.0.1:11434; ... }
}
Prednost je što svaki servis dobija sopstvena pravila, logove i ograničenja, bez petljanja sa putanjama.
Ograničavanje broja zahteva
Model je skup resurs i jedan skript u petlji ume da obori ceo servis. Ograničenje ide u http blok:
$ sudo nano /etc/nginx/nginx.conf
limit_req_zone $binary_remote_addr zone=ai_limit:10m rate=30r/m; limit_conn_zone $binary_remote_addr zone=ai_conn:10m;
Pa u location:
location /api/ {
limit_req zone=ai_limit burst=10 nodelay;
limit_conn ai_conn 3;
proxy_pass http://127.0.0.1:11434/;
...
}
Trideset zahteva u minutu je razumno za pojedinca. Ograničenje istovremenih veza je ovde možda i važnije od broja zahteva — jedan korisnik sa deset otvorenih tokova zauzima celu karticu.
Zahtev preko granice dobija kod 503:
limit_req_status 429;
Provera
$ curl -sv https://ai.primer.rs/api/v1/models 2>&1 | grep -E 'SSL|subject|expire'
Da streaming stvarno teče postepeno:
$ curl -N https://ai.primer.rs/api/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"qwen3:8b","messages":[{"role":"user","content":"Nabroj deset Linux komandi sa objašnjenjem"}],"stream":true}'
Tekst treba da stiže u komadićima. Ako čekaš pa dobiješ sve odjednom, proxy_buffering nije isključen.
Da se port ne vidi spolja:
$ nmap -p 11434,3000,8000 ai.primer.rs
Sva tri treba da budu zatvorena. Ako nisu, ili servis ne sluša lokalno, ili je Docker upisao sopstveno pravilo ispred firewalla — problem opisan u tekstu o Dockeru.
Logovi
$ sudo tail -f /var/log/nginx/ai-access.log
Za API je korisno beležiti i trajanje zahteva:
log_format ai '$remote_addr - $status $request_time "$request"'; access_log /var/log/nginx/ai-access.log ai;
203.0.113.10 - 200 12.483 "POST /api/v1/chat/completions HTTP/1.1"
Vrednost $request_time ovde nosi najviše. Rast tog broja kroz vreme znači da se kartica zasićuje pre nego što se to vidi bilo gde drugde. Prikupljanje ovakvih podataka u vremenske serije tema je teksta o monitoringu.
Rotacija je podešena unapred, ali proveri veličinu ako servis radi neprekidno:
$ du -sh /var/log/nginx/
Praktični scenariji
Scenario 1: streaming ne radi kroz proxy
Dodaj proxy_buffering off;. Ovo je uzrok u gotovo svakom slučaju.
Scenario 2: veza puca posle minuta
upstream timed out (110: Connection timed out)
Podigni proxy_read_timeout na pet do deset minuta.
Scenario 3: Open WebUI se učita, ali razgovor ne radi
Nedostaju zaglavlja Upgrade i Connection. Bez WebSocket podrške interfejs izgleda ispravno, ali ne prenosi poruke.
Scenario 4: 404 na svaki API zahtev
Izostala kosa crta na kraju proxy_pass adrese, pa se putanja ne odseca.
Scenario 5: 502 Bad Gateway
$ sudo systemctl status ollama $ curl http://127.0.0.1:11434
Servis ispod ne radi ili je promenio port. Ako je pod SELinux-om, proveri i da li je proxy-ju uopšte dozvoljeno da otvara mrežne veze:
$ sudo setsebool -P httpd_can_network_connect 1
Scenario 6: port je i dalje dostupan spolja
Servis sluša na 0.0.0.0, ili je Docker mapirao port bez adrese. Proxy tada ne štiti ništa.
Kratka referenca
certbot --nginx -d ime— sertifikat i automatsko obnavljanjeproxy_buffering off;— bez ovoga nema streamingaproxy_read_timeout 600s;— podrazumevanih 60 s je premaloproxy_set_header UpgradeiConnection— WebSocket za Open WebUIproxy_pass http://127.0.0.1:11434/;— kosa crta odseca putanjuclient_max_body_size 50M;— prilaganje dokumenatalimit_req_zone,limit_conn_zone— ograničenja po adresinginx -t— provera pre svakogreloadss -tulpn— servisi ispod moraju biti na 127.0.0.1nmap— provera da portovi nisu vidljivi spolja$request_timeu logu — rani znak zasićenja karticesetsebool -P httpd_can_network_connect 1— SELinux i proxy
Vežba
- Postavi proxy ispred Ollame i potvrdi da API odgovara preko HTTPS-a.
- Namerno izostavi
proxy_buffering offi uporedi ponašanje streaming zahteva. - Dodaj Open WebUI na koren, a API na
/api/, pa proveri obe putanje. - Postavi ograničenje na trideset zahteva u minutu i izazovi ga petljom u bashu.
- Proveri kroz
nmapda portovi servisa nisu dostupni spolja. - Uključi
$request_timeu log i uporedi trajanje kratkog i dugačkog upita.
Sledeći tekst u serijalu: Zaštita API-ja: ključevi, firewall i ograničavanje pristupa
Povezano:
- AI na Linux serveru — pregled celog serijala
- OpenAI-kompatibilan API — prethodni tekst
- Open WebUI — servis kome treba WebSocket podrška
- Ollama kao systemd servis — vezivanje za lokalnu adresu
- firewalld — pravila na nivou sistema
Comments
Post a Comment