Przejdź do treści
Serwery i hosting · · ~18 min czytania

Zaawansowany Nginx na Debianie 13: HTTP/2, PHP-FPM, Redis, Memcached i rate limiting

Jak świadomie zmienić jeden vhost: wybrać potrzebne składniki, przygotować kopię konfiguracji, wdrożyć HTTP/2, PHP-FPM, cache i limity, a następnie sprawdzić oraz wycofać zmianę.

Autor: Zespół LinuxLab

Ten wpis prowadzi przez zmianę jednego serwera wirtualnego Nginx (vhostu). Zakłada działający Nginx z HTTPS i aplikację PHP; podstawowe wdrożenie opisuje wpis o stacku LEMP. Przykładowe nazwy example.com, example_web i ścieżki trzeba zastąpić własnymi, potwierdzonymi wartościami. Polecenia wykonujemy jako root na hoście Nginx.

Najpierw wybierz zakres. HTTP/2 i poprawny vhost są częścią podstawowej ścieżki dla publicznej strony HTTPS. Osobna pula PHP-FPM ma sens, gdy host obsługuje więcej niż jedną aplikację PHP. Redis albo Memcached wdrażamy wyłącznie wtedy, gdy aplikacja umie z nich korzystać. Limity ruchu zaczynamy od wąskich, obserwowanych reguł dla logowania lub kosztownego endpointu, a nie od blokowania całej strony.

Spis treści

  1. Które składniki są potrzebne?
  2. Co sprawdzić i zapisać przed zmianą
  3. Jak przebiega żądanie
  4. HTTP/2 i osobna pula PHP-FPM
  5. Redis czy Memcached?
  6. Limity ruchu bez blokowania prawidłowych klientów
  7. Przykładowy vhost
  8. Jak sprawdzić Nginx, PHP, cache i limity
  9. Jak bezpiecznie wycofać zmianę
  10. Najkrótsza ścieżka decyzji

Które składniki są potrzebne?

Nie instalujemy wszystkich elementów dlatego, że występują w tym samym artykule. Nginx przyjmuje żądanie HTTP i przekazuje żądania PHP do PHP-FPM. PHP-FPM jest menedżerem procesów PHP: uruchamia kod aplikacji poza procesem Nginx. Redis i Memcached są opcjonalnymi magazynami danych w RAM, z których korzysta aplikacja, a nie Nginx. limit_req i limit_conn są modułami Nginx ograniczającymi odpowiednio tempo żądań oraz liczbę jednoczesnych połączeń.

Składnik Kiedy go wybrać Czego nie zakładać
HTTP/2Publiczny vhost HTTPS obsługujący współczesne przeglądarki.Nie przyspiesza samodzielnie wolnej bazy ani kodu PHP.
Osobna pula PHP-FPMNa jednym hoście działa kilka aplikacji lub chcemy oddzielić ich użytkowników i limity procesów.Wartość pm.max_children nie jest uniwersalna; zależy od RAM i aplikacji.
RedisAplikacja wspiera cache obiektów, sesje lub inne dane, do których potrzebuje funkcji Redisa.Usługa uruchomiona na hoście nie oznacza jeszcze, że aplikacja z niej korzysta.
MemcachedPotrzebny jest wyłącznie prosty, ulotny cache klucz-wartość, wspierany przez aplikację.Nie jest magazynem danych trwałych ani obowiązkowym dodatkiem do Redisa.
Limity NginxLogowanie, API i inne kosztowne endpointy wymagają ochrony przed nadmiarem ruchu.Nie włączamy restrykcyjnego limitu globalnie bez obserwacji rzeczywistego ruchu.

Co sprawdzić i zapisać przed zmianą

Najpierw ustalamy faktyczny plik vhostu, wersję PHP-FPM i gniazdo, przez które Nginx kontaktuje się z PHP. Nie zakładamy, że nazwa usługi albo ścieżka z przykładu jest taka sama na każdym hoście. Poniższe polecenia są tylko do odczytu i nie zmieniają konfiguracji.

