HostiServer
2026-09-26 11:18
Docker v produkci: reverse proxy, SSL
📚 Série „Docker na VPS", část 3 z 5:
Docker v produkci: reverse proxy, SSL, automatické spuštění
V předchozím článku jsme dali aplikaci, databázi a cache dohromady do jednoho docker-compose.yml, a stack se rozjede jedním příkazem. V té podobě, v jaké je, má ale k produkci ještě daleko. Port 3000 je otevřený přímo do internetu, provoz jde jen po HTTP, a druhý projekt na stejném VPS by hned narazil na obsazený port. A pokud se server restartuje — třeba po plánované aktualizaci jádra — stack sám nenaběhne: je třeba se ručně přihlásit přes SSH a spustit docker compose up -d.
V tomto článku uzavřeme tři úkoly najednou. První je Nginx jako reverse proxy: jeden server, jedna IP, standardní porty 80 a 443, za nimi libovolné množství aplikací. Druhý je SSL certifikát od Let's Encrypt, zdarma a s automatickou obnovou. Třetí je systemd jednotka, která rozjede celý stack po restartu serveru bez účasti člověka. Jako bonus rozebereme limity zdrojů, aby jeden kontejner, který se vymkne kontrole, nepoložil celý server, a rotaci logů, která jinak dřív nebo později zaplní disk.
2. Nginx jako reverse proxy v Dockeru
2.1 Nginx jako samostatná služba
Nginx se přidává do docker-compose.yml jako další služba, a právě on je teď jediný, kdo má porty otevřené ven — 80:80 a 443:443. Sekce ports: se odstraní ze služby app: aplikace už není dostupná přímo z internetu, jen přes proxy.
2.2 Sdílená síť
Nginx a app zůstávají ve stejné síti app-net jako předtím. Nginx rozpozná jméno app:3000 úplně stejně, jako sám app rozpoznával db:5432 v předchozím článku — přes vestavěné DNS Compose.
2.3 Minimální HTTP konfigurace pro start
Než začnete získávat certifikát, potřebujete funkční HTTP konfiguraci — právě přes ni bude Let's Encrypt ověřovat, že doménu skutečně vlastníte:
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://app:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Každá z těchto hlaviček má konkrétní důvod tam být: Host předá aplikaci originální jméno domény, které by se jinak za proxy ztratilo; X-Real-IP předá skutečnou IP adresu návštěvníka, protože bez ní by aplikace viděla jen interní IP kontejneru Nginx; X-Forwarded-Proto sdělí, zda požadavek přišel po HTTP nebo HTTPS — aplikace to potřebuje například pro správné sestavení absolutních odkazů.
2.4 Závislost na app
Do služby nginx se přidá depends_on: [app], aby Compose spustil aplikaci dřív než proxy. V případě potřeby lze sem přidat i podmínku service_healthy, pokud má aplikace endpoint typu /health, který healthcheck dokáže dotazovat.
3. SSL: Let's Encrypt + Certbot
3.1 Předpoklad — DNS
Než spustíte Certbot, doména musí ukazovat na IP adresu serveru. Ověřit to lze hned:
dig +short example.com
Ve výstupu má být přesně IP vašeho VPS. Pokud se záznam ještě nerozšířil po DNS, Certbot kontrolu zaručeně neprojde — a vyplatí se počkat, ne se snažit ladit něco v konfiguraci Nginx.
3.2 Certbot na hostu, ne v kontejneru
Certbot se instaluje přímo na server, ne jako samostatný Docker kontejner — je to jednodušší pro následnou automatickou obnovu a není potřeba prokopávat sokety nebo svazky tam a zpět. Nejrozšířenější způsob je webroot: Certbot uloží testovací challenge soubor do určeného adresáře, a Nginx, který má tento adresář připojený, ho vydá ven jako odpověď na požadavek Let's Encrypt.
3.3 Alternativa — standalone
Pokud se nechcete zabývat samostatným nastavením webroot, existuje režim standalone: Certbot si sám rozjede dočasný webserver na portu 80. Kvůli tomu je třeba Nginx zastavit zhruba na pět sekund — přijatelné pro první získání certifikátu, ale nepohodlné pro pravidelnou obnovu na běžícím produkčním serveru.
3.4 Kam se dostane výsledek
Po úspěšném vydání se certifikát a soukromý klíč objeví na cestě /etc/letsencrypt/live/example.com/{fullchain.pem,privkey.pem}. Tento adresář se připojí dovnitř kontejneru Nginx jako bind mount, vždy v režimu :ro (read-only) — Nginx má právo certifikát číst, ale neměl by mít možnost ho měnit.
4. Nginx HTTPS konfigurace
4.1 Dva server bloky
Funkční konfigurace se skládá ze dvou bloků: první poslouchá na portu 80 a hned vrátí redirect return 301 https://$host$request_uri;, druhý poslouchá na 443 a obsahuje samotné SSL:
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name example.com;
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;
ssl_prefer_server_ciphers off;
add_header Strict-Transport-Security "max-age=63072000" always;
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options DENY;
location / {
proxy_pass http://app:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 60s;
}
}
4.2 Proč TLSv1.2 a TLSv1.3, a ne níž
ssl_protocols TLSv1.2 TLSv1.3; je minimální hranice, pod kterou se nevyplatí klesat: TLS 1.0 a 1.1 jsou oficiálně považovány za zastaralé a neprojdou moderními bezpečnostními skenery. ssl_prefer_server_ciphers off; odpovídá doporučením Mozilla Intermediate profilu — u moderních klientů je lepší nechat výběr šifry na klientovi, ne mu vnucovat pořadí serveru.
4.3 HSTS a další bezpečnostní hlavičky
Strict-Transport-Security řekne prohlížeči, aby si zapamatoval, že tato doména se má navštěvovat jen po HTTPS, i když uživatel zadá adresu bez https://. Riziko je tu reálné: hodnota max-age=63072000 jsou dva roky, a pokud se během té doby HTTPS náhle stane nedostupným (například certifikát vypršel a nebyl včas obnoven), prohlížeče se prostě odmítnou vrátit na HTTP jako záložní variantu. HSTS se vyplatí zapnout až tehdy, když je automatická obnova certifikátu už otestovaná a funguje. X-Content-Type-Options a X-Frame-Options jsou doplňkové, méně rizikové hlavičky proti MIME-sniffingu a clickjackingu.
4.4 proxy_read_timeout
Výchozí hodnota v Nginx je 60 sekund, a pro většinu API to stačí. Zvyšovat se vyplatí jen pro konkrétní dlouhé operace: generování reportu, zpracování velkého souboru, dlouhý dotaz na externí API. Zvedat timeout globálně „pro jistotu" se nevyplatí — backendová úloha, která se z nějakého důvodu zasekla, je lepší, když spadne po minutě, než aby držela spojení otevřené neomezeně dlouho.
4.5 Aplikace konfigurace bez výpadku
docker compose exec nginx nginx -s reload
nginx -s reload znovu načte konfiguraci a certifikáty a přitom zachová už otevřená spojení — na rozdíl od plného restartu kontejneru (docker compose restart nginx), který na krátkou chvíli přeruší všechna aktivní připojení.
5. Automatická obnova SSL
5.1 Platnost certifikátu
Certifikáty Let's Encrypt se vydávají na 90 dní. Příkaz certbot renew zkontroluje všechny existující certifikáty a obnoví jen ty, kterým zbývá méně než 30 dní — tedy ho lze bezpečně spouštět klidně denně, nic co ještě obnovu nepotřebuje, neobnoví.
5.2 Proč je po obnově potřeba reload
Nginx načte certifikát do paměti jednou, při startu nebo při reloadu, a sám o sobě soubor na disku znovu nečte. Proto i po úspěšném certbot renew bude Nginx dál používat starý certifikát, dokud mu explicitně neřeknete, aby konfiguraci znovu načetl.
5.3 Deploy hook
Certbot umí po úspěšné obnově spustit libovolný skript — právě sem se umístí reload:
certbot renew --deploy-hook "docker compose exec nginx nginx -s reload"
5.4 Cron úloha
Stačí spouštět kontrolu jednou denně, nejlépe v noci při nízké zátěži:
0 3 * * * certbot renew --quiet --deploy-hook "docker compose exec nginx nginx -s reload"
Přepínač --quiet potlačí výstup v případech, kdy obnova není potřeba — jinak se systémová pošta nebo log cronu zaplní stejnými zprávami každý den.
5.5 Ověření, že mechanismus skutečně funguje
certbot renew --dry-run
certbot certificates
--dry-run projde celý proces obnovy bez skutečného vydání nového certifikátu — bezpečný způsob, jak se ujistit, že webroot a deploy hook jsou nastavené správně. certbot certificates zobrazí seznam certifikátů a datum jejich vypršení — vyplatí se to ověřit alespoň jednou ručně po nastavení cronu.
6. Automatické spuštění po reboot
6.1 restart: unless-stopped vs restart: always
Rozdíl se projeví jen v jednom scénáři: pokud byl stack zastaven ručně příkazem docker compose stop. S unless-stopped si Docker zapamatuje, že kontejner byl zastaven vědomě, a nespustí ho automaticky ani po restartu daemonu. S always se kontejner spustí v každém případě, bez ohledu na to, že byl zastaven ručně — pro produkční stack to zřídka bývá žádoucí chování.
6.2 Omezení restart policy
Žádná restart policy nepomůže, pokud byl stack zastaven přes docker compose down: tento příkaz kontejnery nejen zastaví, ale úplně smaže. Není co restartovat — Docker ani neví, že takový kontejner měl existovat. Přesně pro tento scénář je potřeba externí mechanismus, který znovu provede docker compose up -d.
6.3 Systemd jednotka pro Compose
[Unit]
Description=My App (Docker Compose)
After=docker.service network-online.target
Requires=docker.service
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/app
ExecStart=/usr/bin/docker compose up -d
ExecStop=/usr/bin/docker compose down
TimeoutStartSec=300
[Install]
WantedBy=multi-user.target
Type=oneshot spolu s RemainAfterExit=yes jsou tu klíčové: příkaz docker compose up -d sám skončí hned po spuštění kontejnerů na pozadí, a bez těchto dvou parametrů by systemd považoval službu za dokončenou a „neaktivní" hned po startu. After=docker.service network-online.target zaručuje, že jednotka nastartuje až poté, co jsou samotný Docker daemon a síť připravené — jinak by docker compose up -d spadl hned po bootu, dokud Docker ještě nestihl naběhnout.
6.4 Zapnutí a ověření
sudo systemctl enable app.service
sudo systemctl start app.service
journalctl -u app.service
Nejspolehlivější způsob ověření, že to skutečně funguje, je reálný test: sudo reboot, počkat, přihlásit se znovu a podívat se na docker compose ps. Věřit konfiguraci na slovo, aniž byste to alespoň jednou ověřili restartem, se nevyplatí.
6.5 Dvě úrovně dohromady
restart: unless-stopped v compose souboru a systemd jednotka řeší různé úkoly a fungují společně, ne místo sebe navzájem: systemd odpovídá za to, aby se stack znovu rozjel po restartu hostu, zatímco restart: unless-stopped odpovídá za to, aby se jednotlivý kontejner, který spadl uvnitř už běžícího stacku, sám znovu spustil bez restartu všeho ostatního.
7. Limity zdrojů
7.1 K čemu je to potřeba
Bez explicitních limitů dokáže memory leak v jedné službě nebo proces, který se vymkne kontrole, využít veškerou paměť serveru a položit celý VPS — typicky se to stává v noci, kdy se nikdo nedívá na grafy. Limity omezí škodu na jeden konkrétní kontejner: spadne on, ne sousední služby a ne host.
7.2 Syntaxe v Compose v2
Bez Swarm se limity nastavují přes deploy.resources.limits přímo ve standardním docker-compose.yml:
services:
app:
deploy:
resources:
limits:
memory: 384m
cpus: "0.5"
7.3 Různé chování při překročení
Tyto dva limity fungují odlišně. Překročení limits.memory vede k SIGKILL — jádro zabije proces uvnitř kontejneru (OOM killer), a pokud je nastaveno restart: unless-stopped, Docker kontejner znovu spustí. Překročení limits.cpus nic nezabíjí — služba se prostě throttluje, dostává méně procesorového času a pokračuje v běhu pomaleji.
7.4 Orientační rozdělení pro VPS s 2 GB RAM
| Služba | Limit paměti |
|---|---|
| nginx | 64m |
| app | 384m |
| db | 512m |
| cache | 128m |
Je to orientační hodnota, ne univerzální pravidlo — konkrétní čísla závisí na zátěži. Sekce reservations vedle limits nenastavuje pevnou hranici, ale garantované minimum, a je užitečnější spíš jako dokumentace očekávané spotřeby než jako ochranný mechanismus.
7.5 Monitoring v reálném čase
docker stats
Zobrazuje živou spotřebu CPU, paměti a sítě u každého kontejneru — první nástroj, na který se vyplatí sáhnout při hledání realistických hodnot limitů místo čísel od oka.
8. Rotace logů
8.1 Výchozí problém
Logovací driver json-file, který Docker používá ve výchozím stavu, ničím omezený není — soubor s logy roste neomezeně, dokud nedojde místo na disku. Ověřit aktuální stav:
du -sh /var/lib/docker/containers/*/
8.2 Omezení na úrovni služby
services:
app:
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
Tím se omezí každý log soubor na 10 megabajtů a uchovají se maximálně tři soubory v rotaci — starší se mažou automaticky. Stejného výsledku lze dosáhnout globálně pro všechny kontejnery hostu přes /etc/docker/daemon.json, pokud to nechcete psát do každé služby zvlášť.
8.3 Jak to vypadá při prohlížení logů
docker compose logs -f se chová stejně bez ohledu na to, zda je rotace nastavená: příkaz čte aktuální aktivní soubory logů transparentně pro uživatele, rotace ovlivňuje jen to, kolik historie fyzicky zůstává na disku.
9. Traefik jako alternativa k Nginx
9.1 Klíčový rozdíl v přístupu
Místo samostatného konfiguračního souboru čte Traefik nastavení přímo z labels u samotných služeb v docker-compose.yml. ACME klient pro Let's Encrypt je vestavěný rovnou — samostatný Certbot není potřeba.
9.2 Kdy Traefik vyhrává
Výhoda je znát, když je mikroslužeb hodně a často se přidávají nebo mažou: upravovat labels konkrétní služby je jednodušší, než pokaždé editovat sdílený nginx.conf a nezapomenout na reload.
9.3 Kompromis
Za pohodlí se platí přístupem: Traefik potřebuje připojený /var/run/docker.sock uvnitř svého kontejneru, aby viděl labels ostatních služeb — a to je fakticky root přístup k hostu, podle stejné logiky jako členství ve skupině docker z prvního článku této série. Nginx takový přístup nepotřebuje vůbec.
10. Závěr
Stack prošel cestou od přímo otevřeného portu 3000 až po plnohodnotnou produkční konfiguraci: Nginx před aplikací, SSL certifikát od Let's Encrypt s automatickou obnovou, systemd jednotka pro automatické spuštění po restartu, limity zdrojů a rotace logů.
Architektura teď vypadá takto: internet se dostane jen na porty 80 a 443, kde ho čeká Nginx; ten proxíruje požadavky na app:3000 uvnitř sítě app-net; db a cache jsou zvenčí úplně nedosažitelné.
Krátký checklist, než stack označíte za připravený:
- DNS domény ukazuje na IP serveru (
dig +short) - Certbot vydal certifikát, HTTPS se v prohlížeči otevírá bez varování
- Deploy hook volá
nginx -s reload, ověřeno přescertbot renew --dry-run - Systemd jednotka je zapnutá (
systemctl enable) - Stack se po skutečném
rebootopravdu spustí sám, ne jen teoreticky - Limity paměti jsou nastavené a porovnané se skutečnou spotřebou přes
docker stats
Stack je připravený na produkční zátěž. Zbývá poslední ruční krok — deploy nové verze aplikace stále znamená SSH na server a spouštění příkazů ručně. Další článek řeší přesně tohle: automatický deploy přes GitHub Actions nebo GitLab CI, který po každém merge sám sestaví image, nahraje ho do registru a aktualizuje stack na serveru.
📚 Navigace v sérii:
Čtete část 3 z 5 „Docker v produkci".
Předchozí: Část 2. Docker Compose: více služeb dohromady ←
Dál: Část 4. Docker + CI/CD: automatický deploy s GitHub Actions / GitLab CI →
🚀 Produkční server pro váš Docker stack
Reverse proxy, SSL terminace, několik kontejnerů pod zátěží a limity, které dávají smysl jen tehdy, když je zdrojů skutečně dost. Hostiserver dává předvídatelnou infrastrukturu přesně pro tento scénář.
🖥️ Dedikované servery
- Od $90/měsíc, plná kontrola nad hardwarem pro produkční zátěž
- Bez sousedů o zdroje: limity kontejnerů se počítají z reálného, ne sdíleného výkonu
- Podpora 24/7: pomůžeme se systemd jednotkou, Nginx a obnovou certifikátů
💻 Cloud (VPS) hosting
- Od $19.95/měsíc, KVM izolace, vyhrazené vCPU a RAM
- Vyhrazená IP adresa: správná funkce DNS a vydání certifikátu Let's Encrypt bez překvapení
- NVMe disky: logy, certifikáty a data databáze bez propadu výkonu
- Snadné škálování: víc RAM pro nové limity, když stack roste
💬 Nejste si jistí, kterou variantu potřebujete?
💬 Napište nám — pomůžeme s výběrem!
Časté dotazy
- Proč se Certbot instaluje na host, a ne spouští v kontejneru?
Je to jednodušší pro automatickou obnovu: není potřeba prokopávat další svazky nebo sokety mezi kontejnerem Certbot a Nginx, cron úloha na hostu přímo volá systémový příkaz a rovnou restartuje Nginx přes
docker compose exec. Kontejnerizovaný Certbot je taky možný, ale přidává pohyblivé části bez citelné výhody pro jeden server.
- restart: always nebo unless-stopped — jaký je v praxi rozdíl?
Rozdíl se projeví jen tehdy, když byl stack zastaven ručně přes
docker compose stop. SalwaysDocker kontejner znovu spustí, i když byl zastaven vědomě kvůli údržbě. Sunless-stoppedho nespustí, dokud ho sami znovu nespustíte. Pro produkční stack jeunless-stoppedzpravidla bezpečnější volba.
- Udělali jsme docker compose down a stack se sám nerozjel — je to chyba restart policy?
Ne, to je očekávané chování.
docker compose downkontejnery úplně smaže, ne jen zastaví — restart policy se týká existujících kontejnerů, a tady prostě není co restartovat. Přesně pro tento scénář je potřeba systemd jednotka, která znovu provede kompletnídocker compose up -dpo restartu hostu.
- Nginx nebo Traefik pro malý self-hosted projekt?
Pro jednu až dvě aplikace na jednom serveru je Nginx jednodušší a nepotřebuje přístup k Docker socketu. Traefik začíná vyhrávat, když je služeb hodně a často se mění — konfigurace přes labels pak šetří čas oproti ručnímu upravování
nginx.confpři každé změně.
- Jak ověřit, že limit paměti opravdu funguje?
docker statsukazuje aktuální spotřebu v reálném čase — a pokud se kontejner blíží nastavenémulimits.memory, lze to vidět dřív, než dojde k SIGKILL. Samotné spuštění limitu je vidět vdocker compose psnebodocker inspect: kontejner se restartuje s exit kódem typickým pro OOM.
- Lze obnovit SSL certifikát bez výpadku?
Ano, to je standardní scénář.
certbot renewaktualizuje soubory certifikátu na disku bez zastavení Nginx, a deploy hook zavolánginx -s reload, který znovu načte konfiguraci a certifikáty a zachová už otevřená spojení. Výpadek nastane jen při volbě standalone režimu pro získání prvního certifikátu, kdy je Nginx potřeba zastavit na pár sekund.
- Po reboot se stack automaticky nerozjel — kde začít s diagnostikou?
Nejdřív
systemctl status app.serviceajournalctl -u app.service— ukážou, jestli se jednotka vůbec spustila a v jakém kroku spadla. Typické příčiny: jednotka nebyla zapnutá přessystemctl enable, nebo nastartovala dřív než samotný Docker daemon kvůli chybějícímu nebo nesprávnémuAfter=v sekci[Unit].