Deploy, při kterém spadne pár requestů, nikoho nezabije — ale nedůvěra se nasčítá. Uživatel dostane 502, formulář se neodešle, monitoring pípne. A když se něco pokazí, chcete být zpátky na staré verzi hned — ne pět minut cvičit s Gitem na produkci a modlit se. Dobrá zpráva: nasazení bez výpadku není raketová věda ani výsada týmů s Kubernetes. Stačí tři osvědčené kusy a jedna myšlenka. V tomhle článku složíme sestavu, která běžný web nasadí bez jediného zahozeného spojení: Nginx jako reverse proxy, HTTPS přes Let's Encrypt s automatickou obnovou a nasazování přepnutím jediného symlinku. Rollback? Jeden příkaz a jste zpátky.
Všechny příkazy platí pro Debian 12 a Ubuntu 22.04+. Předpokládám, že máte doménu směrovanou na server a pracujete jako uživatel s sudo.
Adresářová struktura, ze které to celé vychází
Základ atomického nasazení je oddělit kód konkrétní verze od toho, co je právě naostro. K tomu se osvědčila tři místa:
releases/— každá verze ve vlastním, časově pojmenovaném adresářishared/— věci, které přežívají napříč verzemi:.env, nahrané soubory, logycurrent— symlink, který ukazuje na právě aktivní release
sudo mkdir -p /var/www/app/{releases,shared}
Aplikace i Nginx pak sahají výhradně na /var/www/app/current. Nikdy neukazují přímo do releases/. Celý deploy se scvrkne na jedinou operaci: přepnout, kam current míří.
Nginx jako reverse proxy
Aplikace (Node, PHP-FPM, Python, cokoli) běží na localhostu, třeba na portu 3000. Nginx stojí před ní, ukončuje HTTPS a předává provoz dál. Konfiguraci dejte do /etc/nginx/sites-available/:
sudo apt update && sudo apt install nginx
server {
listen 80;
server_name example.com www.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# když aplikace používá WebSockety
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
Ty čtyři hlavičky nejsou kosmetika. Bez X-Forwarded-Proto aplikace netuší, že přišla přes HTTPS, a klidně vám udělá redirect smyčku. Bez X-Forwarded-For uvidíte v logu pořád jen IP Nginxu.
Aktivujte site symlinkem a nasypte to naostro:
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
nginx -t je návyk, který vám jednou zachrání večer — otestuje konfiguraci dřív, než ji nasadíte. A reload (na rozdíl od restart) je řízený: Nginx spustí nové workery, staré nechá dokončit rozdělané requesty a teprve pak je ukončí. Žádné zahozené spojení.
HTTPS přes Let's Encrypt
Na Debianu a Ubuntu je nejpřímější cesta certbot z balíčků i s pluginem pro Nginx:
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com
Certbot si vyžádá certifikát a sám dopíše do konfigurace listen 443 ssl, cesty k certifikátům i include s doporučenými SSL parametry. Jednu věc ale za vás automaticky neudělá: přesměrování z HTTP na HTTPS. V interaktivním režimu se na něj zeptá dotazem, jestli chcete provoz přesměrovat — vyberte volbu Redirect. Když certbot spouštíte bezobslužně, dopřejte mu přepínač --redirect (nebo --no-redirect, pokud ho výslovně nechcete). Bez toho zůstane HTTP i HTTPS vedle sebe.
Certifikáty od Let's Encryptu platí 90 dní, takže obnova je nutnost, ne detail.
Automatická obnova
Tady je dobrá zpráva: balíček certbotu automatickou obnovu nastaví za vás. Nainstaluje systemd timer, který dvakrát denně spustí certbot renew. Ten se ale nesnaží obnovovat pořád — sáhne jen na certifikáty, kterým zbývá méně než třetina životnosti. U standardního 90denního certifikátu od Let's Encryptu to vychází zhruba na 30 dní před expirací (starší verze certbotu měly tuhle hranici napevno na 30 dnech, novější počítají třetinu životnosti, takže u kratších certifikátů je práh nižší). Aktuální certbot navíc umí ctít ARI a obnovit i dřív, pokud to certifikační autorita doporučí. Prakticky z toho plyne jediné: timer klidně nechte běžet dvakrát denně, obnova se spustí sama ve správnou chvíli. Ověřte, že timer opravdu jede:
systemctl list-timers | grep certbot
A vyzkoušejte, že obnova reálně proběhne, nanečisto:
sudo certbot renew --dry-run
Když jste certifikát vydali s --nginx, certbot po obnově Nginx sám reloadne. Pokud používáte jiný způsob ověření (webroot, standalone) nebo potřebujete po obnově nakopnout ještě vlastní službu, dejte skript do adresáře deploy hooků — spustí se po každé úspěšné obnově:
# /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
#!/usr/bin/env bash
systemctl reload nginx
Certifikát, který se neobnoví sám, je jen odložený výpadek s datem splatnosti.
Atomické nasazení přes symlinky
Teď to hlavní. Novou verzi nepřepisujeme na místě — vždycky ji celou nachystáme stranou, a teprve když je hotová, přepneme na ni current.
Proč zrovna symlink? Protože jeho přepnutí jde udělat atomicky. Systémové volání rename() buď proběhne celé, nebo vůbec — nikdy neexistuje okamžik, kdy by current mířil „napůl". Jenže pozor: samotné ln -sfn atomické není. Interně nejdřív starý symlink smaže a pak vytvoří nový, a mezi tím je štěrbina, do které se vám může trefit request. Trik je vytvořit dočasný symlink a přejmenovat ho přes mv -T, které pod kapotou volá právě rename():
ln -sfn "$NEW_RELEASE" /var/www/app/current_tmp
mv -Tf /var/www/app/current_tmp /var/www/app/current
-T (--no-target-directory) říká „ber cíl vždycky jako obyčejný soubor, ne jako adresář", takže se nový symlink nevloží dovnitř toho, na co current právě ukazuje. Nepleťte si ho s -n (--no-dereference) — ten dělá něco slabšího a při současném zadání ho -T stejně přebije. Tohle -T je celé kouzlo za slovem „atomické".
Deploy skript
Poskládané dohromady vypadá nasazení takhle. Konkrétní build (npm, composer, cokoli) si dosaďte podle svého stacku:
#!/usr/bin/env bash
set -euo pipefail
APP=/var/www/app
NEW_RELEASE="$APP/releases/$(date +%Y%m%d%H%M%S)"
# 1. nachystat novou verzi stranou
mkdir -p "$NEW_RELEASE"
git clone --depth 1 [email protected]:app.git "$NEW_RELEASE"
# ... build, instalace závislostí ...
# 2. napojit sdílené soubory
ln -sfn "$APP/shared/.env" "$NEW_RELEASE/.env"
ln -sfn "$APP/shared/storage" "$NEW_RELEASE/storage"
# 3. atomicky přepnout current
ln -sfn "$NEW_RELEASE" "$APP/current_tmp"
mv -Tf "$APP/current_tmp" "$APP/current"
# 4. řízený reload aplikace a Nginxu
sudo systemctl reload app.service
sudo nginx -t && sudo systemctl reload nginx
set -euo pipefail na začátku je dobrý zvyk: skript se zastaví, jakmile něco selže, a nezačne slepě pokračovat dál. Nedělejte si z něj ale železnou záruku. set -e má svoje slepá místa — neaktivuje se uvnitř if, ve while, v řetězci &&/||, po !, a třeba local x=$(...) návratový kód spolkne. A hlavně: nic nevrací zpět. Když spadne pozdější krok, adresáře a soubory z těch dřívějších tam prostě zůstanou. Poloviční nasazení tedy neeliminuje set -euo pipefail, ale ten princip pod ním: build běží stranou v samostatném adresáři a naostro se dostane až jediným atomickým přepnutím symlinku. set -euo pipefail tomu jen pomáhá tím, že běh přeruší dřív, než se k přepnutí vůbec dostane — dokud mv neproběhne, jede naostro pořád stará verze.
Poznámka na rovinu: nulový výpadek nakonec stojí a padá s tím, jak umí graceful přechod vaše aplikace. Nginx svou část odbaví bez díry, ale samotnou appku musíte přepnout tak, aby dobojovala rozdělané requesty. Možností je několik:
pm2 reload— bez výpadku, ale jen v cluster módu, kdy pm2 vyměňuje workery po jednom (rolling restart). Ve fork módu z toho je obyčejný stop/start s dírou.systemctl reloadu služby, která má definovanýExecReload— typicky pošle procesu SIGHUP a ten si znovu načte konfiguraci za běhu.- socket aktivace v systemd — tady je bez výpadku i tvrdý
restart: naslouchající socket drží systemd, takže nová spojení čekají v kernelové frontě, než nový proces naběhne. - reload php-fpm (SIGUSR2) — plynulá výměna workerů.
Tvrdý restart bez socket handoveru uprostřed provozu naopak tu vteřinovou díru udělá.
Rychlý rollback
Protože stará verze pořád leží v releases/, návrat zpátky je jen přepnutí symlinku na předchozí adresář:
PREV=$(ls -1dt /var/www/app/releases/*/ | sed -n '2p')
ln -sfn "$PREV" /var/www/app/current_tmp
mv -Tf /var/www/app/current_tmp /var/www/app/current
sudo systemctl reload app.service
ls -1dt seřadí releasy od nejnovějšího, sed -n '2p' vezme druhý v pořadí — tedy tu verzi, co jela před posledním deployem. Žádný build, žádný Git, žádná panika. Vteřina.
Úklid starých releasů
Aby vám releases/ nesnědl disk, na konci deploye smažte všechno kromě posledních pěti verzí:
ls -1dt /var/www/app/releases/*/ | tail -n +6 | xargs -r rm -rf
tail -n +6 propustí řádky od šestého dál, xargs -r nic nespustí, když seznam náhodou vyjde prázdný. Pět verzí je rozumný kompromis — dost na to, abyste se měli kam vrátit, ne tolik, aby to bobtnalo.
Take-away
- Reverse proxy v Nginxu odděluje TLS a routing od aplikace;
reloadje řízený a spojení nezahazuje. - Certbot z apt vyřídí HTTPS i automatickou obnovu; přesměrování HTTP→HTTPS si ale vyžádá volbu Redirect nebo přepínač
--redirect.certbot renew --dry-runje jediný test, který vás od výpadku dělí. - Atomický symlink (
ln -sfndo dočasného +mv -T) přepne verzi bez štěrbiny. Samotnéln -sfnatomické není. - Rollback je jen přepnutí symlinku zpátky — proto starou verzi nemažte hned.
Celé to drží na jedné myšlence: nová verze se nikdy nedotkne té běžící, dokud není stoprocentně hotová. A když se něco pokazí, cesta zpátky vede stejnou branou, kterou jste přišli — jedním přepnutím symlinku.