
Durante mucho tiempo, servir PHP en producción significó dos piezas: un servidor web delante (Apache o nginx) y PHP-FPM detrás, hablando a través de FastCGI, cada uno con su configuración, su servicio y sus registros. El FrankenPHP lo reúne todo en un único binario: el servidor web Caddy con el intérprete PHP integrado. Se obtiene HTTPS automático, HTTP/3 y, de paso, un worker mode que mantiene la aplicación cargada en memoria entre peticiones. Esta guía explica qué es, lo instala en Debian y Ubuntu mediante los paquetes oficiales y muestra ambos modos funcionando.
Qué es FrankenPHP
FrankenPHP es un servidor de aplicaciones PHP escrito en Go, creado por Kévin Dunglas — el autor de API Platform — con el patrocinio de la cooperativa Les-Tilleuls.coop. La versión 1.0 salió en diciembre de 2023. En mayo de 2025, la PHP Foundation pasó a apoyar oficialmente el proyecto y el código pasó a la organización oficial de PHP en GitHub (php/frankenphp), con la gobernanza mantenida por los mantenedores originales. La licencia es MIT.
La versión actual es la 1.13.0, de 4 de octubre de 2026, que incorpora Caddy 2.11.7 y Mercure 1.0 y corrige cinco fallos de seguridad (dos clasificados como altos). El nombre es literal: es un PHP cosido dentro de Caddy, de ahí el elefante con tornillos en el logo.
Qué aporta:
- Un único proceso. El PHP se ejecuta en hilos dentro del proceso de Caddy, sin FastCGI y sin un pool separado que administrar. Por eso FrankenPHP utiliza PHP compilado en modo ZTS (thread safe).
- Todo lo que hace Caddy. Certificado automático, HTTP/2, HTTP/3, compresión zstd/brotli/gzip, proxy inverso y el mismo Caddyfile.
- Worker mode. La aplicación se inicializa una vez y atiende las siguientes peticiones ya cargada. Laravel (vía Octane) y Symfony (vía Runtime) tienen integración oficial.
- Extras de aplicación web. Early Hints (estado 103), tiempo real con Mercure, hot reload y X-Sendfile para archivos grandes.

