Přeskočit obsah
V
Pro vývojáře
Architektura, konvence, jádro systému a bezpečnost
Začínáme / Checklist nasazení

Nasazení#

Naplánované úlohy#

Bez nich systém nefunguje správně. Úlohy jsou presentery v app/UI/Cron/, spouštějí se HTTP požadavkem s tokenem.

Skupina Co dělá Jak často
Fronta e-mailů rozesílá každých pár minut
Expirace upozornění a mazání prošlých inzerátů a zápisů denně
Přepočty počty v kategoriích, ceny 10–15 min
Hlídací psi rozesílá upozornění průběžně nebo denně
Platby dofakturace, vyhodnocení průběžně
Plánovač faktur opakovaná fakturace denně
Elasticsearch synchronizace a noční reindex 10 min / denně
Sitemapy generování, včetně e-shopových a indexu denně v noci, index jako poslední
Úklid notifikací /cron/system/notification/clean-orphans — odpojí notifikace, jejichž cílový záznam už neexistuje (zůstanou jako text bez odkazu, jinak by vedly v administraci na 404); každý běh projde dalších 500 starších notifikací (pozici si pamatuje v cache, po smazání cache začne od nejnovějších); sám se nespustí častěji než 1× za 20 h denně
Retence notifikací /cron/system/notification/purge-read — maže přečtené notifikace starší než notification.readRetentionDays (výchozí 90 dní), nepřečtené nikdy denně
Auth log /cron/system/auth-log/purge-old — maže staré záznamy přihlášení (výchozí 90 dní) denně
Fronta e-mailů — retence /cron/system/email-queue/purge-old — maže vyřízené e-maily starší než email.queue.retentionDays (výchozí 30 dní, Nastavení → E-mail); těla e-mailů nemají zůstávat v databázi donekonečna denně
Dočasné soubory uploaderu /cron/system/dropzone-temp/clean — maže v temp/dropzone soubory a složky starší než 24 h (nedokončené uploady) denně
Zrušení účtů /cron/system/user/anonymize-deleted — anonymizuje účty, kterým uplynula lhůta po žádosti o zrušení (account.deletion.graceDays, výchozí 7 dní); bez něj žádosti jen čekají denně
Logy /cron/system/log/purge-old — maže výpisy chyb Tracy starší než log.retentionDays (výchozí 30) a *.log nad log.maxSizeMb (výchozí 50) odkládá na .log.1 (3 generace); Nastavení → Cron denně

Co nastavit před nasazením

Kromě modulových úloh (fronta e-mailů, expirace, přepočty, Elasticsearch, sitemapy) patří do plánovače vždy těchto pět úklidových úloh pod /cron/system/…: notification/clean-orphans, auth-log/purge-old, email-queue/purge-old, dropzone-temp/clean, log/purge-old, a k nim user/anonymize-deleted (zrušení účtů). Každá se spouští s ?token=<cron.token>. notification/clean-orphans, dropzone-temp/clean, log/purge-old a user/anonymize-deleted se samy hlídají, aby neběžely častěji než jednou za 20 h; auth-log/purge-old a email-queue/purge-old ne, ty zavěs jen denně.

Bez naplánovaných úloh se problémy projeví až za týdny

Inzeráty nevyprší, počty přestanou sedět, e-maily se hromadí. Nic to nehlásí. Po nasazení zkontrolujte, že úlohy skutečně běží.

Souběh úloh je ošetřený zámkem

Dvě instance téže úlohy se nespustí. Nespouštějte je ale zbytečně často — dlouhá úloha zablokuje tu následující.

Nasazení kódu#

Kód se na server dostává jediným krokem: skriptem deploy/deploy-demo.sh, který se spouští ručně z vlastního počítače a míří na váš vlastní server. Push do main sám nic nenasadí (workflow .github/workflows/deploy-demo.yml v repozitáři zůstal, ale nespouští se). Skript vezme přesně obsah commitu (git archive HEAD), nainstaluje composer závislosti bez dev balíčků, přes rsync přes SSH zrcadlí kód na server a v téže SSH relaci hned smaže cache.

