Dealownia
Menu
sprawdzono – jak sprawdzamy oferty

nginx jako reverse proxy dla aplikacji w Dockerze

Jeden plik nginx na usługę, wspólne nagłówki, WebSocket, limity rozmiaru i zapytań, HTTP/2 i kompresja oraz skąd biorą się błędy 502, 504 i 413.

Serwer i VPSzaktualizowano 7 min czytania

Redakcja serwisu Dealownia · ceny i fakty ze stron dostawców, jak sprawdzamy

Kilka aplikacji w kontenerach za jednym nginx na hoście, każda z HTTPS: osobny plik w sites-available dla każdej usługi, wspólne ustawienia w jednym fragmencie dołączanym wszędzie, działający WebSocket oraz świadomie ustawione limity i kompresja. Na końcu jest lista błędów 502, 504 i 413 z przyczynami, bo to one zabierają najwięcej czasu. Ten układ – nginx na hoście, kontenery tylko na 127.0.0.1 – pozwala dodać nową usługę w kilka minut.

Czego potrzebujesz

  • VPS z Ubuntu 24.04 LTS albo Debianem 12 lub 13, z nginx z pakietów systemowych, Dockerem i certbotem jak w poradniku od zera.
  • Minimum: najmniejszy plan, 1 vCPU i 1 GB RAM. Sam nginx zużywa niewiele; o rozmiarze serwera decydują aplikacje za nim.
  • Subdomenę z rekordem A dla każdej usługi, np. app.example.pl.
  • Wersję nginx, bo od niej zależy składnia HTTP/2: nginx -v. Ubuntu 24.04 ma 1.24, Debian 12 – 1.22, Debian 13 – 1.26.

Krok 1. Kontener tylko na pętli zwrotnej

W compose.yaml aplikacji publikuj port wyłącznie na 127.0.0.1:

yaml
services:
  app:
    image: przyklad/aplikacja:1.0
    ports:
      - "127.0.0.1:3000:3000"

Docker dopisuje własne reguły iptables i według swojej dokumentacji opublikowane porty omijają ufw. "3000:3000" wystawiłoby aplikację na świat bez HTTPS, nawet przy zamkniętej zaporze. Każda usługa dostaje inny port na hoście; numery zapisuj w jednym miejscu, np. w komentarzu w pliku nginx. Przykład z portem 3000 jest ogólny. Poradniki w serwisie używają poniższych portów, żeby kilka usług mogło działać na jednym VPS-ie bez kolizji – zmień port w poleceniu, jeśli Twój jest zajęty:

Port Usługa
2283 Immich, na VPS-ie tylko 127.0.0.1
3000 Umami, także Umami w stacku Docker Compose (poradnik o Umami)
3001 Uptime Kuma
3002 Forgejo
3003 Open WebUI
5678 n8n
8000 GlitchTip
8001 Vaultwarden
8006 Proxmox (panel na własnej maszynie, nie w Dockerze)
8080 strona ze stacku Docker Compose
8088 panel Nextcloud AIO (Nextcloud)
9000 listmonk
11000 Nextcloud, Apache z AIO
11434 Ollama, tylko 127.0.0.1 (poradnik)
51820/udp WireGuard – to nie usługa za nginx, port otwiera się w ufw (poradnik)

Sprawdź, że aplikacja odpowiada lokalnie, zanim dotkniesz nginx:

bash
curl -sI http://127.0.0.1:3000 | head -1

Krok 2. Wspólne ustawienia w jednym pliku

Nagłówki przekazywane do aplikacji są takie same dla każdej usługi. Zapisz je raz w /etc/nginx/snippets/proxy-common.conf:

nginx
proxy_http_version 1.1;
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;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;

I mapę dla WebSocketa w /etc/nginx/conf.d/websocket-map.conf (pliki z conf.d trafiają do kontekstu http). Zmienną $connection_upgrade wolno zdefiniować w całej konfiguracji nginx tylko raz – druga definicja kończy nginx -t błędem „duplicate “$connection_upgrade” variable”. Dlatego mapa ma swój własny plik, a polecenie tworzy go tylko wtedy, gdy jeszcze nie istnieje. Tego samego polecenia i tego samego pliku używają pozostałe poradniki z WebSocketem (n8n, Uptime Kuma, Nextcloud, Vaultwarden, stack Docker Compose), więc możesz je wykonywać w dowolnej kolejności:

