FrankenPHP — PHP wbudowany w Caddy: worker mode, Early Hints i Docker
FrankenPHP wkłada PHP do środka serwera Caddy jako SAPI — bez FPM, bez socketu, bez drugiego procesu. Od maja 2025 projekt należy do organizacji PHP i jest wspierany przez PHP Foundation. Pokazujemy tryb worker, konfigurację wątków, integrację z Octane oraz listę rozszerzeń, które w tym modelu po prostu nie działają.
FrankenPHP to serwer aplikacyjny dla PHP zbudowany na Caddym. Zdanie brzmi niewinnie, więc warto od razu powiedzieć, na czym polega różnica wobec wszystkiego, co znasz z produkcji: tu nie ma osobnego procesu PHP. Nie ma PHP-FPM, nie ma socketu uniksowego między serwerem a interpreterem, nie ma fastcgi_pass w konfiguracji. PHP jest wkompilowany w serwer jako SAPI i działa w jego wątkach.
Konsekwencje są odczuwalne od pierwszego uruchomienia. Jedno binarium zamiast dwóch usług do nadzorowania. HTTPS z automatycznym certyfikatem, HTTP/2 i HTTP/3 bez konfigurowania czegokolwiek. Do tego rzeczy, których w modelu FPM nie da się zrobić w rozsądny sposób: kod 103 Early Hints i tryb worker, w którym aplikacja bootuje się raz i zostaje w pamięci.
Najpierw: projekt zmienił adres
Jeśli masz w notatkach albo w Dockerfile'u dunglas/frankenphp jako repozytorium — to już nie tam. Kod mieszka pod github.com/php/frankenphp, w oficjalnej organizacji PHP. Stary adres przekierowuje, więc nic się nie psuje, ale to nie jest kosmetyczna zmiana.
15 maja 2025 PHP Foundation ogłosiła, że obejmuje FrankenPHP oficjalnym wsparciem: kod przechodzi do organizacji projektu PHP, zespół fundacji uczestniczy w utrzymaniu, a część dokumentacji ma trafić na stronę php.net. Struktura zarządzania została zachowana — za wydania i przeglądy kodu odpowiada dalej ta sama trójka opiekunów: Kévin Dunglas (autor), Robert Landers i Alexander Stecher. Projekt jest sponsorowany przez Les-Tilleuls.coop.
Stan na dziś: 11 295 gwiazdek, 152 współtwórców, licencja MIT, napisane w Go, początek w marcu 2022. Ostatnie stabilne wydanie v1.12.7 z 7 sierpnia 2026, a commity wpadają codziennie — ostatni w chwili pisania tego tekstu miał kilkadziesiąt minut. Tempo wydań jest równe od trzech lat: 19 wydań w 2024, 17 w 2025, 10 w 2026 do 20 sierpnia.
Jedna rzecz, która przy tej przeprowadzce potrafi zmylić: obrazy na Docker Hubie nadal nazywają się dunglas/frankenphp. Repozytorium kodu jest w organizacji PHP, obrazy zostały pod starą nazwą. To nie pomyłka w dokumentacji, tylko dwie różne rzeczy pod dwoma różnymi nazwami.
Dwa tryby i to jest cała mapa
Tryb klasyczny
Bez żadnej dodatkowej konfiguracji FrankenPHP działa jak zwykły serwer PHP: dostaje żądanie, wykonuje plik, kończy, zapomina. To zamiennik PHP-FPM albo Apache'a z mod_php i nie wymaga zmiany ani jednej linii w aplikacji.
Różnica wobec FPM siedzi w modelu współbieżności: wątki, nie procesy. Pula wątków PHP ma stały rozmiar ustalany przy starcie, co odpowiada trybowi static w FPM-ie, ale można pozwolić jej rosnąć w czasie działania — wtedy zachowuje się jak dynamic. Liczba przyjętych i zakolejkowanych połączeń jest ograniczona wyłącznie zasobami systemu.
Z tego wynika pułapka, którą trzeba zamknąć od razu: zakolejkowane połączenia czekają na wolny wątek w nieskończoność. Nie ma domyślnego limitu. Do produkcji ustaw max_wait_time, żeby żądanie było odrzucane, zamiast wisieć.
Warto też wiedzieć, że każda instancja Caddy'ego tworzy tylko jedną pulę wątków PHP, dzieloną przez wszystkie bloki php_server. Trzy hosty wirtualne nie oznaczają trzech pul.
Tryb worker
Tu jest cała stawka. Aplikacja bootuje się raz, zostaje w pamięci i obsługuje kolejne żądania w pętli. Odpada autoloader, odpada budowanie kontenera zależności, odpada wczytywanie konfiguracji — przy każdym żądaniu, na zawsze.
Kontraktem jest funkcja frankenphp_handle_request(). Własny skrypt workera wygląda tak:
<?php
// public/index.php
require __DIR__.'/vendor/autoload.php';
$myApp = new \App\Kernel();
$myApp->boot();
// Handler poza pętlą — mniej pracy przy każdym żądaniu
$handler = static function () use ($myApp) {
try {
echo $myApp->handle($_GET, $_POST, $_COOKIE, $_FILES, $_SERVER);
} catch (\Throwable $exception) {
// set_exception_handler odpali się dopiero przy końcu skryptu
// workera, więc wyjątki łapiemy tutaj
(new \MyCustomExceptionHandler)->handleException($exception);
}
};
$maxRequests = (int) ($_SERVER['MAX_REQUESTS'] ?? 0);
for ($nbRequests = 0; !$maxRequests || $nbRequests < $maxRequests; ++$nbRequests) {
$keepRunning = \frankenphp_handle_request($handler);
$myApp->terminate();
// Wywołanie GC tutaj zmniejsza szansę, że odpali się w środku
// generowania strony
gc_collect_cycles();
if (!$keepRunning) {
break;
}
}
$myApp->shutdown();Zwróć uwagę na komentarz o wyjątkach — to nie jest drobiazg. set_exception_handler zostaje wywołany dopiero, gdy kończy się skrypt workera, czyli potencjalnie po tysiącach żądań. Wyjątki trzeba obsłużyć wewnątrz handlera albo zostaną bez obsługi w momencie, w którym mają znaczenie.
Uruchomienie w Dockerze i z binarium:
docker run \
-e FRANKENPHP_CONFIG="worker /app/public/index.php" \
-v $PWD:/app -p 80:80 -p 443:443 -p 443:443/udp \
dunglas/frankenphp
frankenphp php-server --worker /path/to/worker.phpDomyślnie startuje 2 workery na rdzeń CPU. Liczbę podaje się jako trzeci argument: worker ./public/index.php 42.
Jeśli aplikacja mówi PSR-15 zamiast supergloablami, dokumentacja pokazuje wariant z nyholm/psr7 i nyholm/psr7-server — konwersja żądania i odpowiedzi odbywa się na krawędziach handlera, pętla zostaje ta sama.
Laravel: Octane robi to za Ciebie
Skryptu workera do Laravela nie piszesz. Robi to Octane:
composer require laravel/octane
php artisan octane:install --server=frankenphp
php artisan octane:frankenphpDomyślne wartości komendy octane:frankenphp warto znać, bo część z nich jest zaskakująca na produkcji: --host to 127.0.0.1, --port to 8000, --admin-port to 2019, --workers to auto, a --max-requests to 500. Do tego --https (włącza HTTP/2, HTTP/3 i automatyczne certyfikaty), --http-redirect, --watch, --poll dla wolumenów sieciowych, --caddyfile na własną konfigurację i --log-level.
Ta ostatnia opcja ma efekt uboczny, o którym dokumentacja wspomina półgębkiem, a który jest istotny przy centralnym logowaniu: strukturalne logi JSON dostajesz tylko wtedy, gdy jawnie podasz --log-level. Bez tego logi lecą w formacie, którego żaden agregator nie sparsuje sensownie.
W naszym stacku Octane chodzi dziś na Swoole (config/octane.php, OCTANE_SERVER). Przejście na FrankenPHP to zmiana tej zmiennej i przejechanie octane:install — kod aplikacji zostaje ten sam, bo warstwę workera trzyma Octane, nie serwer. Różnica jest w warstwie niżej: Swoole to rozszerzenie PHP z własną pętlą zdarzeń, FrankenPHP to serwer HTTP z PHP w środku. Dostajesz przy tym Caddy'ego z całym jego ekosystemem, automatyczne HTTPS i HTTP/3 bez osobnego proxy przed aplikacją.
Jest też opcja, której Swoole nie ma w ogóle: Laravel spakowany w jedno wykonywalne binarium, razem z PHP i serwerem. Buduje się to obrazem dunglas/frankenphp:static-builder-gnu, a potem wyciąga plik z kontenera. Przy takim wdrożeniu trzeba pamiętać o jednej rzeczy: każda nowa wersja rozpakowuje się do innego katalogu tymczasowego, więc domyślny storage/ przestaje mieć sens. Ustaw LARAVEL_STORAGE_PATH albo wywołaj Application::useStoragePath(), inaczej stracisz uploady, logi i cache przy pierwszej aktualizacji.
Co tryb worker robi z Twoim kodem
To najważniejsza sekcja tego wpisu i jednocześnie ta, którą najłatwiej pominąć podczas migracji, bo aplikacja po przełączeniu zwykle działa. Problemy przychodzą później i wyglądają na losowe.
Proces PHP żyje między żądaniami, więc przeżywa też stan:
- zmienne statyczne w funkcjach i metodach zachowują wartości,
- statyczne właściwości klas zachowują wartości,
- zmienne globalne skryptu workera zachowują wartości,
- wszystko, co trzymasz w pamięci poza handlerem — tablice, obiekty, własne cache.
function getCounter(): int
{
static $count = 0;
return ++$count; // rośnie między żądaniami!
}Supergloable są resetowane: $_GET, $_POST, $_COOKIE, $_FILES, $_SERVER i $_REQUEST. Z jednym wyjątkiem, który dokumentacja wyróżnia pogrubieniem:
$_ENV nie jest resetowany między żądaniami. Cokolwiek tam zapiszesz, zostaje widoczne dla kolejnych żądań obsłużonych przez ten sam wątek workera. Nie trzymaj tam niczego związanego z konkretnym żądaniem ani niczego wrażliwego.Druga pułapka z supergloablami jest subtelniejsza: przed pierwszym wywołaniem frankenphp_handle_request() zawierają wartości workera, a w trakcie i po — wartości z obsługiwanego żądania. Żeby dostać się w handlerze do $_SERVER workera, trzeba go skopiować przed pętlą i wciągnąć kopię przez use.
Octane i Symfony resetują większość stanu za Ciebie. Nie resetują Twoich usług. W praktyce oznacza to jedną regułę, która w projekcie na Octane obowiązuje niezależnie od wybranego serwera: żadnego stanu związanego z żądaniem w singletonach i we właściwościach statycznych. Zamiast singleton() używaj scoped() dla usług per żądanie. Zalogowany użytkownik zapamiętany w statycznej właściwości to nie teoretyczny problem, tylko wyciek danych między klientami — i najbrzydszy rodzaj błędu, jaki można wypuścić na produkcję, bo nie odtwarza się na jednym żądaniu w testach.
Na wycieki pamięci w bibliotekach, których nie kontrolujesz, są dwa wentyle bezpieczeństwa. Restart po N żądaniach — MAX_REQUESTS we własnym skrypcie, --max-requests w Octane, albo globalne max_requests w Caddyfile'u (oznaczone jako eksperymentalne, obejmuje wątki zwykłe i workery). Oraz restart ręczny przez API administracyjne Caddy'ego:
curl -X POST http://localhost:2019/frankenphp/workers/restartGdy skrypt workera padnie z niezerowym kodem wyjścia, FrankenPHP restartuje go z wykładniczym opóźnieniem. Jeśli worker utrzyma się dłużej niż dwukrotność ostatniego opóźnienia, licznik się zeruje. Przy uporczywych awariach — na przykład literówce w skrypcie — serwer kończy się błędem too many consecutive failures. Limit ustawia max_consecutive_failures (domyślnie 6, wartość -1 oznacza restart bez końca).
Early Hints, czyli 103 z PHP-a
Kod 103 pozwala odesłać nagłówki Link z zasobami do wstępnego pobrania, zanim zaczniesz liczyć właściwą odpowiedź. Przeglądarka pobiera CSS w tle, kiedy Twoje zapytania SQL jeszcze się wykonują. Dokumentacja mówi o poprawie czasu wczytywania strony do 30%.
<?php
header('Link: </style.css>; rel=preload; as=style');
headers_send(103);
// tutaj wolne algorytmy i zapytania SQL
echo $renderowanaStrona;Funkcja headers_send() to dodatek FrankenPHP — w PHP-FPM nie ma odpowiednika, bo model „jedno żądanie, jedna odpowiedź, koniec” nie ma gdzie zmieścić odpowiedzi tymczasowej. Działa w obu trybach, klasycznym i worker.
W Laravelu naturalne miejsce na to jest w middleware, przed przekazaniem żądania dalej — pod warunkiem że wiesz, które zasoby są krytyczne dla danego widoku. Wysyłanie 103 z listą wszystkiego z manifestu Vite'a jest gorsze niż niewysyłanie go wcale.
Konfiguracja: Caddyfile
Dla Laravela w wersji minimalnej wystarczy tyle:
# Caddyfile
{
frankenphp
}
localhost {
root public/
encode zstd br gzip
php_server {
try_files {path} index.php
}
}Opcje, które realnie decydują o zachowaniu pod obciążeniem, siedzą w globalnym bloku frankenphp:
num_threads— liczba wątków PHP. Domyślnie 2× liczba rdzeni.max_threads— górny limit wątków dostawianych w czasie działania. Odpowiednikpm.max_childrenz FPM-a, z tą różnicą, że to wątki, a FrankenPHP sam rozdziela je między skrypty workerów i tryb klasyczny.max_wait_time— maksymalny czas oczekiwania żądania na wolny wątek. Domyślnie wyłączone.max_idle_time— po jakim czasie bezczynności wątek dostawiony automatycznie jest wygaszany. Domyślnie 5 s.max_requests— restart wątku po N żądaniach. Eksperymentalne, domyślnie 0.php_ini— dyrektywyphp.iniwprost w Caddyfile'u, pojedynczo albo blokiem.
Do doboru num_threads dokumentacja podaje jedną regułę i jest ona twarda: num_threads × memory_limit < dostępna pamięć. Autorzy wyraźnie zalecają zmianę wartości domyślnych i wyznaczenie ich testami obciążeniowymi (k6, Gatling), a nie zgadywaniem. Przy max_threads auto limit jest szacowany z memory_limit — z zastrzeżeniem, że szacunek może być mocno zaniżony.
Konfiguracja workera z dopasowaniem po ścieżce jest wygodniejsza, niż wygląda. Poniżej wariant, w którym worker dostaje wszystkie żądania, a plików statycznych serwer w ogóle nie szuka na dysku:
route {
@assets {
path /assets/*
}
file_server @assets {
root /app/public
}
rewrite index.php
php {
root /app/public
}
}Trzy rzeczy warte wyłapania z dokumentacji wydajności. php_server sam stawia serwer plików — wygodne, ale kosztuje; wyłącza się przez file_server off. Jawne try_files {path} index.php razem z jawnym root pozwala cache'ować i „znacząco redukuje liczbę zbędnych operacji na plikach”. I trzecie: nie używaj placeholderów Caddy'ego w dyrektywach root i env na gorących ścieżkach — uniemożliwiają cache'owanie tych wartości i mają istotny koszt.
Docker i budowa obrazu
Tagi mają schemat dunglas/frankenphp:<wersja-frankenphp>-php<wersja-php>-<os>, gdzie <os> to trixie, bookworm albo alpine. Obraz bazowy ma w środku docker-php-extension-installer, więc rozszerzenia dokłada się jedną warstwą:
FROM dunglas/frankenphp
ENV SERVER_NAME=twoja-domena.example.com
RUN install-php-extensions pdo_pgsql redis intl opcache
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
COPY . /appDla Laravela kopiujesz cały projekt do /app, nie do /app/public — public/ jest wtedy katalogiem głównym serwera, a nie korzeniem aplikacji. Jeśli katalog publiczny nazywa się inaczej, ustaw SERVER_ROOT. Chcesz wyłączyć HTTPS w kontenerze za terminacją TLS na proxy? SERVER_NAME=:80.
Uruchamianie jako użytkownik inny niż root jest opisane i działa bez nadawania dodatkowych uprawnień — trzeba tylko przekazać własność katalogów /config/caddy i /data/caddy. Jest też obraz builder z gotowym libphp, do składania własnego binarium z modułami Caddy'ego przez xcaddy.
Alpine na produkcji: nie
To rekomendacja wprost z dokumentacji i jedna z rzeczy, w których FrankenPHP odbiega od zwyczajów świata Dockera. Obrazy alpine'owe i domyślne binaria statyczne używają musl libc zamiast glibc. PHP na muslu jest wolniejsze, szczególnie skompilowane w trybie ZTS — a ZTS jest tu wymagany. W mocno zwątkowanym środowisku różnica bywa znacząca. Do tego część błędów PHP występuje wyłącznie na muslu, a flaga GLOB_BRACE jest niedostępna.
Na produkcję: obrazy oparte na Debianie, paczki .deb/.rpm/.apk od opiekunów projektu albo kompilacja ze źródeł. Jeśli chodzi o mniejszy i bezpieczniejszy obraz, dokumentacja odsyła do hartowanego obrazu Debiana, nie do Alpine.
Do środowiska uruchomieniowego Go warto dołożyć dwie zmienne: GODEBUG=cgocheck=0 (w oficjalnych obrazach jest już domyślnie) i GOMEMLIMIT ustawione na pamięć dostępną kontenerowi, jeśli ta jest ograniczona. Bez tego drugiego garbage collector Go nie wie, w jakich granicach się porusza.
Metryki
Przy włączonych metrykach Caddy'ego dostajesz zestaw dla Prometheusa, który pokrywa dokładnie te rzeczy, o które przy tym modelu trzeba pytać: frankenphp_total_threads, frankenphp_busy_threads, frankenphp_queue_depth, a per worker frankenphp_total_workers, frankenphp_busy_workers, frankenphp_worker_request_time, frankenphp_worker_request_count, frankenphp_ready_workers, frankenphp_worker_crashes, frankenphp_worker_restarts i frankenphp_worker_queue_depth.
Dwie z nich to dyżurny alert po migracji. frankenphp_queue_depth rosnące trwale oznacza, że num_threads jest za małe. frankenphp_worker_crashes rosnące powoli to zwykle wyciek pamięci w kodzie, który przed przejściem na worker mode nikomu nie przeszkadzał. Nazwę workera w etykiecie ustawia opcja name — bez niej dostaniesz ścieżkę bezwzględną do pliku, co w Grafanie wygląda źle.
Na co uważać
Model wątkowy i ZTS mają cenę, a ona jest policzalna: konkretne rozszerzenia PHP nie działają albo działają źle.
Niekompatybilne (wszystkie z powodu braku bezpieczeństwa wątkowego):
imap— alternatywy:javanile/php-imap2,webklex/php-imap, ImapEngine,newrelic— bez alternatywy. Jeśli APM stoi na New Relicu, to jest realny blocker,pcov— SIGSEGV w trybie klasycznym. Pokrycie kodu zbieraj SAPI CLI, czyli uruchamiaj PHPUnit poza FrankenPHP.
Działające, ale z błędami:
datadog— niestabilność przy profilowaniu, błąd jest po stronie Datadoga,blackfire— wsparcie w wersji beta, niekompletne,imagick— wątki OpenMP ImageMagicka gryzą się z wątkami FrankenPHP, co daje niestabilność i awarie. Obejście:\Imagick::setResourceLimit(\Imagick::RESOURCETYPE_THREAD, 1)albo ImageMagick zbudowany z--disable-openmp. Uwaga na zakres — OpenMP jest już wyłączony w binariach statycznych i w paczkachapt/apk/rpm, więc dotyczy to tylko obrazów Dockera i instalacji przez Homebrew.
Do tego lista mniejszych rzeczy, które zjadają czas:
- Skrypty Composera z
@phpnie działają. Dotyczy to wprost Laravela, gdzie@php artisan package:discover --ansijest wcomposer.json. Composer nie wie, jak wywołać binarium FrankenPHP, i dokłada flagi-d, których FrankenPHP nie obsługuje. Obejście z dokumentacji to skrypt w/usr/local/bin/php, który wycina-di przekazuje resztę dofrankenphp php-cli, plusPHP_BINARYwskazujące na ten skrypt. https://127.0.0.1w Dockerze nie zadziała bez dodatkowej pracy. Używajhttps://localhost. Na Linuksie ratuje--network host; na macOS i Windowsie zostaje zgadywanie adresu IP kontenera i wpisywanie go doSERVER_NAME, co jest dokładnie tak nietrwałe, jak brzmi.- Binaria statyczne nie zawierają certyfikatów CA. Objawia się przy wysyłce maili przez STARTTLS jako
certificate verify failed. Rozwiązanie:SSL_CERT_FILEiSSL_CERT_DIRalbo paczkaca-certificates. Konteksty webowy i CLI mogą mieć różne ustawienia — sprawdzaj przezopenssl_get_cert_locations()w tym kontekście, który Cię interesuje. get_browser()degraduje się z czasem. Obejście: cache'owanie wyników per user agent, na przykład w APCu.- Kolejkowanie bez limitu. Wracamy do
max_wait_time, bo to najczęstszy brak w konfiguracjach przenoszonych z FPM-a: tam limit wynikał zpm.max_childreni błędu 502, tu żądania po prostu czekają.
Kiedy to ma sens
Ma sens, gdy wąskim gardłem jest bootstrap aplikacji — a w Laravelu jest nim często, bo kontener zależności, dostawcy usług i konfiguracja składają się od nowa przy każdym żądaniu. Tryb worker to usuwa. Ma sens, gdy chcesz jedno binarium zamiast pary nginx plus FPM, HTTP/3 bez proxy przed aplikacją albo Early Hints. I ma sens, gdy potrzebujesz dystrybucji aplikacji jako pliku wykonywalnego, bo w świecie PHP nie ma na to drugiego narzędzia tej klasy.
Nie ma sensu, gdy aplikacja stoi na jednym z niekompatybilnych rozszerzeń i nie masz jak ich wymienić — newrelic jest tu najtwardszym przypadkiem. Nie ma sensu, gdy zespół nie jest gotowy przejrzeć kodu pod kątem stanu przeżywającego żądanie; wtedy tryb klasyczny daje zamiennik FPM-a bez tego ryzyka, a workera można włączyć później. I nie ma sensu jako cel sam w sobie, jeśli profil pokazuje, że czas idzie w zapytania do bazy — wtedy najpierw indeksy, a serwer aplikacyjny potem.
Podsumowanie
FrankenPHP jest dziś najciekawszą rzeczą, jaka zdarzyła się warstwie uruchomieniowej PHP od czasu OPcache, a przejście pod skrzydła organizacji PHP i PHP Foundation w maju 2025 zdejmuje z niego główne ryzyko, jakie miał wcześniej: zależność od jednej osoby. Zespół opiekunów został ten sam, więc zmiana jest przyrostem gwarancji, nie zmianą kierunku.
Co daje:
- PHP jako SAPI wewnątrz Caddy'ego — jedno binarium, bez FPM-a, bez socketu, bez drugiej usługi w nadzorze,
- tryb klasyczny jako zamiennik PHP-FPM i mod_php bez zmian w aplikacji, z pulą wątków o stałym albo automatycznie skalowanym rozmiarze,
- tryb worker z aplikacją trzymaną w pamięci, kontraktem
frankenphp_handle_request()i wsparciem PSR-15, - oficjalną integrację z Laravel Octane (
octane:install --server=frankenphp) i z Symfony, - automatyczne HTTPS z odnawianiem certyfikatów, HTTP/2 i HTTP/3 bez konfiguracji,
- Early Hints (kod 103) przez
headers_send(), w obu trybach, - hot reload i restart workerów na zmianę plików (
watch) oraz przez API administracyjne, - metryki Prometheusa dla wątków, workerów, głębokości kolejki, awarii i restartów,
- Mercure w standardzie, do funkcji czasu rzeczywistego bez osobnego serwera,
- obrazy Dockera dla Debiana i Alpine, paczki
deb/rpm/apk, Homebrew i skrypt instalacyjny, - binaria statyczne z PHP 8.5 w środku oraz pakowanie aplikacji w samodzielny plik wykonywalny,
- możliwość użycia jako biblioteki Go i pisania rozszerzeń PHP w Go.
Licencja: MIT, bez niespodzianek przy komercyjnym użyciu — pozwala na użycie, modyfikację i dystrybucję, także w oprogramowaniu zamkniętym, a jedynym obowiązkiem jest zachowanie noty o prawach autorskich i treści licencji przy redystrybucji. Uruchamianie FrankenPHP w firmie i wdrażanie na nim aplikacji klienckich nie pociąga za sobą żadnych zobowiązań. Caddy, na którym całość stoi, jest na Apache 2.0 — również permisywnej. Nie ma tu odpowiednika sytuacji z MinIO i AGPLv3, gdzie licencja sama bywa argumentem przeciw.
Ryzyko leży zatem nie w licencji, a w modelu wykonania. Tryb klasyczny możesz włączyć w piątek i nic się nie stanie. Tryb worker wymaga przeglądu kodu pod kątem stanu przeżywającego żądanie — statycznych właściwości, singletonów, $_ENV — i to jest praca, którą trzeba wykonać raz, świadomie, zanim ruch produkcyjny znajdzie te miejsca za Ciebie.