Kontekst: powłoka root na hoście Nginx. Sukces: znamy aktywne pliki, usługę PHP-FPM i używane gniazdo. Ryzyko: brak - polecenia niczego nie zapisują.

nginx -t
nginx -T | grep -E '^[[:space:]]*(include|server_name|fastcgi_pass|listen)'
systemctl list-units --type=service 'php*-fpm.service'
ls -l /run/php/
find /etc/nginx/sites-enabled -maxdepth 1 -type l -printf '%f -> %l\n'

W kolejnych fragmentach example.com oznacza nazwę vhostu, example_web użytkownika aplikacji, a php8.4-fpm usługę widoczną w powyższym wyniku. Zastępujemy je własnymi wartościami, zanim otworzymy edytor.

Zapisujemy kopię konkretnego pliku vhostu oraz istniejącej puli PHP-FPM. Jeżeli tworzymy pierwszą, osobną pulę, zapisujemy kopię puli domyślnej zamiast uruchamiać polecenie dla nieistniejącego example.conf. Zmienna backup_dir działa tylko w bieżącej sesji powłoki; końcowe printf wypisuje ścieżkę, którą warto zachować w historii operacyjnej.

Kontekst: nadal root; przed pierwszą edycją. Sukces: dwa pliki istnieją w katalogu kopii. Ryzyko i wycofanie: polecenie jedynie kopiuje pliki; tę kopię wykorzystamy w sekcji „Jak bezpiecznie wycofać zmianę”.

backup_dir=/root/nginx-backup/$(date +%F-%H%M%S)
install -d -m 700 "$backup_dir"
cp -a /etc/nginx/sites-available/example.com "$backup_dir/"
# Jeśli pula już istnieje; przy pierwszej puli zapisz jej poprzednik, np. www.conf.
cp -a /etc/php/8.4/fpm/pool.d/example.conf "$backup_dir/"
printf '%s\n' "$backup_dir"

Jak przebiega żądanie

Zanim zmienimy konfigurację, warto rozdzielić role komponentów. Vhost to blok server { }, który wybiera domenę, certyfikat i reguły obsługi żądań. FastCGI jest protokołem, którym Nginx przekazuje żądanie PHP do PHP-FPM, zwykle przez lokalne gniazdo uniksowe. Cache przechowuje tymczasowy wynik pod kluczem, aby aplikacja nie musiała ponownie wykonywać tego samego kosztownego obliczenia albo zapytania do bazy.

Przeglądarka
klient
HTTP/2 · TLS
Nginx
serwer WWW
HTTP/2 limit_req limit_conn
FastCGI
PHP-FPM
aplikacja
PHP-FPM (aplikacja) komunikuje się z:
Redis
cache obiektów / sesje
Memcached
ulotny cache
MariaDB
dane trwałe

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: jeśli użyty pakiet Nginx zawiera moduł ngx_http_memcached_module, Nginx może czytać gotowe odpowiedzi z Memcached. Redisem w standardowej ścieżce zajmuje się aplikacja.

W zwykłej konfiguracji Nginx nie odczytuje ani nie zapisuje danych w Redisie, Memcached czy MariaDB. Robi to kod aplikacji uruchomiony przez PHP-FPM. Wyjątkiem jest opcjonalny moduł ngx_http_memcached_module, który potrafi odczytać gotową odpowiedź z Memcached; nie jest to domyślna ścieżka dla WordPressa ani punkt startowy tego wdrożenia.

HTTP/2 i osobna pula PHP-FPM

Jak włączyć HTTP/2 bez zmiany ruchu na porcie 80?

HTTP/2 pozwala przesyłać wiele strumieni żądań w jednym połączeniu. Dla publicznej strony włączamy go w vhoście TLS, czyli tym, który nasłuchuje na 443 ssl. W aktualnym Nginx służy do tego osobna dyrektywa http2 on;; nie dodajemy jej do bloku przekierowującego z portu 80. To zmienia tylko negocjację protokołu z klientem, nie konfigurację PHP ani cache.