bash
[ -f /etc/nginx/conf.d/websocket-map.conf ] || sudo tee /etc/nginx/conf.d/websocket-map.conf >/dev/null <<'EOF'
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}
EOF
grep -rn 'map $http_upgrade' /etc/nginx/

Ostatnia komenda powinna pokazać dokładnie jeden wiersz – z pliku websocket-map.conf. Jeśli widzisz drugi (np. mapę wklejoną kiedyś do pliku w sites-available), usuń tamtą definicję.

Po co to wszystko:

  • Host – bez niego nginx wysyła nazwę z proxy_pass, czyli 127.0.0.1:3000, i aplikacja buduje linki oraz przekierowania na ten adres.
  • X-Forwarded-For i X-Real-IP – aplikacja widzi adres odwiedzającego, a nie nginx.
  • X-Forwarded-Proto – aplikacja wie, że użytkownik przyszedł przez HTTPS. Bez tego wiele frameworków przekierowuje z HTTP na HTTPS w nieskończoność.
  • proxy_http_version 1.1 – dokumentacja nginx podaje, że 1.1 jest domyślne dopiero od wersji 1.29.7; wszystkie wersje z Ubuntu i Debiana używają do backendu HTTP/1.0, które nie obsługuje WebSocketa ani keepalive.
  • Upgrade i Connection z mapą – według opisu WebSocketa w nginx te nagłówki nie przechodzą przez proxy same, bo są „hop-by-hop”. Mapa ustawia upgrade tylko wtedy, gdy klient o to prosi, a dla zwykłych żądań close.

Ważna pułapka: dyrektywy proxy_set_header dziedziczą się z wyższego poziomu tylko wtedy, gdy na bieżącym poziomie nie ma ani jednej własnej. Jeśli w location dopiszesz jeden nagłówek, pozostałe znikną – dlatego dołączaj snippet w każdym location, a dodatkowe nagłówki pisz obok.

Krok 3. Jeden plik na usługę

/etc/nginx/sites-available/app.example.pl:

nginx
# app: kontener na 127.0.0.1:3000 (/opt/apps/app)
server {
    listen 80;
    listen [::]:80;
    server_name app.example.pl;

    client_max_body_size 10m;

    location / {
        proxy_pass http://127.0.0.1:3000;
        include snippets/proxy-common.conf;
    }
}
bash
sudo ln -s /etc/nginx/sites-available/app.example.pl /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d app.example.pl

Certbot dopisze do tego pliku blok listen 443 ssl, ścieżki certyfikatu i przekierowanie z HTTP. Usługę wyłączasz, usuwając dowiązanie z sites-enabled – plik zostaje na później.

Uwaga na ukośnik w proxy_pass. proxy_pass http://127.0.0.1:3000; przekazuje ścieżkę bez zmian. proxy_pass http://127.0.0.1:3000/; w location /api/ obetnie prefiks /api/ – aplikacja dostanie /users zamiast /api/users. Przy location / obie formy działają tak samo; problem wychodzi, gdy wystawiasz aplikację pod podścieżką.

Krok 4. HTTP/2

Po dopisaniu HTTPS przez certbota włącz HTTP/2 w bloku z listen 443. Składnia zależy od wersji:

nginx
# nginx 1.25.1 i nowszy (Debian 13):
listen 443 ssl;
listen [::]:443 ssl;
http2 on;

# nginx 1.24 (Ubuntu 24.04) i 1.22 (Debian 12):
listen 443 ssl http2;
listen [::]:443 ssl http2;

Dyrektywa http2 pojawiła się w 1.25.1; starsze wersje jej nie znają i nginx -t zgłosi błąd. HTTP/2 dotyczy tylko połączenia przeglądarka–nginx; do kontenera dalej idzie HTTP/1.1. Sprawdź: curl -sI --http2 https://app.example.pl | head -1 powinno pokazać HTTP/2 200.