deploy/deploy-demo.sh              # nasadí aktuální commit větve main
deploy/deploy-demo.sh --dry-run    # jen ukáže, co by rsync nahrál a smazal
deploy/deploy-demo.sh --allow-dirty # dovolí neuložené změny (nasadí se stejně jen commit)
deploy/deploy-demo.sh --skip-tests  # nouzově nasadí i commit bez zelených testů

Přístup k vašemu serveru (uživatel, cesta, klíč) se bere z deploy/deploy-demo.local.env, který není v gitu (vzor deploy/deploy-demo.local.env.example); připnutý klíč serveru (KNOWN_HOSTS_LINE) ve skriptu nahraďte klíčem svého serveru, stejně jako adresu webu, proti které běží smoke test. Skript odmítne běžet mimo větev main a s neuloženými změnami.

Nasadí se jen commit, který prošel testy: tests/run.sh T1 (nebo vyšší úroveň) po zeleném běhu na čistém stromu zapíše <commit> <úroveň> do tests/.last-run a skript bez záznamu pro aktuální HEAD skončí dřív, než cokoli nahraje. Úrovně testů popisuje Testy a nástroje.

Krok Provedení
výměna souborů rsync -rlptz --delete --delay-updates — soubory se nahrají stranou a přejmenují až na konci, takže výměna kódu trvá zlomek sekundy místo celého přenosu
co se nikdy nepřepíše ani nesmaže deploy/rsync-exclude.txt: config/local.neon, config/Shared/local.neon, config/debug-mode.flag, uploads, uploads_thumbs, feeds, sitemaps, private/pdf, private/eshop, temp, log, var
cache v temp/ smažou se jen soubory (find temp -mindepth 1 -type f -delete), adresáře zůstanou
SQL migrace bin/migrate.php po smazání temp/ a před mazáním Redisu; viz Migrace SQL
Redis klíče app:cache:q:* (query cache) a app:cache:route:* (route tabulka a mapy slugů) v databázi DEMO_REDIS_DB (výchozí 0, viz Více instalací na jednom serveru); relace ani verze tagů se nemažou
klíč serveru připnutý ve skriptu (KNOWN_HOSTS_LINE, StrictHostKeyChecking=yes); nepoužívá se ssh-keyscan, takže podvržený server nedostane deploy klíč. Nesedí-li klíč, ssh/rsync skončí dřív, než cokoli pošle
smoke test po nasazení skript spustí php tests/Manual/Verify/demo_smoke.php <adresa-webu> (jen čtení, bez přihlášení a bez zápisů): úvodní stránky všech jazyků, výpis každého modulu a z něj jeden detail, přihlašovací stránka administrace, robots.txt a sitemapa, na kterou odkazuje. Selhání = jiný stav než 200 (po přesměrování), chybová stránka nebo Tracy; pomalé stránky jen vypíše. Chybějící sitemapa je jen upozornění (generuje ji cron)
výsledek selže-li smoke test, kód je už nasazený, ale skript skončí s kódem 4 a nasazení hlásí jako neúspěšné; selhání migrace vrátí kód migrace
první požadavek trvá kolem 9 s (studená cache), další 0,4–1,1 s; smoke test web zároveň zahřeje, takže ho zaplatí skript, ne návštěvník

První požadavky po smazání cache mohou krátce selhat

Smoke test běží hned po smazání cache, kdy se DI kontejner teprve kompiluje, a první požadavky mohou krátce vrátit 500 nebo 503. Jednotlivé selhání proto ještě neznamená chybu nasazení: spusť smoke test znovu (php tests/Manual/Verify/demo_smoke.php <adresa-webu>). Teprve když chyba trvá, hledej příčinu v log/.

Mazání klíčů v Redisu vždy s xargs -r -d '\n'

Klíče nesou názvy tříd se zpětnými lomítky (app:cache:q:App\UI\…) a výchozí xargs je z argumentů zahodí. Dřívější redis-cli --scan … | xargs redis-cli del proto nenašlo klíč a smazalo 12 ze 4 457; cache jen doexpirovala po TTL. Správně:

redis-cli --scan --pattern 'app:cache:q:*' | xargs -r -d '\n' -n 200 redis-cli del