Kontekst: edycja właściwego pliku /etc/nginx/sites-available/example.com jako root. Sukces: vhost TLS zawiera poniższe dwie linie. Ryzyko i wycofanie: literówkę wykryje nginx -t; aby wycofać, usuwamy http2 on; albo odtwarzamy zapisaną kopię vhostu.

server {
    listen 443 ssl;
    http2 on;
    server_name example.com;
    # pozostałe dyrektywy vhostu
}

Jak odseparować aplikację PHP od innych stron na tym samym hoście?

Pula PHP-FPM to grupa procesów z własnym użytkownikiem, gniazdem i limitem procesów. Osobna pula ogranicza zasięg błędu uprawnień oraz pozwala jednej aplikacji mieć własny limit równoległych żądań. Nginx musi mieć prawa potrzebne do serwowania plików statycznych oraz do połączenia z gniazdem, ale kod PHP działa jako użytkownik puli. Wartości pm.* są punktem startowym, nie gotowym sizingiem dla każdego serwera.

Kontekst: nowy lub istniejący plik puli w /etc/php/8.4/fpm/pool.d/. Sukces: test PHP-FPM przechodzi, usługa tworzy /run/php/example.sock, a vhost wskazuje to samo gniazdo. Ryzyko: błędny użytkownik, ścieżka lub prawa do gniazda daje 502 Bad Gateway. Wycofanie: odtwarzamy kopię puli i przywracamy poprzedni fastcgi_pass.

; /etc/php/8.4/fpm/pool.d/example.conf
[example]
user = example_web
group = example_web
; Nginx pracujący jako www-data może połączyć się z tym gniazdem
listen = /run/php/example.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
pm = dynamic
pm.max_children = 10
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3

Opcje open_basedir i disable_functions można rozważyć jako dodatkowe ograniczenia konkretnej aplikacji, ale nie dodajemy ich w ciemno: wymagania WordPressa, rozszerzeń i zadań CLI mogą być inne. Po ich włączeniu testujemy pełną ścieżkę aplikacji, aktualizację oraz zadania cykliczne. PHP-FPM nigdy nie powinien działać jako root.

Kontekst: po zapisaniu puli, przed przeładowaniem Nginx. Sukces: test kończy się bez błędów, a po reloadzie istnieje gniazdo z prawami dla www-data. Ryzyko i wycofanie: gdy test nie przejdzie, nie przeładowujemy usługi; poprawiamy plik albo odtwarzamy kopię.

php-fpm8.4 -tt
systemctl reload php8.4-fpm
systemctl is-active php8.4-fpm
ls -l /run/php/example.sock

Redis czy Memcached?

Najpierw odpowiadamy na pytanie, jaki problem rozwiązuje cache. Wybieramy Redis, gdy aplikacja wymaga jego funkcji albo ma sprawdzony moduł cache lub sesji dla Redisa. Wybieramy Memcached, gdy aplikacja potrzebuje tylko prostego, ulotnego cache klucz-wartość. W obu przypadkach utrata wpisów cache po restarcie musi być dla aplikacji bezpieczna: dane źródłowe pozostają w MariaDB lub innym trwałym storage. Nie uruchamiamy obu usług bez osobnego, nazwanego zastosowania dla każdej.

Kiedy Redis?

Redis sprawdza się jako cache obiektów oraz magazyn sesji, jeżeli aplikacja i użyte rozszerzenie PHP go obsługują. Lokalne gniazdo uniksowe jest dostępne tylko na jednym hoście. Jeżeli kilka hostów aplikacyjnych ma współdzielić sesje, potrzebny jest osobno zaprojektowany, zabezpieczony Redis dostępny przez sieć; nie zamieniamy lokalnego gniazda w „klaster” samą zmianą ścieżki.

Kontekst: Debian 13, jako root, wyłącznie gdy wcześniej podjęliśmy decyzję o Redisie. Sukces: działa lokalna usługa, a rozszerzenie PHP jest dostępne dla aplikacji PHP. Ryzyko: nie instalujemy drugiego cache „na wszelki wypadek”. Wycofanie: najpierw wyłączamy użycie Redisa w aplikacji, a dopiero potem zatrzymujemy lub usuwamy pakiet.

