Migrace MyInvoice → MyÚčto: tři zádrhely, na které jsem narazil

MyInvoice, který používám na fakturaci, se přejmenoval na MyÚčto — nový projekt od stejného vývojáře (Radek Hulán), sdílející základ, ale s vestavěnou nadstavbou pro podvojné účetnictví. Fakturační funkce zůstávají navždy zdarma, nadstavba je placená. Rozhodl jsem se přejít, protože veškerý vývoj se teď soustředí tam.

Na papíře je to jednoduché: nasadit vedle staré instalace, spustit vestavěný migrační skript, přepnout konfiguraci nginx. V praxi jsem narazil na tři záludnosti, které stály za zdržením — a asi budou trápit i další, kdo migraci budou dělat.

Příprava — nic překvapivého

  • Zálohoval jsem DB dump (mariadb-dump --all-databases) a appdata volume (faktury, loga) přes docker run s bind mountem.
  • Nasadil jsem MyÚčto jako nový, samostatný Docker stack vedle staré instalace — ne upgrade na místě. Nový interní port, vlastní DB kontejner.
  • Propojil jsem appku MyÚčto se sítí staré MyInvoice instalace, aby mohla migrační skript dosáhnout na zdrojová data.

A tady začaly komplikace.

Past č. 1: Docker DNS alias kolize

Appka po nastartování hlásila Access denied for user 'myucto'@'IP' (using password: YES) — heslo bylo správné. A přesto access denied.

Vysvětlení: obě Compose sítě (stará MyInvoice i nová MyÚčto) mají stejný service alias db — to je defaultní jméno, které Compose dá kontejneru podle názvu service v docker-compose.yml. Jakmile jsem appku připojil do obou sítí najednou (kvůli přístupu ke zdrojovým datům), Docker embedded DNS matchnul db na tu špatnou síť. Appka se tak snažila přihlásit do MyInvoice databáze s heslem, které platilo pro MyÚčto — logicky nesedělo.

Řešení: nepoužívat generický service alias db, ale jméno kontejneru — to je globálně unikátní napříč celým Dockerem:

db.host => 'myucto-db-1'   // místo 'db'

Past č. 2: OPcache, který nevidí změny

I po opravě configu appka dál hlásila stejnou chybu. Config na disku byl správně, appka po include v PHP vracela správnou hodnotu — ale PDO connection string v error logu pořád ukazoval starou hodnotu. Jako by appka žila v jiné realitě než souborový systém.

Příčina: opcache.validate_timestamps => Off. PHP OPcache nikdy nezjistí, že se soubor od zkompilování změnil, a drží starou zkompilovanou verzi v paměti — klidně i přes docker compose restart, protože samotný proces uvnitř kontejneru zůstává živý a jeho paměť s ním.

Řešení: po každé úpravě konfigurace udělat docker compose up -d --force-recreate app, ne jen restart. Parametr force-recreate zabije starý kontejner a vytvoří nový proces od nuly.

Past č. 3: Migrace bez pepperu = neplatné přihlašovací údaje

Appka konečně naběhla, migrace dat proběhla (81 z 86 tabulek napoprvé, zbytek po odstranění --no-truncate, který kolidoval se seed daty). Přihlašuju se — "Neplatné přihlašovací údaje".

Bcrypt hash hesla je nerozlučně svázaný s pepperem (dodatečná sůl navíc k samotnému hashi), který appka používá při ověřování. Migrační skript přenese hashe hesel z DB tak, jak jsou — ale nový MyÚčto config měl nově vygenerovaný pepper, ne ten původní z MyInvoice. Stejná logika platí pro secret_encryption_key, který dešifruje TOTP secrets pro 2FA.

Řešení: pepper i secret_encryption_key musí být identické jako ve zdrojové instalaci — nekopíruje se jen databáze, ale i tyhle dva klíče z cfg.php.

Shrnutí — co bych řekl někomu, kdo to bude dělat po mně

  1. Zálohuj DB dump i storage volume, než se čehokoliv dotkneš.
  2. Nasaď novou instanci vedle, ne přes starou.
  3. Zkopíruj pepper a secret_encryption_key ze zdroje 1:1 — jinak žádné heslo/2FA po migraci nepůjde ověřit.
  4. Pokud appku propojuješ se sítí staré instalace kvůli zdrojovým datům, adresuj databáze podle jména kontejneru, ne podle service aliasu — jinak riskuješ, že appka mluví s cizí DB.
  5. Po každé úpravě configu dělej --force-recreate, ne jen restart — OPcache jinak žije ve své vlastní minulosti.
  6. Migrační skript spouštěj bez --no-truncate na čerstvě vytvořené prázdné DB, ať si sám vyčistí seed tabulky.
  7. Nezapomeň na storage/ — DB migrace nenese PDF, loga ani přílohy, to je samostatný krok.

Samotná migrace dat proběhla bez ztráty jediného řádku. Většinu času jsem strávil nad debugováním zmíněných komplikací, které nemají nic společného s kvalitou migračního skriptu samotného — jsou to obecné Docker/PHP nástrahy, na které narazí kdokoliv, kdo propojuje dva stacky a mezitím ladí konfiguraci za běhu.

01_dashboard-2.webp