Po mazání ověř počet (po nasazení mají zůstat jen čerstvé klíče). Totéž platí pro každé ruční mazání.

Nahrání a smazání cache musí být JEDEN krok

Dřív to byly dva kroky a mezi nimi běžel nový kód se starým zkompilovaným DI kontejnerem: každá nově injektovaná závislost vyhodila Typed property … must not be accessed before initialization a RobotLoader: Unable to create …. Trvá sekundy a samo se srovná, ale je to na stránkách vidět. Smazaný adresář pod rukama běžícímu požadavku házel RobotLoader chybu, proto se mažou jen soubory.

SQL migrace pouští nasazení samo

Po nahrání souborů a smazání temp/ spustí skript bin/migrate.php; selže-li migrace, nasazení skončí chybou. Postup, pravidla a první běh viz Migrace SQL.

Serverový local.neon si drží vlastní enabledServices

Nová cachovaná služba se musí doplnit i tam, jinak se čte z databáze (jeden dotaz navíc na požadavek). Viz Cache.

Rotace logů#

Adresář log/ bez rotace roste a nikdo to nehlásí; nejvíc přibývají debug.log a auth.log. Zdroje, které psaly řádek na každý požadavek, byly odstraněny (whitelisty rout při každém sestavení routeru, INTERNAL_AUTH no_token při každém interním čtení, „Dynamic routes" a „Lazy warmup triggered" v překladačích); rozhodnutí RBAC, vadné tokeny, chyby a výjimky se logují dál.

Nastav logrotate

Denně, 3 komprimované generace, dřív při 200 MB; výpisy výjimek *--*.html|md starší 14 dní se mažou. Bez rotace log roste do zaplnění disku.

Migrace SQL#

Schéma se udržuje ručně psanými opakovatelnými skripty v sql/. Při nasazení je na databázi pouští bin/migrate.php — nic se už nespouští ručně a nikdo nemusí pamatovat pořadí.

Co nasazení udělá#

Po nahrání souborů a smazání temp/ spustí SSH krok bin/migrate.php (PHP 8.5, jinak 8.4, jinak výchozí php) a teprve potom maže Redis cache.

  1. Získá databázový zámek coolcms_sql_migrations (GET_LOCK, čeká 30 s) — teprve pod ním čte evidenci, takže dva souběžné běhy nikdy nepustí tentýž soubor dvakrát.
  2. Přečte, které soubory už na této databázi běžely (tabulka cms_system_sql_migrations: filename, checksum = SHA-256 obsahu, applied_at).
  3. Ověří checksumy už aplikovaných souborů. Liší-li se obsah některého od evidence, vypíše jeho název a skončí kódem 1 — nic nepustí.
  4. Vybere čekající migrace — soubory sql/YYYY-MM-DD_NN_popis.sql, které v evidenci nejsou — a spustí je jednu po druhé, podle názvu souboru.
  5. Po každém úspěšném souboru ho zapíše do evidence. Soubor, který je v evidenci, ale ve složce chybí, jen ohlásí varováním.

Výběr řeší App\Core\Utils\Helpers\SqlMigrationPlanner:

Soubor Pustí se?
YYYY-MM-DD_NN_popis.sql ano, jednou
*_rollback.sql nikdy — rollback se pouští ručně
*_dev_pii_anonymization* nikdy — vývojové skripty
cokoli jiného v sql/ (jiný tvar jména) ne

Kód se nahraje PŘED migracemi

Pořadí kroků je: nahrát soubory → smazat temp/ → migrace → smazat Redis cache. Mezi nahráním kódu a koncem migrace proto krátce běží nový kód nad starým schématem. Migrace, která přidává sloupec, který nový kód čte (dřív se jim říkalo „před kódem“), způsobí po tu dobu chyby na dotčených stránkách. Má-li být okno nulové, rozděl změnu na dvě nasazení: nejdřív migrace, kterou starý kód nepozná, a v dalším nasazení kód, který ji používá.

První běh a základní stav#

Na databázi, kde evidence ještě není, se nedá zjistit, co se dřív pustilo ručně. Bez evidence proto bin/migrate.php skončí kódem 1 a jen vypíše, co by vzal za hotové — nic neoznačí potichu. První běh se pouští ručně s --baseline:

php bin/migrate.php --baseline

Ten tabulku založí a všechny migrace, jejichž název je před 2026-10-02_08 (konstanta SqlMigrationPlanner::BASELINE), jen zapíše jako hotové — nespustí je. Od 2026-10-02_08 dál se spouští vše. Na databázi, kde evidence už je, se --baseline nepoužije.

Nový server: první běh ne ze skriptu nasazení

Nasazení --baseline nepředává. Na novém serveru bez evidence by krok migrací skončil chybou — první běh tam pusť ručně a teprve pak nasazuj.

Databáze musí být před prvním během dorovnaná ručně

Co je před základním stavem, se už nikdy samo nespustí. Databáze vzniklá ze staršího dumpu proto musí mít všechny starší migrace aplikované ručně dřív, než poprvé poběží nasazení. Týká se to i 2026-10-02_06_remove_google_settings.sql — spadá pod základní stav, takže ho na databázi, kde ještě neběžel, pusť ručně až po nasazení kódu.

Postup: zjistit, co nasazení pustí#

php bin/migrate.php --dry-run

Vypíše čekající migrace v pořadí spuštění (nebo „žádná čekající migrace“). Do databáze nezapíše nic, ani evidenci; na databázi bez evidence napíše, kolik starších souborů by se při prvním běhu vzalo za hotové. Bez přepínače se migrace pustí; příkaz jde spustit i ručně (např. po opravě selhané migrace). Připojení čte z parameters.mariadb v konfiguraci (config/Shared/parameters.neon, přebité config/Shared/local.neon a config/local.neon).

Co se stane při chybě#

Situace Chování
migrace selže skript vypíše soubor a chybu, skončí kódem 1, další migrace nepustí a selhaný soubor nezapíše do evidence; nasazení skončí chybou
migrace selže uprostřed souboru změny struktury (DDL) se nevrátí; soubor se při dalším nasazení pustí znovu od začátku
soubor proběhl, ale zápis do evidence selhal skript skončí chybou; soubor se příště pustí znovu
migrace běží zároveň ještě jinde skript čeká na zámek coolcms_sql_migrations 30 s, pak skončí chybou
obsah už aplikované migrace se změnil (checksum nesedí) skript vypíše soubory, skončí kódem 1 a nespustí nic; nasazení skončí chybou
evidence na databázi neexistuje bez --baseline skončí kódem 1, viz První běh
na serveru není PHP migrace se nespustí a nasazení skončí chybou

Po každém běhu, který spustil alespoň jednu migraci, skript sám smaže query cache a routovací cache v Redisu (<prefix>cache:q:*, <prefix>cache:route:*, předvolba app:; host, port a prefix čte z konfigurace). Migrace mění data mimo uložení přes managery, takže se žádný tag nezneplatní a služby s delší platností cache (viz Platnost po službách) by držely starý obsah až hodinu. Relace a verze tagů se nemažou. Bez Redisu (nebo s vypnutým úložištěm) skript jen vypíše, že cache doexpiruje sama. Běh bez čekající migrace cache nemaže.

Selhání migrace nezabrání smazání Redis cache; jen celý krok skončí chybou, aby si nasazení někdo všiml. Po opravě stačí nasadit znovu (nebo pustit php bin/migrate.php).

Pravidla pro psaní migrace#

Pravidlo Proč
jméno YYYY-MM-DD_NN_popis.sql, rollback YYYY-MM-DD_<vyšší NN>_popis_rollback.sql rollbacky mají prefixy od 99 dolů a runner je přeskakuje
migrace je opakovatelná (IF NOT EXISTS, INSERT IGNORE, NOT EXISTS) selhaný soubor se pouští znovu od začátku; DDL se v MariaDB potvrzuje implicitně, transakce nic nehlídá
žádný příkaz DELIMITER soubor se posílá serveru vcelku (multi_query), DELIMITER je příkaz klienta a tam nefunguje
prefix YYYY-MM-DD_NN je jednoznačný pořadí se řídí celým názvem souboru; dva skripty se stejným prefixem se řadí podle zbytku názvu, což je náhoda
už nasazený soubor neupravuj runner porovná checksum a při neshodě zastaví celé nasazení. Oprava = nová migrace; změnu bez vlivu na databázi (komentář) přijmi vědomě php bin/migrate.php --accept-changed — přepíše jen checksum v evidenci, nic nepouští znovu
soubory s osobními údaji do sql/ nepatří rollbacky s původními daty (*_dev_pii_anonymization*) jsou mimo index gitu a v .gitignore; runner je navíc nikdy nepustí