apt install redis-server php8.4-redis
systemctl enable --now redis-server
systemctl is-active redis-server

Pakiet php8.4-redis jest potrzebny tylko wtedy, gdy aplikacja PHP używa rozszerzenia Redisa. Aplikacja w innym języku korzysta ze swojej biblioteki klienta, nie z tego pakietu.

Konfiguracja lokalnego Redisa: plik /etc/redis/redis.conf powinien wiązać usługę wyłącznie z localhostem i pozostawiać protected-mode yes. Nie publikujemy portu 6379 przez firewall ani przez reverse proxy. Jeśli wybierasz gniazdo uniksowe, jego ścieżka i prawa muszą odpowiadać użytkownikowi puli PHP-FPM.

Kontekst: konfiguracja lokalnej usługi Redis jako root. Sukces: usługa nasłuchuje tylko lokalnie. Ryzyko: usługa dostępna z internetu nie jest bezpiecznym cache. Wycofanie: odtwarzamy poprzedni plik Redis i restartujemy wyłącznie redis-server.

# /etc/redis/redis.conf
bind 127.0.0.1 ::1
protected-mode yes

# Opcjonalnie, gdy aplikacja ma używać lokalnego gniazda:
# unixsocket /run/redis/redis-server.sock
# unixsocketperm 770

🔒 Bezpieczeństwo: Ochronę budujemy przede wszystkim przez ograniczenie sieci: bind 127.0.0.1 ::1 lub dobrze dobrane prawa do lokalnego gniazda, protected-mode yes, firewall oraz uwierzytelnianie, jeśli Redis ma dostęp przez sieć. Port 6379 nie powinien być publicznie dostępny. Samo ukrycie komend nie zastępuje tych kontroli.

Po uruchomieniu Redisa konfigurujemy aplikację. Nginx nie ma w tym domyślnej roli: to PHP lub biblioteka aplikacji zapisuje i odczytuje cache. Gdy chcemy przenieść natywne sesje PHP do lokalnego Redisa, najpierw potwierdzamy zainstalowane rozszerzenie oraz faktyczną ścieżkę gniazda w redis.conf.

Kontekst: plik puli PHP-FPM dla aplikacji. Sukces: po przeładowaniu PHP-FPM aplikacja tworzy sesje, a jej logi nie pokazują błędów połączenia. Ryzyko: błędna ścieżka lub brak rozszerzenia kończy się błędami sesji. Wycofanie: usuwamy te dwie linie albo odtwarzamy kopię puli.

; /etc/php/8.4/fpm/pool.d/example.conf
; wymaga aktywnego rozszerzenia redis dla tej wersji PHP
php_value[session.save_handler] = redis
php_value[session.save_path] = "unix:///run/redis/redis-server.sock"

Kiedy Memcached?

Memcached przechowuje proste pary klucz-wartość w RAM. Wybieramy go wtedy, gdy aplikacja jawnie go wspiera i akceptuje, że każdy wpis jest tymczasowy. Nie traktujemy go jako bazy danych ani „dodatku wydajnościowego” bez pomiaru. Jeżeli Redis już spełnia potrzeby aplikacji, drugi cache zwiększa liczbę usług i ścieżek awarii.

Kontekst: Debian 13, jako root, tylko gdy aplikacja ma sprawdzone zastosowanie dla Memcached. Sukces: usługa jest aktywna, a opcjonalne rozszerzenie PHP jest dostępne. Ryzyko: nie instalujemy go równolegle z Redisem bez osobnego celu. Wycofanie: najpierw usuwamy konfigurację cache z aplikacji, a dopiero potem zatrzymujemy lub usuwamy usługę.

apt install memcached php8.4-memcached
systemctl enable --now memcached
systemctl is-active memcached

Pakiet php8.4-memcached jest potrzebny tylko dla aplikacji PHP korzystającej z tego rozszerzenia. Sama aktywna usługa nie przełącza aplikacji na cache.