Instalación en Debian y Ubuntu
Hay tres caminos: paquetes .deb/.rpm, binario estático y Docker. Para servidor, el paquete es el más práctico, porque trae servicio systemd, php.ini de producción y extensiones instalables vía apt. Los paquetes los mantiene el equipo del proyecto en un repositorio propio, con una variante por versión de PHP (8.2 a 8.5):
sudo apt install -y curl ca-certificates
sudo mkdir -p /etc/apt/keyrings
VERSION=85 # 82, 83, 84 ou 85
sudo curl -fsS https://pkg.henderkes.com/api/packages/${VERSION}/debian/repository.key \
-o /etc/apt/keyrings/static-php${VERSION}.asc
echo "deb [signed-by=/etc/apt/keyrings/static-php${VERSION}.asc] https://pkg.henderkes.com/api/packages/${VERSION}/debian php-zts main" \
| sudo tee /etc/apt/sources.list.d/static-php${VERSION}.list
sudo apt update
sudo apt install frankenphp
Probé estos comandos en un Debian 13 limpio. El resultado:
$ frankenphp version
FrankenPHP v1.13.0 PHP 8.5 Caddy v2.11.7
$ php-zts -v
PHP 8.5.11 (cli) (ZTS gcc 16.2.0 x86_64)
El paquete crea el usuario frankenphp (miembro del grupo www-data), instala el servicio frankenphp.service y el CLI php-zts. Use este CLI para Composer y scripts: en 1.13, el subcomando frankenphp php-cli pasa a usar la SAPI de línea de comandos nativa de PHP y, con PHP por debajo de 8.6, rechaza ejecutarse con el mensaje esta funcionalidad no está disponible.
Los archivos se encuentran en:
| Ítem | Ruta (paquete deb/rpm) |
|---|---|
| Configuración del servidor | /etc/frankenphp/Caddyfile |
| Configuraciones extra (cargadas automáticamente) | /etc/frankenphp/Caddyfile.d/*.caddyfile |
| php.ini (ya con preajuste de producción) | /etc/php-zts/php.ini |
| adicionales | /etc/php-zts/conf.d/*.ini |
| Sitio de ejemplo | /usr/share/frankenphp/ |
Las extensiones entran a través de apt con el prefijo php-zts-. La instalación base ya trae OPcache, mbstring, curl, sodium, dom y otras; el resto se instala así:
apt-cache search php-zts- | grep -v debuginfo
sudo apt install php-zts-pdo-mysql php-zts-gd php-zts-intl php-zts-redis
php-zts -m
¿Prefieres no añadir un repositorio? El script oficial descarga el binario estático, que se ejecuta en cualquier distribución sin dependencias: curl https://frankenphp.dev/install.sh | sh. El cambio es que las extensiones tienen que ser compiladas dentro del binario.
Modo clásico: sustituyendo nginx + PHP-FPM
En el modo clásico, FrankenPHP se comporta como el dúo tradicional: cada solicitud carga el script desde cero. Es el modo para cualquier aplicación PHP existente — WordPress, Nextcloud, un sistema legado. El Caddyfile de un sitio en producción:
{
frankenphp
}
app.exemplo.com.br {
root /var/www/app/public
encode zstd br gzip
php_server
log
}
La directiva php_server resume lo que en nginx requeriría try_files, location ~ \.php$ e fastcgi_pass: sirve archivos estáticos, envía .php al intérprete y redirige el resto al index.php. Como en Caddy, el dominio en la dirección del bloque basta para que el certificado sea emitido y renovado automáticamente, con el DNS apuntando y los puertos 80 y 443 abiertos.
sudo frankenphp fmt --overwrite /etc/frankenphp/Caddyfile
sudo frankenphp validate --config /etc/frankenphp/Caddyfile
sudo systemctl enable --now frankenphp
sudo systemctl reload frankenphp
journalctl -u frankenphp -f
Para probar sin tocar el servicio, ejecuta en la carpeta del proyecto frankenphp php-server: sirve el directorio actual al instante. Para WordPress, la documentación oficial recomienda un bloque equivalente: dominio, php_server, compresión y registro.
Worker mode: la aplicación queda en memoria
En un framework moderno, gran parte del tiempo de cada petición se va en la inicialización: autoload, lectura de configuración, montaje del contenedor de dependencias. En el modo worker, el script de entrada hace esto una vez y entra en un bucle que atiende petición tras petición con frankenphp_handle_request(). Las superglobales ($_GET, $_POST, $_SERVER) se renuevan en cada vuelta; lo demás sigue cargado.
Un worker mínimo, para ver el efecto:
<?php
// /srv/app/worker.php
$boot = microtime(true); // executa uma única vez
$count = 0;
$handler = function () use (&$count, $boot) {
$count++;
echo "requisição $count, app iniciada em " . round($boot) . "\n";
};
while (frankenphp_handle_request($handler)) {
gc_collect_cycles();
}
{
frankenphp {
worker {
file /srv/app/worker.php
num 4
}
}
}
app.exemplo.com.br {
root /srv/app
php_server
}
En la prueba, tres peticiones consecutivas respondieron requisição 1, 2 e 3, todas con la misma hora de inicialización: el estado sobrevivió entre peticiones. En el modo clásico, el mismo contador volvería siempre a 1.
Esto es poderoso y cobra disciplina. Las variables estáticas, singletons y conexiones persisten entre usuarios; las fugas de memoria se acumulan; una excepción no tratada derriba al worker. Por eso la documentación recomienda capturar excepciones dentro del handler, y FrankenPHP reinicia los workers que fallan (el límite es configurable con max_consecutive_failures). Para cambios de código en desarrollo, la opción watch reinicia el worker cuando cambian los archivos.
Laravel y Symfony
No necesitas escribir el bucle a mano. En Laravel, Octane se encarga de esto:
composer require laravel/octane
php-zts artisan octane:install --server=frankenphp
php-zts artisan octane:frankenphp
En Symfony, el componente Runtime tiene soporte nativo; la configuración está en la página de Symfony de la documentación. En ambos, ejecuta las pruebas de la aplicación en worker mode antes de pasar a producción: el código que asume “proceso nuevo en cada petición” aparece rápido.
Docker
La imagen oficial es dunglas/frankenphp, con variantes por versión de PHP y base Debian o Alpine. Para levantar el directorio actual:
docker run -v $PWD:/app/public \
-p 80:80 -p 443:443 -p 443:443/udp \
dunglas/frankenphp:1.13-php8.5
Abre https://localhost (no https://127.0.0.1, para el cual no hay certificado por defecto) y acepta el certificado local. Para una imagen de producción, las extensiones entran con el install-php-extensions que ya viene en la base, y el worker con la variable FRANKENPHP_CONFIG:
FROM dunglas/frankenphp:1.13-php8.5
RUN install-php-extensions pdo_mysql gd intl zip opcache
RUN cp $PHP_INI_DIR/php.ini-production $PHP_INI_DIR/php.ini
COPY . /app
ENV FRANKENPHP_CONFIG="worker ./public/index.php"
Prefiere las imágenes basadas en Debian. Las Alpine y el binario estático usan musl libc, y la documentación lista incompatibilidades, como la falta del flag GLOB_BRACE del glob(). Para lo básico de contenedores, consulta la curso de Docker.
Precauciones antes de migrar
- Extensiones no thread safe.
imap, el agente de New Relic y elpcovno funcionan; elimagickchoca con los hilos de ImageMagick, y la documentación indica desactivar esos hilos; los perfiladores de Datadog y de Blackfire aún presentan limitaciones. La lista completa se encuentra en Known issues. - Hilos, no procesos. El valor predeterminado es el doble del número de CPUs. En la 1.13,
num_threadsse empezó a contar solo los hilos para las peticiones fuera de los workers, que entran de forma adicional; quienes fijaban ese valor junto con los workers deben revisar la configuración antes de actualizar. - El modo worker es opcional. Comience en el modo clásico, que no cambia el comportamiento de la aplicación, y solo active el worker después de probarlo.
- Manténgase actualizado. La 1.13 corrigió, entre otras cosas, una falsificación de cabecera por nombres con punto y un
putenv()que filtraba valores entre peticiones. Actualizar essudo apt upgrade.
Si mantiene varias versiones de PHP lado a lado con nginx, la entrada Nginx y varias versiones de PHP muestra el enfoque tradicional — útil para comparar y para las aplicaciones que aún dependen de una extensión incompatible.
Enlaces
- frankenphp.dev · documentación · github.com/php/frankenphp
- Modo worker · Configuración · Laravel · Producción
- Aquí en el blog: Caddy en Linux · La historia del lenguaje Go
FrankenPHP no le pide que reescriba nada para empezar: en el modo clásico, simplemente sustituye dos servicios por uno y trae consigo el HTTPS automático de Caddy. La gran ganancia llega después, con el modo worker — y entonces conviene tratar la migración como un cambio de arquitectura, con pruebas, y no como un simple cambio de servidor.