tests/Unit/Sql/SqlMigrationPrefixTest.php shodí test při nové kolizi prefixu (historické kolize jsou v seznamu ALLOWLIST) a tests/Unit/Sql/SqlMigrationPlannerTest.php hlídá výběr, pořadí, základní stav a hlášení změněných i chybějících souborů.

Závislost mezi migracemi vyjadřuje jen číslo

Pořadí řídí jen název. Potřebuje-li migrace B proběhnout po migraci A, dej jí vyšší NN — nic jiného závislosti nevyjadřuje.

Migrace z 1.–2. 10. 2026#

Řazeno tak, jak je runner pustí (podle názvu). _01 až _07 spadají pod základní stav — na čerstvě založené evidenci se nespouštějí, jen zapisují.

Soubor Poznámka
2026-10-01_01_blog_article_news_region.sql region u článků a novinek
2026-10-02_01 … _05 externí připojení: tabulka, Api oprávnění, admin stránka, recaptcha_v3, čtyři Google typy
2026-10-02_06_remove_google_settings.sql smaže staré řádky nastavení; po kódu, ručně (viz výš)
2026-10-02_07_admin_discussion_message_reports.sql admin stránka nahlášených příspěvků
2026-10-02_08_response_mark_read_pages.sql stránky 2011 a 2012, routy — první, kterou runner pouští sám
…_09_response_mark_read_privilege.sql klíče markReadByKey
…_10_response_mark_read_email_button.sql tlačítko v e-mailových šablonách
…_11_response_read_key_column.sql sloupec read_key u reakcí
…_12_response_receiver_role.sql role receiver; nahrazuje grant z _09
…_13_rating_save_reply_receiver.sql Rating:saveReply z owner na receiver
…_14_message_thread_owner_receiver.sql role nad vlákny zpráv
…_15_message_read_receipts_setting.sql přepínač potvrzení o přečtení
…_16_message_thread_participants_admin.sql správa účastníků vlákna v administraci
…_17_demo_admin_password_repair.sql vrací heslo správce veřejnému demu; běží jen na jeho databázi a jen když je heslo prázdné
…_18_admin_roles_page_href_lowercase.sql adresy rolí, oprávnění a skupin malými písmeny

Rollback k migraci najdeš podle popisu v názvu: …_<vyšší číslo>_popis_rollback.sql (pro tuhle dávku 2026-10-01_91 a 2026-10-02_82 až _99).

Po ruční aplikaci smaž temp/cache a vyprázdni Redis

Nasazení maže cache samo. Pustíš-li migraci ručně (rollback, oprava), RBAC, router, mapa hezkých URL a metadata Doctrine se cachují dál. Skripty, které přidávají stránky nebo routy, neinvalidují route-table cache (klíč app:cache:route:table) — bez smazání se nová routa nenačte. Mazání klíčů viz výš.

Provozní požadavky serveru#