Kontekst: edycja /etc/memcached.conf jako root. Sukces: Memcached słucha wyłącznie na localhost, ma świadomie ustawiony limit RAM i nie używa UDP. Ryzyko: port dostępny z sieci lub UDP tworzy niepotrzebną powierzchnię ataku. Wycofanie: odtwarzamy poprzedni plik i restartujemy memcached.

# /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ć? Zaletą Memcached jest mały, prosty model danych. Wybór powinien wynikać z obsługi w aplikacji, potrzeb dotyczących sesji oraz pomiaru własnego obciążenia - nie z samej deklarowanej różnicy wydajności między usługami.

🔒 Bezpieczeństwo: Utrzymujemy nasłuch na 127.0.0.1 i wyłączamy UDP przez -U 0. Jeżeli wymóg architektoniczny każe udostępnić usługę innemu hostowi, projektujemy tę zmianę osobno: ograniczamy źródła firewallem, sprawdzamy mechanizm uwierzytelniania dostępny w użytym pakiecie i testujemy awarię połączenia.

Jest też wariant zaawansowany: moduł ngx_http_memcached_module odczytuje gotową odpowiedź z Memcached, jeśli aplikacja wcześniej zapisała ją pod dokładnie takim samym kluczem. Ten wariant omija PHP. Nie jest właściwą pierwszą konfiguracją dla WordPressa ani dla strony z odpowiedziami zależnymi od zalogowanego użytkownika, ciasteczek albo nagłówków. Przed użyciem trzeba zaprojektować klucz, unieważnianie wpisów i zachowanie przy chybieniu cache.

Kontekst: własna aplikacja, która samodzielnie zapisuje zgodne odpowiedzi do Memcached. Sukces: chybienie cache wraca do PHP, a trafienie zwraca właściwą odpowiedź dla tego samego URI i parametrów. Ryzyko: zbyt prosty klucz może zwrócić niewłaściwą treść. Wycofanie: usuwamy ten blok location i wracamy do zwykłego przekazania do PHP.

location / {
    set $memcached_key "$uri?$args";
    memcached_pass 127.0.0.1:11211;
    error_page 404 502 504 = @app;
}

location @app {
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/example.sock;
}

Standardowa ścieżka pozostaje prostsza: cache obsługuje aplikacja, a Nginx przekazuje do niej ruch przez PHP-FPM. Do wariantu z memcached_pass wracamy dopiero wtedy, gdy mamy testy poprawności cache i zmierzoną korzyść.

Limity ruchu bez blokowania prawidłowych klientów

limit_req ogranicza średnie tempo żądań dla klucza, najczęściej adresu IP. limit_conn ogranicza liczbę równolegle obsługiwanych połączeń dla tego samego klucza. To dwa różne problemy: pierwszy chroni przed serią prób logowania lub ciężkich wywołań API, a drugi przed nadmierną równoległością. Pierwszą regułę dokładamy do jednego kosztownego endpointu i uruchamiamy w trybie obserwacji.

Kontekst: plik w /etc/nginx/conf.d/, którego dyrektywy są wczytywane wewnątrz http { }, oraz właściwy location vhostu. Sukces: Nginx akceptuje konfigurację, a tryb obserwacji rejestruje nadmiar bez odrzucania ruchu. Ryzyko: zbyt niski limit blokuje prawidłowych klientów. Wycofanie: usuwamy dyrektywy z location albo pozostawiamy limit_req_dry_run on; do czasu korekty wartości.

# kontekst http { } - definicja strefy
limit_req_zone $binary_remote_addr zone=req_per_ip:10m rate=10r/s;

# tylko w kosztownym endpointie, np. logowaniu WordPressa
location = /wp-login.php {
    limit_req zone=req_per_ip burst=20 nodelay;
    limit_req_dry_run on;   # najpierw obserwujemy, potem usuwamy tę linię
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/example.sock;
}

