Sok Laravel-projektnél az első Docker-kísérlet egyetlen Dockerfile-lal indul, amit aztán környezeti változókkal próbálnak fejlesztői és éles módra kapcsolgatni. Ez működik egy darabig, aztán elkezd fájni. A fejlesztői konténerben kellene a Xdebug, a forráskód élő szinkronizálása és a részletes hibaüzenet, míg az éles konténerben pont ezek jelentenek biztonsági kockázatot vagy teljesítményvesztést.
Két cél, két irány
A fejlesztői konténer dolga a gyors visszajelzés, vagyis hogy mentés után azonnal lássam, mi történt, és hiba esetén lássam a hívási láncot is. Az éles konténer dolga a stabilitás és a kis felület, mert minél kevesebb van benne, annál kevesebb dolog tud elromlani vagy kihasználhatóvá válni.
Ez a két cél tényleg ellentétes irányba húz, és éppen ezért nem lehet ugyanazt az image-et két környezeti változóval a másikká alakítani. A környezeti változó a viselkedést állítja, nem azt, hogy mi van bent az image-ben.
Multi-stage build: két cél, egy fájl
Nálunk a két konténer két külön build célja ugyanannak a Dockerfile-nak, multi-stage build-del. A közös rétegben ül minden, ami mindkettőhöz kell, tehát a PHP-alap, a rendszerkönyvtárak és a Composer-függőségek.
A fejlesztői réteg tartalmazza a Xdebugot, és a forráskódot kötetként (volume) csatolja be, hogy egy mentés után azonnal látszódjon a változás, tehát nincs újra build és nincs várakozás. A .env a fejlesztő gépéről jön, a hibaüzenetek részletesek, és a fejlesztői segédeszközök (teszt, statikus elemzés, kódformázó) is bent vannak.
Az éles réteg ezzel szemben a forráskódot bemásolja az image-be, nem kötetként csatolja; kikapcsolja a debug módot; optimalizált autoloadert és előre lefordított konfigurációt használ; és csak azokat a PHP-kiterjesztéseket tartalmazza, amikre az alkalmazásnak ténylegesen szüksége van. A fejlesztői függőségek (--no-dev) ki sem kerülnek bele.
Ami mindkettőben ugyanaz
Van, aminek viszont muszáj egyeznie, különben az egész gyakorlat értelmét veszti, tehát a PHP főverziójának, a kiterjesztések listájának és az alkalmazás futtatási módjának. Ha a fejlesztői gépen PHP 8.4 fut, élesen pedig 8.3, akkor a konténer nem védett meg semmitől, csak áthelyezte a meglepetést.
Ugyanez igaz a sor- és ütemező-workerekre. Ha élesen külön szolgáltatásként futnak, a fejlesztői összeállításban is így fussanak, különben az első aszinkron hiba éles bemutatón derül ki.
A build parancs, és ami mögötte van
A gyakorlatban ez a különbség egy build parancs argumentumában (--target=dev vagy --target=production) dől el, de a mögötte álló döntés nem technikai apróság, mert egy éles image, amiben ott marad a Xdebug vagy a forráskód élő szinkronizálása, lassabb és sebezhetőbb, mint kellene.
Egy fejlesztői image pedig, ami az éles optimalizálásokat használja, minden mentés után egy több másodperces build-et kényszerít ki. Ez naponta több tucatszor összeadódik, és pontosan az a fajta súrlódás, ami miatt a fejlesztők előbb-utóbb visszaállnak a konténer nélküli munkára. A legdrágább hiba tehát nem a lassú build, hanem az elhagyott gyakorlat.
Miért egyetlen fájlban
A multi-stage Dockerfile nem elrejti ezt az ellentétet, hanem explicitté teszi, mert egyetlen fájlban látszik, mi az, ami csak fejlesztéskor kell, és mi az, aminek élesen egyáltalán nem szabadna ott lennie. Két külön fájl ugyanezt megoldaná, egy darabig. Aztán az egyikbe bekerül egy kiterjesztés, a másikba nem, és a különbség csak élesben derül ki.