Co Požadavek Proč
PHP-FPM memory_limit aspoň 256M; pro plný strop obrázků 40 Mpx kolem 320M kompilace DI kontejneru po nasazení má ve webovém požadavku špičku ~128 MB — se 128M padaly první požadavky na 500. Obrázek se ověřuje odhadem paměti proti volné části limitu; se 128M projdou obrázky jen do ~12–15 Mpx
CLI memory_limit vyšší než FPM, nebo -1 kompilace v CLI (bin/migrate.php, jiný kontejner) potřebuje ~200 MB; pád bin/*.php na 128M není chyba aplikace
ffmpeg / ffprobe nainstalované video se před uložením ověřuje obsahem; bez ffprobe se video odmítne (zápis do log/assets.log), obrázky fungují dál
Redis Restart=always v systemd a hlídání výpadek už web neshodí — viz níž — ale query cache a ES jedou bez něj pomaleji
logrotate druhá pojistka vedle cronu log/purge-old viz Rotace logů

Relace přežijí výpadek Redisu

Když Redis při čtení relace selže, TieredTtlSessionHandler přepne relace toho požadavku do souborů v temp/sessions (volba sessionTtl.fallbackDir) a další požadavek zkusí Redis znovu. Do warning.log jde jeden řádek za hodinu, po návratu Redisu řádek do info.log. Přihlášení z doby výpadku po návratu propadnou. Podrobně v Cache.

Webroot se neodvozuje

App\Bootstrap nastavuje %wwwDir% napevno na <kořen>/www. Dřív ho nette/bootstrap dopočítával z nejvnějšího rámce zásobníku, takže skript spuštěný odjinud (harness, runner) skládal cesty o úroveň vedle a zapisoval do www/www/. Existující soubor pod www/www/ vrací www/.htaccess jako 403.

Více instalací na jednom serveru#

Na jednom serveru může běžet víc instalací CoolCMS, třeba ostrý web a jeho veřejné demo. Databázi, soubory, temp/ a logy má každá instalace vlastní. Redis, Elasticsearch a cookies v prohlížeči ale sdílejí, dokud se v local.neon neoddělí. Klíče v Redisu totiž nenesou žádné označení instalace: jwt:denylist:…, ratelimit:…, app:cache:…, Contributte.Storage:… i relace pod holým session ID jsou u všech instalací stejné.

Před nasazením další instalace na tentýž server zkontroluj local.neon

Se stejnou konfigurací si instalace přepisují cache a navzájem odhlašují uživatele. Zneplatňují si tokeny API, sčítají si limity požadavků a zapisují do stejných vyhledávacích indexů: dokument s id 5 jedné instalace přepíše dokument s id 5 druhé. Nic z toho se nehlásí, projeví se to až podivným chováním webu.

Nastavení v parameters Výchozí Co odděluje Kdy změnit
redis.database 0 celý obsah Redisu: cache, relace, JWT deny-list, limity požadavků, stav Elasticsearch každá instalace se zapnutým Redisem má jiné číslo (Redis jich má ve výchozím stavu 16, 0–15)
elasticsearch.indexPrefix cms indexy a aliasy <prefix>_<zdroj>, klíče es:<prefix>:… v Redisu každá instalace se zapnutým Elasticsearch; jen malá písmena a číslice
sessionName coolsoftcms jméno cookie relace instalace na stejné doméně druhého řádu (firma.cz a demo.firma.cz), i bez Redisu
sessionCookieDomain domain domain posílá cookie relace celé doméně druhého řádu včetně subdomén, null jen aktuálnímu hostu u druhé a další instalace na téže doméně stačí null

Druhá instalace na stejném serveru a stejné doméně má v config/Shared/local.neon například:

parameters:
    redis:
        database: 1
    elasticsearch:
        indexPrefix: 'firma'
    sessionName: 'firmaweb'
    sessionCookieDomain: null

První instalace může zůstat na výchozích hodnotách. Změna redis.database nebo indexPrefix u instalace, která už běží, znamená prázdnou cache, odhlášení všech uživatelů a nový reindex všech zdrojů Elasticsearch.

Volbu prefix z Predisu nepoužívej

Predis umí ke všem klíčům přidat prefix (redis.connection.default.options.prefix), ale nepřidá ho do vzoru u SCAN. Mazání vlastní cache by pak nic nenašlo a mazání podle KEYS by prefix přidalo dvakrát. Instalace se oddělují jen číslem databáze.

Instalace bez Redisu a Elasticsearch#

Menší web Redis ani Elasticsearch nepotřebuje. S tímto nastavením se k nim instalace vůbec nepřipojí, takže sdílený Redis a cluster sousedních instalací nijak neovlivní:

parameters:
    redis:
        storage: false        # Nette cache do souborů v temp/
        sessions: false       # relace jako běžné PHP sessions
    jwt:
        useRedis: false       # deny-list tokenů v databázi; limity požadavků se přepnou s ním
    elasticsearch:
        enabled: false
    cache:
        config:
            enabled: false
            routerEnabled: false

cache.config.enabled a routerEnabled jsou ve výchozím stavu vypnuté, uvedené jsou pro jistotu. Když jsou obě vypnuté, nezapisují se do Redisu ani verze tagů a statistiky cache. sessionName je potřeba i tady, protože jde o cookie v prohlížeči, ne o Redis.

Relace v souborech: každý web vlastní složku

Bez Redisu ukládá PHP relace do session.save_path. Sdílejí-li dva weby jednu složku, úklid starých relací webu s kratší životností smaže relace i druhému webu. Ověř, že má každý PHP-FPM pool vlastní php_admin_value[session.save_path]:

grep -H "session.save_path" /etc/php/*/fpm/pool.d/*.conf

Nasazení a ruční mazání cache#

Kde Jak zná číslo databáze
deploy/deploy-demo.sh proměnná DEMO_REDIS_DB v deploy/deploy-demo.local.env (výchozí 0); maže redis-cli -n <číslo>
bin/migrate.php čte redis.database sám z konfigurace
ruční mazání vždy s -n <číslo>: bez něj redis-cli pracuje s databází 0, tedy s jinou instalací
redis-cli -n 1 --scan --pattern 'app:cache:q:*' | xargs -r -d '\n' -n 200 redis-cli -n 1 del

Naplánované úlohy si každá instalace zakládá zvlášť, se svým tokenem. Týká se to i synchronizace a nočního reindexu Elasticsearch.

Sdílená kapacita serveru#

Co Proč to hlídat
OPcache instalace na stejné verzi PHP-FPM sdílejí jednu OPcache. Jedna instalace má i se závislostmi kolem 15–20 tisíc souborů PHP, k tomu zkompilované šablony a kontejner. Pro dvě instalace zvyš opcache.max_accelerated_files aspoň na 65407 a opcache.memory_consumption úměrně, jinak se OPcache neustále přeplňuje a resetuje
paměť Redisu maxmemory platí pro celý Redis. Nastav politiku noeviction: allkeys-lru by při zaplnění vyhazovala i relace a deny-list tokenů
heap Elasticsearch indexy všech instalací jsou v jednom clusteru a jedné paměti
PHP-FPM každý web má vlastní pool a vlastní pm.max_children; součet pracovních procesů se musí vejít do paměti serveru

Kontrola oddělení#

redis-cli INFO keyspace                           # každá instalace má vlastní řádek db<číslo>
curl -s -u elastic 'http://127.0.0.1:9200/_cat/aliases?v'   # aliasy začínají prefixem instalace

V prohlížeči se přihlas do obou instalací a přejdi mezi nimi. Ani jedna nesmí uživatele odhlásit a každá musí mít cookie relace s vlastním jménem.

PHP 8.5#

Kód je ověřen na PHP 8.4 a 8.5: php8.5 -l nad celým app/ (5 821 souborů) bez chyb, composer why-not php 8.5.0 bez překážky a po opravě níž prošel proklik frontu i administrace. Unit testy pod 8.5 zatím nebyly spuštěny (lokál běží na 8.4).

Redeklarace injektované vlastnosti v traitu shodí všechny stránky presenteru

AddAdvertFormTrait (Bazaar, Comcat) deklaroval public CurrencyManager $currencyManager; bez #[Inject], zatímco BasePresenter tutéž vlastnost dědí s #[Inject]. PHP 8.5 při reflexi vrací deklaraci potomka (bez atributu), takže ji Nette nenaplnilo a každá stránka AdvertPresenter padla na must not be accessed before initialization. Na 8.4 to fungovalo. Oprava: deklaraci z traitu odstranit. Sken celého app/ jiný případ nenašel.

Pravidlo: trait nebo potomek nesmí znovu deklarovat vlastnost, kterou předek injektuje. Potřebuješ-li ji, použij tu zděděnou.

Při podezření na „PHP 8.5“ nejdřív vylučuj zastaralý kontejner

Stejná hláška vzniká i při nasazení se starým DI kontejnerem (viz výš). Rozliší se tím, že po smazání temp/ zmizí; skutečná redeklarace zůstane.

Produkční nastavení#

Věc Nastavení
Ladicí režim vypnutý
Zobrazování chyb vypnuté, logovat
Mezipaměť zapnutá
docs/sql/ nedostupné zvenčí
Přístupy k branám jen v local.neon
www/uploads/ bez spouštění skriptů

Ladicí panel vypisuje konfiguraci včetně přístupových údajů

Zapnutý na produkci znamená, že klíče k platebním branám a dalším službám jsou ve zdroji stránky. Ověřte, že je vypnutý.

V www/uploads/ se nikdy nespouští kód

Nahrané soubory leží ve veřejném stromu a názvy si zachovávají příponu. Aplikace odmítá nebezpečné přípony (php, phtml, phar, cgi, sh, .htaccess …) v kterékoli části názvu, ale webový server je druhá pojistka.

Apache čte verzovaný soubor www/uploads/.htaccess (vypnutý PHP engine, odmítnuté skriptové přípony). Vyžaduje AllowOverride All (nebo alespoň FileInfo Options Limit) pro adresář webu.

nginx .htaccess nečte. Do bloku server doplňte před obecné location ~ \.php$:

location ~* ^/uploads/.*\.(php[0-9]?|phtml|phar|pht|phps|cgi|pl|py|sh)(\.|$) {
    deny all;
}

Po nasazení ověřte: nahraný test.php.txt v uploads/ se vrátí jako text, ne jako výstup skriptu.

Jak se rozhoduje o ladicím režimu#

Produkční výchozí stav je vypnuto; zapíná se explicitně. Rozhoduje App\Bootstrap::resolveDebugMode() v tomhle pořadí:

  1. proměnná prostředí NETTE_DEBUG — 1, true, on, yes zapnou, cokoli jiného včetně 0 a false vypne,
  2. existence souboru config/debug-mode.flag (v repu není; na dev serveru ho vytvoříte touchem),
  3. jinak vypnuto.

Proměnná prostředí přebíjí flag soubor

Když je NETTE_DEBUG nastavená na cokoli mimo povolenou čtveřici, flag soubor už se nečte. Na stroji, kde je proměnná nastavená omylem, proto touch nepomůže.

Sestavení assetů#

npm run build:admin:css
npm run build:admin:js

Používejte globální sass, ne npx sass

Jsou to jiné verze a produkují jiný výstup. Rozdíl se projeví jako drobné posuny v rozvržení, které nikdo nespojí s nasazením.

Nasazení na existující databázi#

Když se povyšuje starší instalace, nedědí se jen data, ale i vady schématu, které v ní léta nikdo neviděl. Projděte proto před nasazením Kontrolu databáze — jsou to samé SELECTy, dají se pustit i na produkci.

Nejdůležitější je první dotaz: vypíše překladové tabulky *_texts, kterým chybí složený primární klíč. Bez něj databáze pouští duplicitní překlady a každý další běh seedu je přidává.

Automatické kontroly tuhle vadu nenajdou

SchemaValidator::validateClass porovnává sloupce a typy, ne indexy — chybějící primární klíč mu projde. Zjistí se to jedině tímhle dotazem, nebo až tím, že se na webu ukazuje jiný text než v administraci.

Postup, když audit něco najde, i vzorové opravné skripty jsou v té kapitole.

Kontrola po nasazení#

Smoke test ze skriptu ověří jen to, že se stránky vykreslí. Po nasazení projdi ještě:

  1. Titulní stránka a přihlášení.
  2. Jeden výpis v administraci a jeden formulář.
  3. Zkušební objednávka až po odeslání.
  4. Fronta e-mailů — odchází?
  5. Log chyb — přibylo něco?
  6. Kontrola databáze — u nasazení na existující databázi znovu, ať se ověří, že migrace nezavedly duplicitní překlady.
  7. Běží-li na serveru další instalace, kontrola oddělení — vlastní databáze Redisu, prefix indexů a jméno cookie relace.

Zkušební objednávka je nejlepší test

Projde přes košík, platbu, sklad, fakturaci a e-maily naráz. Jeden průchod otestuje víc než hodina klikání po obrazovkách.

Nastavení cookies