Za reverse proxy lub CDN: zanim ograniczysz ruch według IP, ustaw real_ip_header oraz set_real_ip_from wyłącznie dla rzeczywistych adresów lub sieci zaufanego proxy. Bez tego Nginx może widzieć adres proxy zamiast klienta i limitować wszystkich wspólnie. Zaufanie nagłówkowi od dowolnego klienta pozwala go sfałszować.

Po co tak używać? rate=10r/s oznacza średnio dziesięć żądań na sekundę, burst=20 dopuszcza krótki nadmiar, a nodelay nie kolejkuje tego nadmiaru. Po usunięciu limit_req_dry_run on; żądania przekraczające dopuszczalny burst zostaną odrzucone; domyślny status to 503. Wartości są przykładowe - przed egzekwowaniem sprawdzamy logi i normalny ruch aplikacji.

Jak ograniczyć równoległe połączenia?

Limit połączeń stosujemy osobno od limitu żądań. Nie oznacza on prostego limitu wszystkich otwartych połączeń TCP: Nginx liczy połączenie, gdy obsługuje żądanie i odczytał jego nagłówek. Wysokość limitu zależy między innymi od sposobu, w jaki użytkownicy korzystają ze strony oraz od proxy przed serwerem.

Kontekst: ta sama konfiguracja http { } i vhost, co w poprzednim przykładzie. Sukces: po przeładowaniu Nginx rejestruje nadmiar bez blokowania, dopóki pozostaje limit_conn_dry_run on;. Ryzyko i wycofanie: globalny limit może blokować legalne pobrania lub ruch z NAT; zaczynamy od trybu obserwacji i usuwamy dyrektywę z vhostu, jeśli limit jest nieadekwatny.

# kontekst http { }
limit_conn_zone $binary_remote_addr zone=conn_per_ip:10m;

 # w server { } lub w wąskim location { }
limit_conn conn_per_ip 10;
limit_conn_dry_run on;          # najpierw obserwujemy

Po co tak używać? limit_req i limit_conn mogą się uzupełniać, ale nie muszą być włączane jednocześnie w całym serwisie. Najpierw obserwujemy każde ograniczenie, sprawdzamy logi i dopiero potem usuwamy odpowiadającą mu dyrektywę *_dry_run on;, aby zacząć odrzucać nadmiar.

Jakie zabezpieczenia pozostają w każdym vhoście?

Te ustawienia nie zastępują aktualizacji systemu, uprawnień plików i kontroli dostępu, ale ograniczają przypadkowe ujawnienie plików oraz wzmacniają zachowanie HTTPS:

  • 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.

Kontekst: vhost HTTPS; nagłówek HSTS wysyłamy wyłącznie przez HTTPS. Sukces: nginx -t przechodzi, odpowiedź HTTPS zawiera oczekiwane nagłówki, a pliki typu .env nie są dostępne z sieci. Ryzyko: HSTS zapamiętuje się w przeglądarce. Wycofanie: nie dodajemy includeSubDomains, dopóki wszystkie subdomeny nie mają działającego HTTPS.

server_tokens off;
add_header Strict-Transport-Security "max-age=31536000" 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.

Najpierw wdrażaj HSTS bez includeSubDomains. Dodaj ten parametr dopiero po inwentaryzacji i teście HTTPS na wszystkich obecnych oraz planowanych subdomenach: przeglądarka zapamiętuje tę politykę i błędna deklaracja odcina nieszyfrowaną subdomenę.

Jak złożyć zmianę w jeden vhost

Poniżej znajduje się świadomie zachowawczy przykład. Strefy limitów są w pliku wczytywanym przez http { }, a sam vhost używa osobnej puli PHP-FPM i HTTP/2. Limity pozostają w trybie obserwacji, dopóki nie sprawdzimy logów. Redis i Memcached nie trafiają do tego vhostu, ponieważ w standardowej ścieżce obsługuje je aplikacja.

Kontekst: po wykonaniu kopii i dopasowaniu wszystkich nazw, ścieżek oraz certyfikatów do konkretnej strony. Sukces: nginx -t potwierdza poprawną składnię, a strona nadal działa przez właściwe gniazdo PHP-FPM. Ryzyko: nie kopiujemy tego bloku nad istniejący vhost bez porównania z jego regułami aplikacji, przekierowaniami i certyfikatami. Wycofanie: używamy kopii z katalogu /root/nginx-backup/.

