Compiling FrankenPHP from source on Debian 12

Why FrankenPHP

FrankenPHP is a PHP application server built on top of Caddy. The thing that sets it apart from a traditional PHP deployment is that it embeds the PHP interpreter directly inside the Caddy process: at its core, FrankenPHP runs PHP in-process through cgo.

The payoff is worker mode. A normal PHP-FPM setup walks through startup, load, and teardown on every request; worker mode bootstraps the application once and then keeps it resident in memory to handle requests over and over, cutting out the per-request cold start. That matters most for frameworks like Symfony and Laravel, where the bootstrap itself isn’t cheap. On top of that you get Early Hints, automatic HTTPS, HTTP/2, and HTTP/3.

On the speed question: from what we see, it’s faster than FastCGI / PHP-FPM. It can also be used as a standalone Go library to embed PHP into any program built on net/http.

We compile it ourselves rather than use a prebuilt binary because we want to pick the PHP extensions. As you’ll see below, we deliberately build in SNMP so that network monitoring tools like Cacti, Zabbix, and LibreNMS can run on this runtime too.

Before you build

0. Install dependencies

apt-get install libbrotli-dev \
                libsnmp-dev \
                libxml2-dev \
                libsqlite-dev \
                libsnmp-dev \
                libxml2-dev \
                libsqlite3-dev \
                libzstd-dev \
                liblz4-dev \
                libjpeg-dev \
                libpng-dev \
                libpcre3-dev \
                libmcrypt-dev \
                libgd-dev \
                libbz2-dev \
                libonig-dev \
                libpq-dev \
                libreadline-dev \
                libsodium-dev

1. Compile PHP

./configure --prefix=/usr --libdir=/usr/local/lib --enable-embed=static --enable-static  --enable-zts --disable-shared --disable-cgi --disable-fpm --disable-phpdbg  --with-config-file-path=/etc/frankenphp --with-config-file-scan-dir=/etc/frankenphp/php.d --enable-zend-max-execution-timers --with-snmp --enable-bcmath --enable-mbstring --enable-ctype --enable-dom --enable-session --enable-simplexml --enable-sockets --enable-tokenizer --enable-xml --enable-xmlreader --enable-xmlwriter --enable-soap --enable-opcache --enable-pdo --with-pdo-mysql --with-pdo-pgsql --with-pgsql --with-zip --with-curl --with-zlib --with-readline --with-iconv --with-libxml --with-openssl --with-bz2 --enable-gd --with-jpeg --with-webp --with-freetype --with-sodium --enable-sysvsem --enable-sysvshm --enable-sysvmsg --enable-pcntl --enable-shmop --enable-phar
make -j $(nproc)
make install

A handful of flags in that configure line are prerequisites for FrankenPHP, not options:

  • --enable-embed=static --enable-static --disable-shared: FrankenPHP embeds PHP as a static library inside Caddy, so PHP has to be built as a static embed rather than an ordinary shared library.
  • --enable-zts: worker mode is multi-threaded, so PHP must be built thread-safe (ZTS). Without it, none of this works.
  • --enable-zend-max-execution-timers: under ZTS this is how max_execution_time is enforced; it’s the recommended pairing with ZTS.
  • --disable-cgi --disable-fpm --disable-phpdbg: we don’t need the CGI, FPM, or phpdbg SAPIs. The embed is the only runtime, so turning the rest off keeps things smaller and simpler.

The remaining --with-* / --enable-* flags are extensions chosen for the workload. We specifically build in --with-snmp because we want Cacti, Zabbix, and LibreNMS to be able to run on this runtime. Add or drop extensions to suit your own needs.

2. Set the environment

After PHP is installed, and before building FrankenPHP with xcaddy, point the compiler and linker at the PHP you just built:

export PKG_CONFIG_PATH=/usr/lib/pkgconfig:/usr/local/lib/pkgconfig
export LIBRARY_PATH=/usr/local/lib
export C_INCLUDE_PATH=/usr/include/php:/usr/include/php/main:/usr/include/php/TSRM:/usr/include/php/Zend:/usr/include/php/ext
export LD_LIBRARY_PATH=/usr/local/lib
export PHP_CONFIG=/usr/bin/php-config
export CGO_ENABLED=1
export XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx,nowatcher"
export CGO_CFLAGS=$(php-config --includes)
export CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)"

A few notes:

  • CGO_ENABLED=1 is the master switch. FrankenPHP calls into PHP through cgo; turn it off and there’s nothing to link.
  • CGO_CFLAGS / CGO_LDFLAGS come straight from php-config, so the headers and link flags the Go side uses match exactly the PHP you built in the previous step. A version or path mismatch surfaces as a link error.
  • C_INCLUDE_PATH lists PHP’s core header directories (main / TSRM / Zend / ext) explicitly so cgo can find them at compile time.
  • In XCADDY_GO_BUILD_FLAGS, -w -s strips debug symbols to shrink the binary, and the nobadger,nomysql,nopgx,nowatcher build tags drop Caddy components we don’t use.

Takeaway

The whole build hinges on how PHP is compiled: a static embed (--enable-embed=static) and thread-safe (--enable-zts), both mandatory. The environment-variable layer just aims xcaddy’s cgo build at that PHP, and taking CGO_CFLAGS / CGO_LDFLAGS from php-config is what keeps the headers and library versions from drifting apart. Trim the extensions to the workload, the way we build in SNMP to run monitoring tools.

Published At (Updated At) Tagged with