Membongkar arsitektur internal FrankenPHP: Caddy (Go) sebagai HTTP server dengan PHP ter-embedded via libphp, mengapa PHP harus dikompilasi ZTS, cara kerja thread pool, dan dua konsep kunci — mode classic (per request) versus mode worker (persistent).

Setelah di episode 1 kita memahami mengapa FrankenPHP ada — menjawab masalah bootstrap ulang PHP-FPM dengan app server modern — pada episode ini kita membedah bagaimana ia bekerja di dalam. Memahami arsitektur ini bukan sekadar kepuasan akademis: ketika worker kalian crash, ketika memory membengkak, atau ketika kalian harus memilih num_threads yang tepat di episode 17, semuanya berakar pada konsep yang kita bahas sekarang.
FrankenPHP adalah dua dunia yang disatukan: Caddy — web server Go yang super kaya fitur — dan PHP yang dikompilasi sebagai library bersama. Keduanya hidup dalam satu proses binary frankenphp. Mari kita bedah lapis demi lapis.
Caddy adalah web server modern yang ditulis dalam Go. Ia menangani seluruh pekerjaan "pinggir jalan" HTTP: parsing request, routing, TLS, HTTP/2, HTTP/3/QUIC, kompresi, reverse proxy, sampai logging. Keunggulan paling terkenalnya: automatic HTTPS — Caddy meminta sertifikat Let's Encrypt sendiri, memperbarui, dan memperpanjangnya tanpa intervensi manusia.
Di dalam FrankenPHP, Caddy berperan sebagai lapisan HTTP penuh. Kalian mengonfigurasinya lewat Caddyfile (atau JSON, episode 11). FrankenPHP menambahkan directive baru di atas Caddy: php, php_server, dan global option frankenphp.
Ini adalah konsep paling penting di episode ini. PHP-FPM menjalankan model process-per-request: setiap worker adalah proses terpisah dengan memori sendiri, jadi dua request tidak pernah berbagi memori secara bersamaan. FrankenPHP sebaliknya — ia menjalankan banyak thread di dalam satu proses agar konteks switch dan penggunaan memori jauh lebih murah.
Agar aman berjalan multi-thread, PHP harus dikompilasi dengan ZTS (Zend Thread Safety). Tanpa ZTS, variabel global PHP (misalnya request context) bisa diakses dua thread sekaligus dan terjadi data race. Karena itu:
.deb/.rpm/.apk resmi dinamai php-zts.--enable-zts wajib (episode 21).Imbalannya: satu thread jauh lebih ringan daripada satu proses. FrankenPHP bisa membuka ratusan thread dengan footprint jauh lebih kecil dibanding ratusan proses PHP-FPM.
Secara default (mode classic), FrankenPHP berperilaku seperti PHP-FPM: setiap request dieksekusi di sebuah thread, thread selesai → dipakai lagi untuk request berikutnya. Tidak ada state aplikasi yang bertahan.
Jumlah thread diatur lewat global option frankenphp di Caddyfile:
{
frankenphp {
num_threads 16
max_threads auto
max_wait_time 10s
max_idle_time 5s
}
}
localhost {
root * public/
php_server
}| Option | Arti | Padanan PHP-FPM |
|---|---|---|
num_threads | Thread yang dibuat di awal | pm = static + pm.max_children |
max_threads | Thread tambahan saat beban naik (auto = estimasi dari memory_limit) | pm = dynamic |
max_wait_time | Batas waktu request mengantre menunggu thread bebas | pm.max_requests terkait queueing |
max_idle_time | Thread tambahan di-nonaktifkan setelah idle | — |
Secara default FrankenPHP membuat 2x jumlah core CPU. max_threads auto menebak batas atas dari memory_limit di php.ini — mirip cara PHP-FPM menghitung max_children. Kita bedah tuning-nya di episode 17.
Tip
Ingat aturan emas dari dokumentasi resmi: num_threads x memory_limit harus lebih kecil dari memori total server. Thread hidup berdampingan di satu proses, tetapi memory_limit tetap per-thread.
Konsep kunci kedua adalah dua mode eksekusi FrankenPHP:
| Aspek | Classic | Worker |
|---|---|---|
| Bootstrap aplikasi | Setiap request | Sekali (saat worker start) |
| State antar request | Tidak ada | Persisten (static, global, cache) |
| Performa | Setara PHP-FPM | Jauh di atas (aplikasi panas di memori) |
| Kompatibilitas | 100% semua aplikasi | Butuh aplikasi yang "worker-safe" |
| Config | php_server / php | Tambah subdirective worker |
Mode classic adalah pilihan aman — drop-in replacement PHP-FPM tanpa mengubah kode aplikasi sama sekali. Mode worker adalah superpower FrankenPHP, tetapi menuntut disiplin: kode tidak boleh bergantung pada global state yang di-reset otomatis antar request (kita dalami di episode 5 dan 13).
php_server dan phpFrankenPHP menambahkan dua directive HTTP pada Caddy:
php_server — gabungan php + static file server + penanganan try_files (index, path info). Ini yang paling sering dipakai; di balik layar ia menambah file_server, redirect trailing slash, dan rewrite ke index.php.php — directive tingkat rendah; meneruskan semua request ke PHP tanpa memeriksa keberadaan file statis dulu. Berguna saat ingin kontrol penuh (misal memecah rute /assets/* ke file server dan sisanya ke PHP — kita bahas di episode 8).Keduanya menerima subdirective root, split_path, env, request_body_timeout, dan worker. Contoh dasar:
# Mode classic, gampang
localhost {
root * public/
php_server
}
# Mode worker, performa maksimal
localhost {
root * public/
php_server {
worker index.php 4
}
}frankenphp_handle_request()Di dalam mode worker, aplikasi tidak lagi "berakhir" setelah response. Sebaliknya, skrip worker berisi loop yang memanggil fungsi frankenphp_handle_request() untuk setiap request yang masuk:
<?php
// public/worker.php
require __DIR__ . '/vendor/autoload.php';
$app = new \App\Kernel();
$app->boot(); // Bootstrap SEKALI, bukan per request
while (\frankenphp_handle_request(static function () use ($app) {
echo $app->handle($_GET, $_POST, $_COOKIE, $_FILES, $_SERVER);
})) {
gc_collect_cycles();
}Bootstrap (boot()) dijalankan sekali; handler yang menerima request dijalankan berkali-kali di dalam loop. Inilah sumber kecepatan worker mode — dan juga sumber kehati-hatian yang harus kalian bawa. Detail lengkap fungsi ini kita bedah di episode 5.
Pada episode 2 ini, kalian telah membedah arsitektur inti FrankenPHP.
Inti yang harus dibawa pulang:
num_threads/max_threads adalah padanan pm.max_children/pm = dynamic PHP-FPM.php_server adalah directive sehari-hari; php untuk kontrol penuh.frankenphp_handle_request() adalah jantung worker mode.Di episode 3 selanjutnya, kita masuk fase hands-on penuh: instalasi FrankenPHP di berbagai platform dan konfigurasi Caddyfile pertama — static binary, Homebrew, paket distro, image Docker, lalu menyusun Caddyfile dengan root, php_server, dan routing dasar. Pastikan kalian siap mengetik, karena praktik dimulai sekarang!