# /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" 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;

    limit_conn conn_per_ip 10;
    limit_conn_dry_run on;              # usuń po okresie obserwacji

    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    location = /wp-login.php {
        limit_req zone=req_per_ip burst=20 nodelay;
        limit_req_dry_run on;          # usuń po okresie obserwacji
        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;
}

Plik z sites-available musi być aktywowany dokładnie tak, jak robi to obecna konfiguracja, zwykle przez dowiązanie w sites-enabled. Nie tworzymy drugiego vhostu dla tej samej domeny „na próbę”, bo wybór serwera przez Nginx zależy od pary adres/port i server_name.

Jak sprawdzić Nginx, PHP, cache i limity

Test składni nie testuje aplikacji, a działająca usługa cache nie dowodzi, że aplikacja faktycznie z niej korzysta. Weryfikujemy więc kolejno konfigurację, usługi, odpowiedź vhostu oraz wybrany wariant cache. Poleceń wdrożeniowych nie łączymy w jedno długie polecenie: po każdym etapie można zatrzymać się i przywrócić kopię.

Czy Nginx i PHP-FPM przyjęły konfigurację?

Kontekst: po zapisaniu plików, jako root. Sukces: oba testy kończą się kodem 0, usługi są active, a strona zwraca oczekiwaną odpowiedź. Ryzyko i wycofanie: jeśli którykolwiek test zgłosi błąd, nie przeładowujemy tej usługi; poprawiamy plik albo odtwarzamy kopię.

php-fpm8.4 -tt
nginx -t
systemctl reload php8.4-fpm
systemctl reload nginx
systemctl is-active php8.4-fpm nginx
curl -sS -o /dev/null -w '%{http_code}\n' https://example.com/

Do sprawdzenia HTTP/2 potrzebny jest klient curl z obsługą HTTP/2. Pierwsze polecenie pokazuje możliwości lokalnego curl; drugie powinno wyświetlić w pierwszej linii HTTP/2 oraz poprawny kod odpowiedzi. Test wykonujemy z miejsca, z którego chcemy sprawdzić rzeczywistą trasę do vhostu, nie tylko z samego serwera.

Kontekst: host administracyjny lub serwer Nginx, po przeładowaniu konfiguracji. Sukces: curl deklaruje HTTP/2 i odpowiedź zawiera HTTP/2. Ryzyko: brak obsługi HTTP/2 w lokalnym curl nie jest błędem Nginx. Wycofanie: nie jest potrzebne - poniższe polecenia są wyłącznie odczytem.

curl --version | grep -i HTTP2
curl -sS -o /dev/null -D - --http2 https://example.com/ | sed -n '1,12p'

Czy cache jest dostępny i nie wystaje poza host?

Wybieramy tylko blok dla wdrożonej usługi. Odpowiedź PONG z Redisa potwierdza połączenie, natomiast NOAUTH oznacza, że usługa odpowiada, ale klient musi użyć skonfigurowanych poświadczeń. Wynik VERSION ... potwierdza odpowiedź Memcached. Te testy nie tworzą wpisów w cache.

Kontekst: tylko lokalna usługa cache. Sukces: usługa jest aktywna i nasłuchuje na adresie pętli zwrotnej. Ryzyko: nie testujemy połączenia przez publiczny adres. Wycofanie: brak - polecenia są odczytowe.

# Redis
systemctl is-active redis-server
ss -ltnp '( sport = :6379 )'
redis-cli -h 127.0.0.1 ping

# Memcached
systemctl is-active memcached
ss -ltnp '( sport = :11211 )'
printf 'version\r\n' | timeout 2 nc 127.0.0.1 11211

Na końcu sprawdzamy miernik właściwy dla aplikacji: panel modułu cache, metryki hit/miss lub jej logi. Samo PONG nie odpowiada na pytanie, czy aplikacja wykonuje mniej zapytań do bazy.

