To głębsze rozwinięcie naszego wpisu o stacku LEMP. W LinuxLab tak właśnie konfigurujemy Nginx pod ruchem produkcyjnym: szybko, z cache'owaniem i z ograniczeniami chroniącymi serwer przed nadużyciami. Przy każdym elemencie odpowiadamy na pytanie „po co tak używać?" i dokładamy aspekt bezpieczeństwa — bo wydajność bez kontroli nad ruchem to prosta droga do awarii.
W tym artykule pokazujemy wydajną konfigurację Nginx na Debianie 13 na konkretnym przykładzie: HTTP/2 dla szybszego transportu, PHP-FPM do wykonywania PHP, Redis i Memcached jako warstwy cache, a do tego ograniczanie ruchu modułami limit_req i limit_conn. Dla każdego dodatku wyjaśniamy, po co tak go używać, i wskazujemy, jak go zabezpieczyć.
Spis treści
Co konfigurujemy
Punktem wyjścia jest działający stack z poprzedniego wpisu: Nginx, PHP-FPM i MariaDB na Debianie 13. Tutaj idziemy dalej — dokładamy HTTP/2, dwie warstwy cache (Redis i Memcached) oraz mechanizmy ograniczania ruchu. Architektura wygląda tak:
Nginx łączy się wyłącznie z PHP-FPM (przez FastCGI). Cache (Redis, Memcached) i bazą danych zarządza aplikacja — Nginx nie rozmawia z nimi bezpośrednio.
Opcjonalnie: Nginx potrafi czytać gotowe odpowiedzi wprost z Memcached wbudowanym modułem ngx_http_memcached_module (część standardowego Nginx, bez dodatkowych pakietów). Redisem zajmuje się aplikacja.
W tym stacku to aplikacja PHP korzysta z Redisa, Memcached i MariaDB — Nginx łączy się wyłącznie z PHP-FPM (przez FastCGI) i nie rozmawia ani z bazą, ani z cache. Redis i Memcached odciążają bazę danych, trzymając w pamięci RAM to, co inaczej trzeba by liczyć przy każdym żądaniu.
Czy Nginx mógłby sięgać do cache sam? Dla Memcached — tak, ma wbudowany moduł ngx_http_memcached_module (bez dodatkowych pakietów), więc potrafi serwować zbuforowane odpowiedzi z pominięciem PHP. To jednak opcja zaawansowana; w typowym WordPressie cache obsługuje warstwa PHP. Dla Redisa rdzeń Nginx nie ma modułu, a my świadomie nie sięgamy po moduły zewnętrzne (np. z nginx-extras) — Redisem zarządza aplikacja.
HTTP/2
HTTP/2 to nowsza wersja protokołu HTTP. Zamiast otwierać wiele połączeń TCP, multipleksuje wiele żądań i odpowiedzi w jednym połączeniu, kompresuje nagłówki (HPACK) i ogranicza narzut. Na Debianie 13 (Nginx 1.26+) włączamy go osobną dyrektywą http2 on; — flaga przy listen jest już przestarzała:
server {
listen 443 ssl;
http2 on; # od Nginx 1.25.1 — osobna dyrektywa
server_name example.com;
# ... TLS, root, location ...
}
Po co tak używać? Strona to dziś dziesiątki plików (CSS, JS, obrazy). Po HTTP/1.1 przeglądarka pobierałaby je w kilku połączeniach, czekając w kolejce. HTTP/2 ładuje je równolegle jednym połączeniem — krótszy czas ładowania i mniej narzutu na zestawianie TLS. Dla użytkownika to po prostu szybsza strona.
🔒 Bezpieczeństwo:
Przeglądarki obsługują HTTP/2 wyłącznie po TLS (negocjacja przez ALPN), więc http2 on; ma sens tylko w bloku ssl — nigdy na czystym porcie 80. Trzymaj się nowoczesnych protokołów (ssl_protocols TLSv1.2 TLSv1.3;); starsze, podatne wersje (SSLv3, TLS 1.0/1.1) zostaw wyłączone.
PHP-FPM
Nginx nie wykonuje PHP samodzielnie — przekazuje żądania do PHP-FPM (FastCGI Process Manager) przez gniazdo uniksowe. Kluczowa dobra praktyka to osobna pula (pool) dla każdej strony, z własnym użytkownikiem i własnym gniazdem:
; /etc/php/8.4/fpm/pool.d/example.conf
[example]
user = example_web
group = example_web
; gniazdo tej puli — Nginx (www-data) musi mieć do niego dostęp
listen = /run/php/example.sock
listen.owner = www-data
listen.group = www-data
; przy pm = dynamic wymagane są też trzy parametry pm.*_servers
pm = dynamic
pm.max_children = 10
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
; twardy hardening na poziomie puli
php_admin_value[open_basedir] = /var/www/example.com:/tmp
php_admin_value[disable_functions] = exec,passthru,shell_exec,system,proc_open,popen
Po co tak używać?
Osobna pula na stronę daje izolację i kontrolę zasobów. Każda witryna działa jako inny użytkownik systemowy, więc błąd lub włamanie na jednej stronie nie pozwala odczytać plików sąsiada. Dodatkowo limit pm.max_children sprawia, że jedna strona pod obciążeniem nie zje całego RAM-u i nie pociągnie za sobą pozostałych.
🔒 Bezpieczeństwo:
open_basedir zamyka PHP w katalogu strony (brak dostępu do reszty systemu plików), a disable_functions wyłącza funkcje uruchamiające polecenia powłoki — najczęstszy wektor po wgraniu webshella. PHP-FPM nigdy nie powinien działać jako root.
Redis
Redis to szybki magazyn danych w pamięci RAM. W tym stacku używamy go z poziomu aplikacji do dwóch rzeczy: jako cache obiektów (np. wyniki kosztownych zapytań do MariaDB) i jako magazyn sesji PHP współdzielony między procesami:
; /etc/php/8.4/fpm/pool.d/example.conf — sesje PHP w Redis
; wymaga rozszerzenia: apt install php8.4-redis
php_value[session.save_handler] = redis
php_value[session.save_path] = "unix:///run/redis/redis.sock?persistent=1"
; gdy Redis ma requirepass, dodaj do save_path parametr auth, np. &auth=twoje_haslo
Po co tak używać? Te same zapytania powtarzają się przy niemal każdym żądaniu. Trzymając ich wynik w Redisie, odciążamy MariaDB i skracamy czas odpowiedzi z milisekund do mikrosekund. Współdzielony magazyn sesji oznacza z kolei, że użytkownik pozostaje zalogowany niezależnie od tego, który proces (a nawet który serwer) go obsłuży — to warunek skalowania poziomego.
🔒 Bezpieczeństwo:
Redis bez zabezpieczeń to klasyczny cel ataku — wystawiony na świat pozwala na nieautoryzowany zapis i nierzadko zdalne wykonanie kodu. Dlatego: bind 127.0.0.1 ::1 (lub wyłącznie gniazdo uniksowe), protected-mode yes, ustawione requirepass, oraz wyłączone/przemianowane groźne komendy (rename-command FLUSHALL "", podobnie CONFIG, DEBUG). Port 6379 nigdy nie wychodzi poza localhost.
W odróżnieniu od Memcached, Nginx nie łączy się z Redisem — jego rdzeń nie ma takiego modułu, a my świadomie nie instalujemy modułów zewnętrznych (np. z nginx-extras). Redis pozostaje domeną aplikacji: to ona zapisuje i odczytuje cache obiektów oraz sesje. Dzięki temu konfiguracja Nginx jest prostsza, a stos lżejszy w utrzymaniu.
Memcached
Memcached to prosty, wielowątkowy cache klucz–wartość, również trzymany w RAM. Stawiamy go obok Redisa do tego, w czym jest najlepszy: ulotnego cache fragmentów i wyników, gdzie nie potrzebujemy trwałości ani złożonych typów danych.
# /etc/memcached.conf
-m 256 # limit pamięci (MB)
-l 127.0.0.1 # tylko localhost
-U 0 # wyłącz nasłuch UDP
Po co tak używać? Memcached jest wielowątkowy, więc na maszynach wielordzeniowych skaluje przepustowość prostego cache lepiej niż jednowątkowy rdzeń Redisa. Gdy potrzebujemy wyłącznie błyskawicznego, ulotnego „kubełka" na dane tymczasowe (a nie struktur, trwałości czy pub/sub), Memcached jest prostszym i lżejszym wyborem. W praktyce: Redis tam, gdzie liczy się bogactwo funkcji i trwałość; Memcached tam, gdzie liczy się czysta, równoległa prędkość.
🔒 Bezpieczeństwo:
Memcached był jednym z głównych narzędzi w rekordowych atakach DDoS amplification, właśnie przez otwarty interfejs UDP. Dlatego obowiązkowo: -l 127.0.0.1 (tylko localhost) i -U 0 (całkowite wyłączenie UDP). Jeśli usługa musi być dostępna w sieci wewnętrznej — włącz uwierzytelnianie SASL i ogranicz dostęp firewallem.
Memcached ma jeszcze jedno, opcjonalne zastosowanie specyficzne dla Nginx. Dzięki modułowi ngx_http_memcached_module — który jest częścią standardowego Nginx (nie wymaga pakietów typu nginx-extras) — Nginx potrafi serwować odpowiedzi bezpośrednio z Memcached, w ogóle nie uruchamiając PHP, pod warunkiem że właściwy klucz wcześniej umieściła tam aplikacja (moduł działa tylko do odczytu):
location / {
set $memcached_key "$uri";
memcached_pass 127.0.0.1:11211; # spróbuj podać z Memcached...
error_page 404 502 504 = @app; # ...a w razie pudła oddaj do PHP
}
location @app {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/example.sock;
}
To jeden z niewielu przypadków, gdy Nginx „rozmawia" z warstwą cache samodzielnie. W typowym WordPressie i tak zwykle cache obsługuje wtyczka po stronie PHP, ale dla własnych aplikacji potrafi to dać zauważalny zysk — najszybsze żądanie to takie, które nigdy nie dotarło do PHP.
Ograniczanie żądań — ngx_http_limit_req
Moduł ngx_http_limit_req (wbudowany w Nginx) ogranicza tempo żądań według wybranego klucza — zwykle adresu IP. Działa w oparciu o algorytm „cieknącego wiadra" (leaky bucket): definiujemy strefę i dozwoloną częstotliwość, a Nginx wygładza nadmiarowy ruch. Strefę deklarujemy w kontekście http, a stosujemy w server/location:
# kontekst http { } — definicja strefy
limit_req_zone $binary_remote_addr zone=req_per_ip:10m rate=10r/s;
# w location, np. logowanie do WordPressa
location = /wp-login.php {
limit_req zone=req_per_ip burst=20 nodelay;
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/example.sock;
}
Po co tak używać?
Bez limitu pojedynczy bot może bombardować /wp-login.php setkami prób na sekundę (atak słownikowy) albo zalewać kosztowne endpointy. rate=10r/s ustala normę, burst=20 dopuszcza chwilowy skok (np. legalny użytkownik klikający szybko), a nodelay obsługuje ten skok natychmiast, nie kolejkując. Nadmiar dostaje 503 — serwer chroni siebie i bazę.
Ograniczanie połączeń — ngx_http_limit_conn
Bliźniaczy moduł ngx_http_limit_conn ogranicza liczbę jednoczesnych połączeń z jednego klucza (IP). Tam, gdzie limit_req patrzy na tempo, limit_conn patrzy na równoległość:
# kontekst http { }
limit_conn_zone $binary_remote_addr zone=conn_per_ip:10m;
# w server { } lub location { }
limit_conn conn_per_ip 10; # maks. 10 równoczesnych połączeń z jednego IP
Po co tak używać?
Jeden klient otwierający setki równoległych, powoli czytanych połączeń potrafi wyczerpać pulę robotników Nginx i zablokować innych (rodzina ataków typu slowloris, agresywne pobieranie wielowątkowe). Limit równoczesnych połączeń wymusza uczciwy podział zasobów i utrzymuje serwer responsywnym dla pozostałych użytkowników. limit_req i limit_conn stosujemy razem — uzupełniają się.
Bezpieczeństwo przekrojowo
Poza punktowymi uwagami przy każdym komponencie, na poziomie całego serwera pilnujemy kilku stałych zasad:
- Ukryj wersję —
server_tokens off;nie zdradza numeru Nginx (mniej podpowiedzi dla skanerów). - Nagłówki bezpieczeństwa —
Strict-Transport-Security(HSTS),X-Content-Type-Options: nosniff,X-Frame-Options,Referrer-Policy. - Blokuj pliki ukryte i wrażliwe — odmowa dostępu do
.git,.env, kopii zapasowych. - Cache i bazy tylko na localhost — Redis, Memcached i MariaDB nie wychodzą poza
127.0.0.1.
server_tokens off;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
location ~ /\.(?!well-known) { deny all; } # blokuj .git, .env itp.
Pełny przykład konfiguracji
Wszystko razem — strefy limitów w kontekście http, a poniżej kompletny serwer wirtualny z HTTP/2, FastCGI do PHP-FPM, nagłówkami i ograniczeniami ruchu:
# /etc/nginx/conf.d/limits.conf (kontekst http)
limit_req_zone $binary_remote_addr zone=req_per_ip:10m rate=10r/s;
limit_conn_zone $binary_remote_addr zone=conn_per_ip:10m;
# /etc/nginx/sites-available/example.com
server {
listen 443 ssl;
http2 on;
server_name example.com www.example.com;
root /var/www/example.com;
index index.php;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
server_tokens off;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
limit_conn conn_per_ip 10; # globalnie dla tego serwera
location / {
try_files $uri $uri/ /index.php?$args;
}
location = /wp-login.php {
limit_req zone=req_per_ip burst=20 nodelay; # ochrona logowania
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/example.sock;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/example.sock; # pula PHP-FPM strony
}
location ~ /\.(?!well-known) { deny all; }
}
# Przekierowanie HTTP -> HTTPS
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
Po każdej zmianie testujemy składnię i przeładowujemy usługę:
nginx -t && systemctl reload nginx
Jak robimy to w LinuxLab
Taka konfiguracja nie powstaje ręcznie przy każdym wdrożeniu — mamy ją opisaną w rolach Ansible (strefy limitów, pule PHP-FPM, hardening Redisa i Memcached), a infrastrukturę pod nią powołujemy Terraformem. Dzięki temu każdy serwer dostaje identyczny, przetestowany zestaw zabezpieczeń i optymalizacji. Połączenie wydajności (HTTP/2, cache) z kontrolą ruchu (limit_req, limit_conn) to realne zapobieganie awariom: serwer pozostaje szybki pod normalnym obciążeniem i odporny, gdy ruch staje się złośliwy.
Podsumowanie
Wydajny Nginx to suma świadomych decyzji: HTTP/2 skraca czas ładowania, PHP-FPM z osobnymi pulami daje izolację i kontrolę zasobów, Redis i Memcached odciążają bazę, a limit_req i limit_conn chronią serwer przed nadużyciami. Przy każdym z tych elementów warto zadać dwa pytania: po co go tak używam i jak go zabezpieczam — odpowiedzi na nie odróżniają konfigurację „działającą" od konfiguracji produkcyjnej.
Chcesz wydajny i odporny serwer WWW?
Projektujemy konfiguracje Nginx pod ruch produkcyjny — z cache'owaniem, HTTP/2 i ochroną przed nadużyciami. Porozmawiajmy o Twoim serwerze, bezpłatnie i bez zobowiązań.