Deploy / produkce
Staging instance nové Symfony aplikace běží na https://beta.slack.cz. Cutover na slack.cz ještě neproběhl — legacy PHP app stále běží na starém boxu (154.43.62.26).
Kompletní audit setup procesu je na produkčním serveru v /root/deploy.log (ssh deploy@... && sudo cat /root/deploy.log). Tenhle dokument je high-level reference; deploy.log je ground truth co se reálně spustilo.
Infrastruktura na první pohled
Tři prostředí:
| Prostředí | Kde | Co tam je | Spravované jak |
|---|---|---|---|
| Dev | tvůj laptop, Docker Compose | Apache + PHP-FPM + Postgres 16 + MySQL legacy + Adminer + Mailpit. Bind-mount repa. | docker compose + make dc* targety, viz dev.md |
| CI | GitHub Actions (.github/workflows/deploy.yml) |
ephemeral Ubuntu runner. Triggered push do main + workflow_dispatch. Joby: preflight → deploy. |
YAML inlinuje ssh ... 'bash -s' < scripts/<X>.sh, žádný copy-paste z deploy.sh |
| Prod (beta.slack.cz) | Hetzner CX22 (178.105.81.158), Ubuntu 24.04 |
Native PHP 8.3 + Postgres 16 + Caddy + ufw + fail2ban. Žádný Docker. App v /var/www/slack-cz jako deploy user. |
Skripty v scripts/, viz tabulka níž |
Pět skriptů drží celou prod stranu. Skript = spec. Když přidáš novou závislost (PHP extension, writable dir, env klíč), updatuj patřičný skript ve stejném commitu jako kód — další deploy padne fail-fast, dokud server nedoinstaluješ.
| Skript | Kdy se pouští | Co dělá |
|---|---|---|
scripts/setup-server.sh |
1× při fresh louce (ssh root@HOST), pak občas update-za-chodu (ssh deploy@HOST) |
Idempotentní provisioning: apt packages (PHP+ext, Postgres, Caddy přes vlastní apt repo), deploy user + NOPASSWD sudoers, git clone, Postgres role/DB/.env.local atomicky, ACL pro www-data na writable dirs, enable+start systemd. Nikdy nepřepisuje .env.local, nedropuje DB, nezasahuje do Caddyfile. Spustí se přes make setupServer. |
scripts/check-server-env.sh |
Před každým deployem (lokál i CI), volitelně manuálně před git push |
Preflight gate. Verifikuje git stav (server HEAD vs lokál), PHP+extensions, GD WebP, sys binárky, služby, FS perms www-data, .env.local klíče, Postgres connect, pending migrace. Exit 1 = deploy zhasne. Spustí se přes make checkServerEnv. |
scripts/check-caddy.sh |
Vedle check-server-env.sh před každým deployem (lokál i CI) |
Drift gate. SSH sudo cat /etc/caddy/Caddyfile, diff vs infra/Caddyfile. Exit 1 = drift, deploy zhasne. Spustí se přes make checkCaddy. |
scripts/deploy-caddy.sh |
Manuálně kdykoliv změníš infra/Caddyfile |
scp do /tmp → sudo caddy validate → atomic cp do /etc/caddy/Caddyfile → sudo systemctl restart caddy (NE reload, viz Gotchas) + smoke test. Spustí se přes make deployCaddy. NIKDY se nevolá z make deploy ani z CI — Caddy restart je side-effect, který má vlastní rozhodovací bod. |
scripts/deploy.sh |
Při každém deployi (lokál make deploy i CI deploy job) |
git pull, mkdir -p writable dirs, composer install --no-dev, doctrine:migrations:migrate, asset-map:compile, cache:clear (×2 — composer hook + explicit), cache:pool:clear cache.app, systemctl reload php8.3-fpm. |
scripts/sync-beta-restore.sh |
Po make syncBetaFromLocal (přes SSH stdin) |
Destruktivní psql restore lokálního dumpu na betač + cache:clear. Pouze staging fáze. |
Flow při běžné změně kódu:
git commit + push → CI:preflight (check-server-env.sh + check-caddy.sh) → CI:deploy (deploy.sh)
│
↓ pokud kterýkoli fail
deploy se nezačne, server zůstává na předchozí verzi
Flow při změně závislostí (nová PHP ext, writable dir, env klíč):
1. update spec v skriptu (setup-server.sh PKGS list / check-server-env.sh REQUIRED_*)
2. update kód
3. commit oboje spolu
4. ssh deploy@HOST → make setupServer (nebo přes Makefile target setupServer)
5. push → CI preflight projde → deploy projde
Cesta zpět ke konkrétním detailům je v sekcích dál v tomhle dokumentu (Server, Stack, Aplikace, Caddy, Operace, Gotchas).
Server
| Provider | Hetzner Cloud |
| Tarif | CX22 (x86_64, 2 vCPU, 4 GB RAM, 40 GB NVMe), ~€4.5/měs + IPv4 ~€0.7/měs |
| IPv4 | 178.105.81.158 |
| IPv6 | 2a01:4f8:1c18:6966::1/64 |
| OS | Ubuntu 24.04.3 LTS (kernel 6.8.0-111-generic) |
| Hostname | slack-cz-prod |
| Timezone | Europe/Prague |
Přístup
ssh -i ~/.ssh/slack_cz_prod deploy@178.105.81.158
| User | deploy (uid 1000, sudoer s NOPASSWD via /etc/sudoers.d/deploy) |
| SSH key | ed25519 (lokálně ~/.ssh/slack_cz_prod, na serveru v /home/deploy/.ssh/authorized_keys) |
| Root SSH | zakázán (PermitRootLogin no v /etc/ssh/sshd_config) |
| Password auth | zakázán (PasswordAuthentication no) |
| Backup sshd config | /etc/ssh/sshd_config.pre-lockdown.bak (na revert) |
⚠ Klíč
~/.ssh/slack_cz_prodje bez passphrase (dedicated deploy key, ne osobní). Drž ho v bezpečí — ztráta = vykop někoho přes Hetzner Console (KVM web shell) → znovu nahrát pubkey do/home/deploy/.ssh/authorized_keys.
Bezpečnost
| Opatření | Stav |
|---|---|
ufw firewall |
aktivní, povolené 22/tcp, 80/tcp, 443/tcp |
fail2ban |
aktivní, default jail sshd (5 fails / 10 min → 10 min ban) |
| Automatické security upgrades | dosud manuální, plánovat unattended-upgrades |
Stack (native, žádný Docker)
| Vrstva | Komponenta | Verze | Endpoint |
|---|---|---|---|
| Reverse proxy + auto-HTTPS | Caddy | 2.11.2 | :80, :443 |
| App | PHP-FPM | 8.3.6 | unix socket /run/php/php8.3-fpm.sock |
| DB | PostgreSQL | 16.13 | 127.0.0.1:5432 (jen lokálně) |
| Composer | apt package | 2.7.1 | — |
PHP extenze: pgsql, mbstring, xml, curl, zip, intl, opcache, readline, mysql (+ mysqli/pdo_mysql — Doctrine registruje old connection na boot, nevyužívá ji), gd, exif (foto galerie — origin EXIF strip + thumby přes liip_imagine).
Žádný MySQL na prod. Legacy MySQL je jen v dev pro import. Pro produkci se data převedou lokálně do Postgresu a pošlou jako
pg_dumpna server.
Aplikace
| Path | /var/www/slack-cz |
| Owner | deploy:deploy |
| Repo | https://github.com/petr-panoska/slack-cz.git (public, HTTPS clone) |
| Branch | main |
.env.local |
mode 640, owner deploy:www-data (PHP-FPM jako www-data musí číst) |
var/cache, var/log |
ACL pro www-data (rwX recursive + default), plus deploy (kvůli composer scriptům) |
public/uploads/, public/media/cache/ |
ACL pro www-data (rwX recursive + default). Uploads = origin fotky (vich/uploader), cache = liip_imagine on-demand thumby. Caddy file_server je servíruje staticky. |
.env.local obsahuje (na serveru, nikde jinde):
APP_ENV=prod
APP_DEBUG=0
APP_SECRET=<random hex 32>
DATABASE_URL=postgresql://slack_cz:<random>@127.0.0.1:5432/slack_cz?serverVersion=16&charset=utf8
OLD_DATABASE_URL=mysql://nobody:nobody@127.0.0.1:3306/none?serverVersion=8.0
MAILER_DSN=null://null
APP_URL=https://beta.slack.cz
APP_URLčteframework.router.default_uri(vizconfig/packages/routing.yaml) — bez něj by CLI commandy (např.app:user:reset-password) generovaly URL nahttp://localhost. Po cutoveru na apex změnit nahttps://slack.cz.
⚠ DB heslo a
APP_SECRETjsou random vygenerované při setupu, NIKDE jinde nezálohované. Když se ztratí.env.local, je třeba znovu vytvořit Postgres roli + heslo. Až bude vault / secrets management, přesunout sem.
Postgres
-- Role + DB (vytvořeno setupem)
CREATE ROLE slack_cz WITH LOGIN PASSWORD '<random>';
CREATE DATABASE slack_cz OWNER slack_cz;
GRANT ALL ON SCHEMA public TO slack_cz;
Doctrine migrations spuštěné php bin/console doctrine:migrations:migrate během deploy. Žádná legacy data zatím — schema je prázdné kromě Doctrine struktury.
Připojení z deploy usera:
sudo -u postgres psql -d slack_cz
Caddy
Source of truth: infra/Caddyfile v repu. Server kopii v /etc/caddy/Caddyfile udržujeme synchronně přes make deployCaddy. Drift detekuje make checkCaddy (volá se taky automaticky jako preflight v make deploy — fail-fast na neshodu).
| Akce | Příkaz | Co dělá |
|---|---|---|
| Drift check | make checkCaddy |
SSH sudo cat /etc/caddy/Caddyfile, diff vs infra/Caddyfile. Exit 1 = drift. |
| Push verze z repa | make deployCaddy |
scp → sudo caddy validate → atomic cp → sudo systemctl restart caddy + smoke test. |
| Stáhnout server verzi | ssh deploy@beta 'sudo cat /etc/caddy/Caddyfile' > infra/Caddyfile |
Kdyby někdo edit-nul ručně na serveru a chceš to zpětně commitnout (raději ne). |
Workflow: změnu Caddy konfigurace dělej v infra/Caddyfile, commitni do repa, pusť make deployCaddy. Nikdy needituj /etc/caddy/Caddyfile přímo na serveru — drift check ti to při příštím deploy zachytí, ale udržíš tím repo jako jediný kanonický stav.
Cert pro beta.slack.cz |
Let's Encrypt E8, valid 90 dní (auto-renew) |
| Logy | journalctl -u caddy (default systemd journal, custom log files se nepoužívají kvůli apparmor restrikcím) |
| Restart | sudo systemctl restart caddy (NE reload — pokud se zasekne, viz "Gotchas") |
DNS
Cloudflare drží zónu slack.cz (NS: gemma.ns.cloudflare.com, thaddeus.ns.cloudflare.com).
Aktuálně přidané záznamy pro novou app:
| Type | Name | Value | Proxy |
|---|---|---|---|
| A | beta |
178.105.81.158 |
DNS-only (gray cloud) |
| AAAA | beta |
2a01:4f8:1c18:6966::1 |
DNS-only |
Proč DNS-only (gray cloud): Caddy auto-HTTPS přes Let's Encrypt potřebuje, aby HTTP-01 / TLS-ALPN-01 challenge dorazila přímo na origin. Když je Cloudflare proxy ON (orange cloud), CF zachytí request a Caddy cert nedostane. Pro produkci s CF proxy je řešení Cloudflare Origin Certificate nebo DNS-01 challenge přes CF API token — řešíme až později.
Po cutoveru: přidat slack.cz (A + AAAA) → 178.105.81.158 + AAAA, ze stejných důvodů zatím gray cloud.
Operace
SSH dovnitř
ssh -i ~/.ssh/slack_cz_prod deploy@178.105.81.158
Audit log setupu
sudo cat /root/deploy.log
Každý setup krok je tam s timestamp, vstupy, výstupy. Když se chceš podívat, "kde jsme přesně udělali co", jdi sem.
Symfony console (jako deploy user, prod env)
cd /var/www/slack-cz
APP_ENV=prod php bin/console <cmd>
Pro úkony co potřebují čtení .env.local jako www-data (test perms):
sudo -u www-data bash -c "cd /var/www/slack-cz && APP_ENV=prod php bin/console <cmd>"
Reset hesla / aktivace účtu, když nechodí maily
Dokud nebude nasazený externí SMTP relay (viz Cutover TODO níž), mailer na betě je null://null — registrační maily i password-reset maily se tiše zahazují. Workaround přes konzoli:
ssh -i ~/.ssh/slack_cz_prod deploy@178.105.81.158
cd /var/www/slack-cz
# 1) Najdi usera (filtr substringem, nebo --unverified pro neaktivované)
APP_ENV=prod php bin/console app:user:list -s pepa
APP_ENV=prod php bin/console app:user:list --unverified
# 2) Vygeneruj password-reset URL (email nebo id). URL pošli userovi ručně
# (Signal, Messenger, SMS, ...). Token je jednorázový a má lifetime z bundlu.
APP_ENV=prod php bin/console app:user:reset-password panda@example.com
APP_ENV=prod php bin/console app:user:reset-password 42
URL se generuje s absolute scheme + host přes framework.router.default_uri (= APP_URL v .env.local). Po cutoveru na slack.cz přepiš APP_URL v /var/www/slack-cz/.env.local na https://slack.cz a sudo systemctl reload php8.3-fpm.
Sync dat z lokálu na betu
Když chceš dostat aktuální stav lokální Postgres DB (highlines, users, crossings, ...) na beta.slack.cz:
make syncBetaFromLocal
Co target dělá:
pg_dumpz kontejneruslack-cz-database-1(plain SQL,--clean --if-exists --no-owner --no-privileges) do/tmp/slack-cz.sql.scppřes~/.ssh/slack_cz_prodnadeploy@178.105.81.158:/tmp/slack-cz.sql.- Na serveru spustí
scripts/sync-beta-restore.sh(přes SSH stdin), který:- vytáhne
DATABASE_URLz/var/www/slack-cz/.env.local, - strippne query string (
?serverVersion=...&charset=...) — Doctrine formát, kterýpsqlneumí, psql -v ON_ERROR_STOP=1 -f /tmp/slack-cz.sqljako roleslack_cz(správný ownership),APP_ENV=prod cache:clear --no-warmupjakowww-data,- vypíše row counts pro sanity check,
- smaže
/tmp/slack-cz.sqlna serveru.
- vytáhne
- Lokální
/tmp/slack-cz.sqlsmaže taky.
⚠ Destruktivní na betě. Dump má
DROP TABLE IF EXISTSpro všechny app tabulky → kompletní replace. Dokud je beta jen staging bez vlastních dat, je to OK. Až bude na betě reálný traffic / uživatelské změny, nahradit zaINSERT ... ON CONFLICTflow nebo migraci diff.
⚠ Drží to stejnou Doctrine migration version na lokále i betě (
doctrine_migration_versionsse kopíruje). Po sync běž rovnoumakedeploy nebo manuálnícomposer install + migrate -njen když na lokále přibyla nová migrace.
Deploy
make deploy
Co target dělá:
- SSH na
deploy@178.105.81.158přes~/.ssh/slack_cz_prod. - Spustí
scripts/deploy.sh(přesbash -sstdin):git pull --ff-only origin maincomposer install --no-dev --optimize-autoloaderAPP_ENV=prod doctrine:migrations:migrate -nAPP_ENV=prod asset-map:compileAPP_ENV=prod cache:clearAPP_ENV=prod cache:pool:clear cache.app(vyhodí docs/wiki LKG fallback)sudo systemctl reload php8.3-fpm(refresh opcache)
- Smoke test: HTTP status pro
/,/mapa,/wiki,/docs,/o-projektu.
Předpoklad: commit + push do origin/main máš lokálně hotový — make deploy jen tahá na serveru.
Ruční deploy (kdyby SSH script selhal)
ssh -i ~/.ssh/slack_cz_prod deploy@178.105.81.158
cd /var/www/slack-cz
git pull --ff-only origin main
composer install --no-dev --optimize-autoloader --no-interaction
APP_ENV=prod php bin/console doctrine:migrations:migrate -n
APP_ENV=prod php bin/console asset-map:compile
APP_ENV=prod php bin/console cache:clear
sudo systemctl reload php8.3-fpm
⚠ Pořadí matters:
composer installspustí přes post-install hookscache:clear+assets:install+importmap:install(viz Gotchas). Náš následnýasset-map:compileproto MUSÍ jít až po něm — jinak bymanifest.jsonodkazoval na staré hashe. Druhý explicitnícache:clearnení redundantní: vyčistí cache, kterou si composer hook právě naplnil starými hashe.
--ff-onlychrání před tím, abys neúmyslně nemergeoval, kdyby na betě někdo udělal lokální commit (typicky když SSH-režíruješ nějaký quick fix).
CI workflow
.github/workflows/deploy.ymlzrcadlí přesně tenhle flow —preflightjob spustíscripts/check-server-env.shpřes SSH, na němneeds:mádeployjob, který spustíscripts/deploy.shpřes SSHbash -s. Žádné inline duplikace.
Post-deploy smoke
Rychlý sanity check po deployi (běží lokálně, target je beta):
for path in / /mapa /login /register /reset-password /profile /o-projektu; do
printf '%s %s\n' "$(curl -s -o /dev/null -w '%{http_code}' https://beta.slack.cz$path)" "$path"
done
# /profile by mělo 302 (anon redirect na /login). Zbytek 200.
# Ověř, že nový CSS hash je v HTML a obsahuje aktuální classes:
CSS=$(curl -s https://beta.slack.cz/login | grep -oE 'assets/styles/app-[a-zA-Z0-9_-]+\.css' | head -1)
echo "CSS asset: $CSS"
curl -s "https://beta.slack.cz/$CSS" | grep -cE 'auth-page|panel' # nenulové = nové styly živé
Logy aplikace
# Symfony prod log (vytvoří se při prvním logu)
tail -f /var/www/slack-cz/var/log/prod-*.log
# Caddy (HTTP přístupy, cert events)
sudo journalctl -u caddy -f
# PHP-FPM
sudo tail -f /var/log/php8.3-fpm.log
# fail2ban
sudo journalctl -u fail2ban -n 50
Restart služeb
sudo systemctl restart php8.3-fpm
sudo systemctl restart caddy
sudo systemctl restart postgresql
sudo systemctl restart fail2ban
Cutover na slack.cz — TODO
Než se traffic přepne ze starého 154.43.62.26 na nový VPS:
- Doimport legacy dat — lokálně dotáhnout MySQL → Postgres user/highline/crossing import (částečně hotovo dle
migration.md), pakpg_dumpz lokálu,pg_restorena produkci. - MAILER_DSN — externí SMTP relay (Brevo / Mailgun / Postmark / ...), ne vlastní mailserver. Hetzner blokuje port 25 outbound default.
- DNS swap pro
slack.czv Cloudflare:- Změnit A z legacy IP na
178.105.81.158 - Přidat AAAA na
2a01:4f8:1c18:6966::1 - Gray cloud (DNS-only) kvůli Caddy auto-HTTPS
- Změnit A z legacy IP na
- Caddyfile pro
slack.cz— vinfra/Caddyfilepřidat blok analogický kbeta.slack.cz(sdílejí@photosmatcher). Pokud chcemewww.slack.czredirect na apex, přidat redir block. Pakmake deployCaddy. - YouTube API key + GitHub PAT — dostat reálné hodnoty do
.env.local(YOUTUBE_API_KEY,DOCS_GITHUB_TOKEN). - (volitelné)
unattended-upgradespro automatické security patche. - (volitelné) snapshot/backup přes Hetzner Cloud — pravidelné snapshoty disku stojí ~20 % ceny serveru.
Gotchas (z reálného setupu, nech tu, ať to nezblbneš znovu)
Caddy reload se zasekne, když config selhal
Když ti systemctl reload caddy vrátí timeout (zejména po prvním deploy), pravděpodobně předchozí ExecReload selhal s "permission denied" nebo podobně, a systemd visí v reloading stavu. Recovery:
sudo systemctl reset-failed caddy
sudo systemctl restart caddy
Restart (ne reload) zabije starý proces a startuje fresh.
.env.local musí číst www-data
Default mode 600 / owner deploy:deploy způsobí 500 — PHP-FPM jako www-data nemůže .env.local přečíst. Musí být:
sudo chown deploy:www-data /var/www/slack-cz/.env.local
sudo chmod 640 /var/www/slack-cz/.env.local
DNS records dřív než Caddy site block
Když přidáš site blok do Caddyfile před tím, než existuje DNS pro doménu, Caddy začne hammerovat ACME challenge → public resolvery (zejména Google 8.8.8.8) si uloží NXDOMAIN do negativní cache podle SOA min TTL zóny (slack.cz má 1800s = 30 min).
Po přidání DNS pak browsery uživatelů mohou ještě hodinu vidět jen AAAA (pokud byla přidána později než A) → fail na "no IPv4 + no IPv6 routing doma".
Pravidlo: vždy nejdřív DNS, pak Caddy. Když se to udělá obráceně, čekat 30 min nebo přepnout DNS upstream na 1.1.1.1 / 9.9.9.9.
psql neumí Doctrine DATABASE_URL query string
Symfony / Doctrine zapisuje DSN jako postgresql://user:pass@host:port/db?serverVersion=16&charset=utf8. Když ten celý URL pošleš do psql, vrátí:
psql: error: invalid URI query parameter: "serverVersion"
Fix: před voláním psql strippni vše po ?:
DB_URL=$(grep -E '^DATABASE_URL=' .env.local | sed -E 's/^DATABASE_URL=//; s/^"//; s/"$//; s/\?.*$//')
psql "$DB_URL" -c "..."
Tohle dělá scripts/sync-beta-restore.sh — kdybys chtěl ručně něco pgčíst na betě, použij stejný pattern.
Doctrine old EM vyžaduje php-mysql
I když na prod nepoužíváme MySQL, config/packages/doctrine.yaml registruje old connection na boot. Bez php8.3-mysql extension by se Doctrine ani nezavedlo. Setup ho instaluje, OLD_DATABASE_URL v .env.local ukazuje do prázdna (mysql://nobody:nobody@127.0.0.1:3306/none), connection se nikdy neotevře.
var/log neexistuje, dokud Symfony poprvé nezaloguje
Na čistém serveru po prvním deployi je var/cache/ (vytvořila composer post-install hook), ale var/log/ ještě ne — Symfony ji vytváří lazily při prvním zápisu logu. Když si do deploy skriptu šoupneš defenzivní chown -R deploy:www-data var/log před prvním zalogováním, padne chown: cannot access 'var/log': No such file or directory.
Buď:
[ -d var/log ] && sudo chown -R deploy:www-data var/log
nebo nech ji vzniknout přirozeně (po prvním requestu se vytvoří jako www-data, což je správně — PHP-FPM tam zapisuje).
Preflight check (make checkServerEnv + CI gate)
Před každým deployem (lokální make deploy i GH Actions push-to-main) se pouští scripts/check-server-env.sh jako fail-fast gate:
- Lokálně: Makefile
deploy: checkServerEnvdependence. - CI:
.github/workflows/deploy.ymlmá samostatnýpreflightjob,deployjob na němneeds:. Stejný skript, stejný SSH klíč (DEPLOY_SSH_KEYsecret).
Skript běží na serveru přes SSH a verifikuje:
- Git stav (server HEAD vs lokální HEAD, kolik commitů pozadu, jestli přibyly nové migrace / composer.lock změny)
- PHP verze ≥ 8.3, všechny vyžadované extensions (
pgsql,gd,exif, …) + GD má WebP support - Systémové binárky (
composer,caddy,psql,setfacl) - Běžící služby (
caddy,php8.3-fpm,postgresql) - FS perms pro PHP-FPM (
www-dataumí zapsat dovar/cache,var/log,public/uploads,public/media/cache) .env.localčitelnáwww-dataem + obsahuje všechny očekávané klíče- Postgres
DATABASE_URLconnect funguje - Pending Doctrine migrace (info)
Když cokoli failne, deploy se vůbec nezačne (exit 1 → CI job fail → deploy job se neaktivuje, lokálně make deploy rovněž zhasne). Pustit samostatně před push:
make checkServerEnv
To je doporučená cesta pro „pre-push manual check" — žádný git hook se neinstaluje, devloák co chce fast feedback prostě pustí target před git push.
Skript = spec. Když přidáš novou závislost (PHP extension, writable dir, env klíč) updatuj scripts/check-server-env.sh ve stejném commitu jako kód. Lokální verze skriptu je kanonický expected-state — server se kontroluje proti tomu, co se chystá deploynout, ne proti aktuální server-side verzi.
Skript spoléhá na to, že deploy user má NOPASSWD sudoers pro sudo -u www-data ... (jinak FS-perm checky se přeskočí s warningem). Setup sudoers entry:
# /etc/sudoers.d/deploy (visudo -f)
deploy ALL=(ALL) NOPASSWD: ALL
Server provisioning — scripts/setup-server.sh
Jediný idempotentní skript pro dvě role:
-
Fresh louka — čerstvé Hetzner image (Ubuntu 24.04). Spustit jako root:
ssh root@HOST 'bash -s' < scripts/setup-server.shNainstaluje apt packages (PHP 8.3 + ext, Postgres 16, Caddy přes vlastní apt repo, git, acl, …), vytvoří
deployusera + NOPASSWD sudoers, naklonuje repo do/var/www/slack-cz, vytvoří Postgres roli + DB +.env.localatomicky (randomAPP_SECRET+ DB heslo, mode 640, ownerdeploy:www-data), nastaví ACL prowww-datana writable adresáře (var/cache,var/log,public/uploads,public/media/cache), enable + start systemd služeb. -
Update za chodu — když přibyla nová závislost (PHP extension, writable dir). Spustit jako deploy:
ssh deploy@HOST 'bash -s' < scripts/setup-server.shStejný skript, ale díky idempotenci no-opuje vše už hotové. Reálně se přeinstalují jen nově přidané apt packages a refresh ACL.
.env.localse v žádném scénáři nepřepisuje.
Co skript NEDĚLÁ úmyslně:
- nezasahuje do
/etc/caddy/Caddyfile— řízeno separátně přesinfra/Caddyfile+make deployCaddy(viz sekce Caddy výš) - nedropuje žádné Postgres role / DB / data
- nepřepisuje
.env.local— pokud existuje, skipuje regeneraciAPP_SECRET/DB hesla (jejich ztrátu chceme prevent)
„Weird state" detekce: pokud existuje .env.local, ale Postgres role chybí (nebo opačně), skript exit 1 s instrukcí ručního recovery — odmítne uhodnout heslo a generovat nový (rozbil by connect / cache invalidace).
Po fresh setupu zbývá ručně:
- Caddy konfig — push z repa:
make deployCaddy(předtím vinfra/Caddyfiledoplň site blok pro novou doménu, viz sekce Caddy výš) - SSH klíč pro
deploy(ssh-copy-id) — pokud setup běžel jako root, deploy zatím SSH nemá - GH repo secret
DEPLOY_SSH_KEY(privátní klíč pro deploy usera) - DNS records v Cloudflare na IP tohoto serveru
Pak make checkServerEnv + make checkCaddy z lokálu pro verifikaci.
Composer post-install scripts spouštějí cache:clear v prod
Po composer install --no-dev Symfony auto-spustí cache:clear, assets:install, importmap:install. To je důvod proč var/cache musí být writable PHP procesem ještě před prvním bin/console cache:clear.