Czy limity są właściwie obserwowane?

Gdy limit_req_dry_run on; lub limit_conn_dry_run on; pozostaje w konfiguracji, Nginx nie odrzuca ruchu, lecz ewidencjonuje przekroczenia. Najpierw zbieramy zwykły ruch przez uzgodniony okres i oglądamy log błędów. Dopiero po ocenie wyników usuwamy jedną dyrektywę *_dry_run on;, aby włączyć egzekwowanie.

Kontekst: po okresie obserwacji; test obciążeniowy wykonujemy wyłącznie na środowisku testowym albo uzgodnionym endpointzie. Sukces: znamy liczbę przekroczeń i wiemy, czy próg nie dotyka normalnego ruchu. Ryzyko: nie wysyłamy serii żądań do produkcyjnego logowania wyłącznie po to, by sprawdzić limit. Wycofanie: pozostawiamy tryb dry-run lub przywracamy kopię vhostu.

tail -n 200 /var/log/nginx/error.log | grep -Ei 'limiting requests|limiting connections'

# Tylko na stagingu lub uzgodnionym endpointzie testowym:
for attempt in $(seq 1 25); do
    curl -sS -o /dev/null -w '%{http_code}\n' https://staging.example.com/wp-login.php
done

Jak bezpiecznie wycofać zmianę

Najbezpieczniejsza granica zmiany to pojedynczy vhost i pojedyncza pula PHP-FPM. Gdy test składni nie przejdzie, niczego nie przeładowujemy - działająca konfiguracja pozostaje aktywna. Gdy po reloadzie pojawi się 502, nieprawidłowa odpowiedź lub problemy z sesjami, przywracamy zapisane pliki, testujemy je i przeładowujemy najpierw PHP-FPM, potem Nginx.

Kontekst: katalog kopii utworzony w sekcji przygotowania; w przykładzie zastępujemy datę faktyczną nazwą katalogu wypisaną przez printf. Sukces: oba testy przechodzą, a strona wraca do stanu sprzed edycji. Ryzyko: nie zatrzymujemy Redisa lub Memcached przed wyłączeniem ich użycia w aplikacji, ponieważ może to przerwać sesje albo żądania. Wycofanie: to właśnie wykonuje poniższy blok.

cp -a /root/nginx-backup/2026-10-03-120000/example.conf \
    /etc/php/8.4/fpm/pool.d/example.conf
cp -a /root/nginx-backup/2026-10-03-120000/example.com \
    /etc/nginx/sites-available/example.com

php-fpm8.4 -tt && systemctl reload php8.4-fpm
nginx -t && systemctl reload nginx

Po stabilizacji zapisujemy działający wariant w roli Ansible, razem z wartościami specyficznymi dla środowiska. Dzięki temu kolejny host nie dostaje przypadkowo innego zestawu limitów, ścieżek czy usług cache. Infrastrukturę potrzebną do wdrożenia można utrzymywać osobno przez Terraform.

Najkrótsza ścieżka decyzji

  1. Na działającym vhoście HTTPS włącz http2 on; i sprawdź negocjację HTTP/2.
  2. Jeżeli host obsługuje wiele aplikacji PHP, utwórz osobną pulę z własnym użytkownikiem i gniazdem.
  3. Wybierz Redis albo Memcached tylko wtedy, gdy aplikacja to wspiera i potrafisz wskazać, co będzie cacheowane.
  4. Trzymaj lokalne cache na localhost; nie publikuj ich portów.
  5. Dodaj limit najpierw do kosztownego endpointu w trybie dry-run, sprawdź logi, a dopiero potem egzekwuj.
  6. Przed każdą edycją zapisz kopię; testuj PHP-FPM i Nginx osobno, zanim wykonasz reload.

Chcesz wydajny i odporny serwer WWW?

Projektujemy konfiguracje Nginx pod ruch produkcyjny - z buforowaniem, HTTP/2 i ochroną przed nadużyciami. Porozmawiajmy o Twoim serwerze, bezpłatnie i bez zobowiązań.