Otázka nezní, jestli váš web spadne, ale kdy. Proces zadusí memory leak, deploy nechá viset starou verzi, přijde nárazová návštěvnost a PHP-FPM dojdou workeři. To potká každého. Rozdíl mezi amatérským a profesionálním provozem není v tom, že profesionálovi to nikdy nepadá — je v tom, že se jeho server zvedne sám dřív, než si toho kdokoli všimne.
V tomhle článku vám ukážu ověřené mechanismy, kterými to na běžném serveru zařídíte. Všechny příkazy platí pro Debian 12 (Bookworm) a Ubuntu 22.04+. Verze balíčků si podle své distribuce upravte — u PHP-FPM na to konkrétně upozorním.
Proč weby vlastně padají
Než začneme cokoli automatizovat, je dobré vědět, proti čemu bojujeme. V praxi jde skoro vždy o jednu z těchhle věcí:
- Proces spadne — segfault, neošetřená výjimka nebo memory leak, který proces nafoukne, až ho zabije jádro.
- Proces běží, ale neodpovídá — deadlock, zaseknuté připojení k databázi, vyčerpaná fronta workerů. Nejzákeřnější varianta, protože „zvenčí" to vypadá živě.
- Došla paměť — a OOM killer sestřelí něco důležitého, typicky databázi nebo právě váš aplikační proces.
- Špatně nastavené limity — Nginx nebo PHP-FPM narazí na strop, který jste nastavili (nebo nechali v defaultu) vy sami.
Ke každé příčině existuje obrana:
| Typ selhání | Vrstva obrany |
|---|---|
| Proces spadne | systemd Restart=on-failure |
| Proces neodpovídá | health-check přes curl |
| Došla paměť | swap, vm.swappiness, OOMScoreAdjust, MemoryMax |
| Špatné limity | pm.max_children, rate limiting v Nginxu |
Pojďme popořadě.
systemd: první záchranná síť
Základní pravidlo: aplikaci nikdy nespouštějte „ručně na pozadí" ani přes nohup. Ať ji drží systemd — ten umí proces automaticky restartovat, když spadne.
Klíčová je direktiva Restart=. Hodnota on-failure znovu nastartuje proces ve čtyřech případech: skončí s nenulovým návratovým kódem, je ukončen „nečistým" signálem (typicky SIGKILL, SIGSEGV, SIGABRT), vyprší mu timeout operace, nebo se spustí watchdog. Pozor na detail: mezi „nečisté" signály nepatří SIGTERM, SIGINT, SIGHUP ani SIGPIPE. Právě proto vás čisté systemctl stop (které posílá SIGTERM) nerozhodí — systemd to bere jako řádné ukončení a restartovat nebude. Nebude s vámi bojovat.
Ukázková unita /etc/systemd/system/muj-web.service:
[Unit]
Description=Muj web
After=network.target
StartLimitIntervalSec=60
StartLimitBurst=5
[Service]
ExecStart=/usr/bin/node /var/www/app/server.js
Restart=on-failure
RestartSec=5s
User=www-data
[Install]
WantedBy=multi-user.target
Dvě věci, které lidé pletou:
RestartSec=5s říká, jak dlouho systemd počká před dalším pokusem. Když ho necháte v defaultu (100 ms) a aplikace padá hned po startu, dostanete se do smyčky.
A tady přichází past: StartLimitIntervalSec a StartLimitBurst (pozor, patří do sekce [Unit], ne [Service]) omezují, kolikrát se smí unita nastartovat za daný interval. Default je 5 startů za 10 sekund. Když to překročíte, systemd to vzdá a napíše „start request repeated too quickly". Proto ten RestartSec=5s — s pěti sekundami mezi pokusy se do defaultního limitu nevejdete a musíte si ho vědomě rozvolnit, jak je výše.
Po každé editaci souboru:
sudo systemctl daemon-reload
sudo systemctl enable --now muj-web.service
Když už unita jednou „vzdala" restarty, vynulujete čítač takhle:
sudo systemctl reset-failed muj-web.service
Co se děje, sledujete v žurnálu:
journalctl -u muj-web.service -f
Health-check: když proces běží, ale web neodpovídá
systemd hlídá, jestli proces žije, ne jestli odpovídá. Zaseknutou aplikaci musíte odhalit zvenčí — zavoláním na health endpoint.
Jednoduchý skript /usr/local/bin/healthcheck.sh:
#!/usr/bin/env bash
if ! curl -fsS --max-time 5 http://127.0.0.1/health >/dev/null; then
systemctl restart muj-web.service
fi
Přepínač -f u curl vrátí nenulový kód i při HTTP chybě (5xx/4xx), --max-time 5 utne zaseknuté spojení. Pusťte to jednou za minutu — nejjednodušeji přes cron:
* * * * * /usr/local/bin/healthcheck.sh
Pokud nechcete cron, sáhněte po healthcheck.timer. Jenže samotný OnUnitActiveSec=60 první spuštění nikdy neodpálí — nemá se od čeho odrazit, dokud služba aspoň jednou neproběhla. Zakotvěte ho proto přes OnBootSec=:
[Timer]
OnBootSec=60
OnUnitActiveSec=60
Kvůli výchozí AccuracySec=1min navíc timer nefiruje přesně po minutě; pokud potřebujete přesný interval, přidejte AccuracySec=1s.
Monitoring a alerting bez cirkusu
Automatický restart je skvělý, ale musíte se dozvědět, že k němu došlo — jinak vám server tiše restartuje umírající aplikaci třikrát za hodinu a vy o problému nevíte.
Nejčistší je systemd direktiva OnFailure=, která při pádu spustí notifikační unitu:
[Unit]
OnFailure=alert@%n.service
K tomu šablona [email protected], která zavolá skript posílající e-mail nebo webhook (Slack, Telegram):
[Unit]
Description=Alert pro %i
[Service]
Type=oneshot
ExecStart=/usr/local/bin/alert.sh %i
Jedno důležité upozornění z praxe: monitoring, který běží na stejném serveru, vám neřekne, že celý server leží. Proto vždy doplňte kontrolu zvenčí — z druhého stroje nebo z externí uptime služby. Lokální hlídání řeší proces, externí řeší výpadek celé mašiny.
Kdo chce hotové řešení, apt install monit dá dohledový démon, který umí procesy hlídat, restartovat i alertovat. Nedělá nic, co byste nesvedli výše popsaným způsobem, ale ušetří psaní.
Nginx a PHP-FPM: správné limity
Většina „záhadných" 502 a 504 na PHP webech má jednu příčinu — došli workeři PHP-FPM.
PHP-FPM: strop workerů
Konfigurace poolu je v /etc/php/8.2/fpm/pool.d/www.conf (na Ubuntu 22.04 je to 8.1, cestu si upravte podle verze).
Nejdůležitější je pm.max_children — tvrdý strop souběžně zpracovávaných PHP požadavků. Když se vyčerpá, PHP-FPM zapíše do logu jasnou hlášku:
WARNING: [pool www] server reached pm.max_children setting (10), consider raising it
Číslo ale nenastavujte od oka. Změřte si, kolik paměti průměrně žere jeden worker — master proces přitom vynechte, jinak si průměr zkreslíte:
ps -o rss=,args= -C php-fpm8.2 | awk '/pool/{s+=$1; n++} END {if(n) printf "%.0f MB prumer, %d workeru\n", s/n/1024, n}'
Filtr /pool/ vybere jen workery (php-fpm: pool www) a vynechá master proces. A pak počítejte: pm.max_children = (paměť pro PHP) / (velikost jednoho workeru). Když serveru zbude po odečtení systému, Nginx a databáze třeba 2 GB a jeden worker bere 60 MB, vejde se vám zhruba 30 workerů. Nastavit strop výš, než unese RAM, je nejrychlejší cesta k OOM killeru.
Přidejte ještě pm.max_requests = 500 — worker se po 500 požadavcích recykluje, což omezí dopad drobných memory leaků. Po změně:
sudo systemctl reload php8.2-fpm
Nginx: rate limiting
Na straně Nginxu drží web ve zdraví hlavně rozumné timeouty a ochrana proti náporu. Rate limiting nastavíte takto:
limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s;
server {
location / {
limit_req zone=perip burst=20 nodelay;
}
}
Tím server pod náporem odmítne přebytek požadavků slušným 503 místo toho, aby se celý zahltil a spadl.
Swap a OOM killer
Když serveru dojde fyzická paměť i swap, jádro spustí OOM killer a sestřelí proces s nejvyšším oom skóre (tzv. největší badness) — bohužel to bývá ten největší žrout paměti, tedy databáze nebo vaše aplikace. Že k tomu došlo, poznáte takto:
journalctl -k | grep -i "out of memory"
Swap je pojistka proti krátkým špičkám, ne náhrada za chybějící RAM. Pár giga swapu často stačí, aby nárazový výkyv neshodil celý stroj. Na ext4 ho na Debianu/Ubuntu založíte jednoduše:
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
Aby přežil restart, přidejte řádek do /etc/fstab:
/swapfile none swap sw 0 0
Poznámka: tenhle postup platí pro ext4. Na Btrfs by takový swap soubor swapon odmítl — soubor musí být NOCOW. Nejjednodušeji ho na Debianu 12 / Ubuntu 22.04+ založíte jediným příkazem:
sudo btrfs filesystem mkswapfile --size 2G /swapfile
sudo swapon /swapfile
Jak agresivně jádro swapuje, řídí vm.swappiness (default 60). Na serveru s databází se osvědčuje snížit ho — data zůstanou v RAM a swap se sáhne až pod tlakem:
echo 'vm.swappiness=10' | sudo tee /etc/sysctl.d/99-swappiness.conf
sudo sysctl --system
A konečně — pokud máte proces, kterého se OOM killer nemá dotknout jako prvního, řekněte to systemd přímo v unitě:
[Service]
OOMScoreAdjust=-500
Nižší skóre = menší šance, že padne jako první. Naopak MemoryMax=512M v unitě nastaví tvrdý strop paměti pro jeden proces — když ho překročí, jádro sestřelí jen jeho (jeho cgroup), a ne zbytek serveru.
Shrnutí
Odolný server není o jednom kouzelném nastavení, ale o vrstvách, které se zastupují:
- systemd s
Restart=on-failurea rozumnýmRestartSeczvedne spadlý proces. - Health-check přes
curlodhalí proces, který běží, ale neodpovídá. OnFailure=a externí monitoring vám dají vědět, že se něco děje.- Změřený
pm.max_childrena rate limiting v Nginxu drží web ve zdravých mezích pod zátěží. - Swap,
vm.swappinessaOOMScoreAdjuststojí mezi vámi a OOM killerem.
Web, který se sám zvedne do minuty, je pro uživatele lepší než web, který nespadl nikdy — protože ten první existuje, kdežto ten druhý ne.
Nastavte tyhle vrstvy jednou pořádně a příští pád zvládne server sám. Vy se o něm dozvíte z alertu, ne z telefonu naštvaného klienta.