Krok 5. Kompresja

Według dokumentacji modułu gzip domyślnie kompresowany jest tylko text/html, a przy gzip_proxied off nic nie jest kompresowane dla żądań, które przyszły przez inny serwer pośredniczący (z nagłówkiem Via, typowym dla CDN). Plik nginx.conf z Ubuntu i Debiana ma już gzip on;, więc dopisz w /etc/nginx/conf.d/gzip.conf tylko brakujące ustawienia:

nginx
gzip_vary on;
gzip_proxied any;
gzip_comp_level 5;
gzip_min_length 1000;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml;

Nie powtarzaj gzip on; – druga taka linia w tym samym kontekście kończy się błędem „directive is duplicate”. Obrazków JPEG, PNG ani WebP nie dodawaj do gzip_types; są już skompresowane. Jeśli aplikacja sama kompresuje odpowiedzi, nginx przekaże je bez ponownej kompresji.

Krok 6. Limity

Rozmiar żądania. Domyślne client_max_body_size to 1 MB. Ustaw je per usługa (jak w kroku 3): kilka MB dla formularzy, więcej dla aplikacji z wgrywaniem plików.

Liczba zapytań na logowanie. Strefę definiujesz w kontekście http, np. w /etc/nginx/conf.d/limits.conf:

nginx
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
limit_req_status 429;

i używasz jej tylko tam, gdzie trzeba:

nginx
location = /login {
    limit_req zone=login burst=5 nodelay;
    proxy_pass http://127.0.0.1:3000;
    include snippets/proxy-common.conf;
}

Według dokumentacji limit_req strefa 1 MB mieści około 8 tysięcy stanów na systemie 64-bitowym, a domyślny kod odrzucenia to 503 – zmieniamy go na 429, żeby w logach dało się odróżnić limit od awarii.

Czas oczekiwania. proxy_read_timeout domyślnie wynosi 60 sekund między kolejnymi odczytami z backendu. Podnoś go tylko dla usług, które naprawdę długo liczą (eksporty, raporty), np. proxy_read_timeout 300s; w ich location.

Najczęstsze problemy

  • 502 Bad Gateway. nginx nie połączył się z backendem. Kontener nie działa (docker compose ps), port w proxy_pass jest inny niż opublikowany albo aplikacja w kontenerze słucha na 127.0.0.1 zamiast 0.0.0.0 i nie przyjmuje połączeń spoza kontenera. Szczegóły są w sudo tail -f /var/log/nginx/error.log – szukaj connect() failed (111: Connection refused).
  • 504 Gateway Timeout. Backend przyjął połączenie, ale nie odpowiedział w ciągu proxy_read_timeout. Albo aplikacja wisi (sprawdź jej logi), albo operacja trwa dłużej niż 60 s – wtedy podnieś limit dla tej jednej ścieżki.
  • 413 Request Entity Too Large. Plik większy niż client_max_body_size. Ustaw większą wartość w bloku server tej usługi. Pamiętaj, że aplikacja może mieć własny limit.
  • WebSocket rozłącza się po minucie albo nie łączy się wcale. Brak Upgrade/Connection (często przez własny proxy_set_header w location, który wyłączył dziedziczenie) albo brak ruchu przez 60 s – podnieś proxy_read_timeout lub włącz w aplikacji pingi.
  • nginx -t zgłasza „duplicate map” lub „duplicate directive”. Ta sama mapa albo gzip on zdefiniowane dwa razy, zwykle po skopiowaniu konfiguracji z innego poradnika. Trzymaj mapę w jednym pliku w conf.d.

Co dalej

Na tej bazie postawisz konkretne narzędzia: Umami, n8n albo Vaultwarden. Jeśli masz dużo subdomen, jeden certyfikat wildcard upraszcza sprawę – opisuje to poradnik o certbocie. Serwer pod kilka usług znajdziesz w dziale promocje: VPS.

Powiązane poradniki