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.
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:
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:
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:
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:
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ę zproxy_pass, czyli127.0.0.1:3000, i aplikacja buduje linki oraz przekierowania na ten adres.X-Forwarded-ForiX-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.UpgradeiConnectionz mapą – według opisu WebSocketa w nginx te nagłówki nie przechodzą przez proxy same, bo są „hop-by-hop”. Mapa ustawiaupgradetylko 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:
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:
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:
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:
i używasz jej tylko tam, gdzie trzeba:
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 wproxy_passjest inny niż opublikowany albo aplikacja w kontenerze słucha na127.0.0.1zamiast0.0.0.0i nie przyjmuje połączeń spoza kontenera. Szczegóły są wsudo tail -f /var/log/nginx/error.log– szukajconnect() 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 blokuservertej 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łasnyproxy_set_headerwlocation, który wyłączył dziedziczenie) albo brak ruchu przez 60 s – podnieśproxy_read_timeoutlub włącz w aplikacji pingi. nginx -tzgłasza „duplicate map” lub „duplicate directive”. Ta sama mapa albogzip onzdefiniowane dwa razy, zwykle po skopiowaniu konfiguracji z innego poradnika. Trzymaj mapę w jednym pliku wconf.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.