FrankenPHP es un servidor de aplicaciones PHP moderno escrito en Go que integra el servidor web Caddy. Su característica más diferenciadora son los workers persistentes: el proceso PHP arranca una vez y atiende miles de peticiones sin reiniciarse, eliminando el coste de bootstrap que sufre php-fpm.
¿Qué problema resuelve?
En el modelo tradicional php-fpm, cada petición inicia el proceso PHP desde cero: carga el autoloader de Composer, instancia el contenedor de inyección de dependencias y lee la configuración. Con Workers, todo eso ocurre una vez al arrancar; cada petición simplemente llama al handler y devuelve la respuesta.
Instalación con Docker
FROM dunglas/frankenphp WORKDIR /app COPY . . RUN composer install --no-dev --optimize-autoloader CMD ["frankenphp", "run", "--config", "/app/Caddyfile"]
O con la imagen que incluye extensiones adicionales:
docker run -v $PWD:/app -p 80:80 dunglas/frankenphp
Caddyfile básico
{
frankenphp
}
localhost {
root * /app/public
encode zstd br gzip
php_server
}
Worker PHP
<?php
// worker.php - Se ejecuta una vez al arrancar
require 'vendor/autoload.php';
$app = new MiAplicacion(); // Bootstrap: una sola vez
// El loop atiende peticiones indefinidamente
while ($peticion = frankenphp_handle_request()) {
// Aquí responde cada petición
$response = $app->handle($peticion);
$response->send();
}
?>
Configura el worker en el Caddyfile:
localhost {
root * /app/public
php_server {
worker /app/worker.php 4 # 4 workers paralelos
}
}
Compatibilidad con Laravel
composer require runtime/frankenphp-symfony # o para Laravel: composer require runtime/frankenphp-laravel
APP_RUNTIME=Runtime\FrankenPhpLaravel\Runtime php artisan serve
FrankenPHP entiende el ciclo de petición de Laravel y reinicia el estado de la aplicación entre peticiones de forma segura.
Compatibilidad con Symfony
composer require runtime/frankenphp-symfony
<?php
// public/index.php
use RuntimeFrankenPhpSymfonyRuntime;
$_SERVER['APP_RUNTIME'] = Runtime::class;
require_once dirname(__DIR__) . '/vendor/autoload_runtime.php';
return function (array $context) {
return new AppKernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
};
?>
HTTP/2, HTTP/3 y Early Hints
Caddy activa HTTP/2 por defecto con TLS y HTTP/3 (QUIC) opcionalmente:
ejemplo.com {
root * /app/public
tls /etc/certs/cert.pem /etc/certs/key.pem
# HTTP/3 (UDP)
http_port 80
https_port 443
php_server
header {
# Early Hints (HTTP 103)
Link "</styles.css>; rel=preload; as=style"
}
}
Mercure integrado
FrankenPHP incluye el hub de Mercure para Server-Sent Events sin servidor adicional:
{
frankenphp
order mercure before respond
}
localhost {
mercure {
anonymous
publisher_jwt secret
}
php_server
}
<?php
// Publicar un evento desde PHP
$mercureUrl = 'http://localhost/.well-known/mercure';
$token = 'tu-jwt-de-publicador';
$ch = curl_init($mercureUrl);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query([
'topic' => 'https://mi-app.com/usuarios/42',
'data' => json_encode(['mensaje' => 'Nuevo pedido']),
]),
CURLOPT_HTTPHEADER => ["Authorization: Bearer $token"],
CURLOPT_RETURNTRANSFER => true,
]);
curl_exec($ch);
?>
Rendimiento vs. php-fpm
Los benchmarks de la comunidad muestran mejoras de 3× a 10× en aplicaciones Symfony y Laravel en modo worker, principalmente por eliminar el tiempo de bootstrap. La mejora es mayor cuanto más pesado es el framework.
Errores comunes
- Estado global entre peticiones: los workers son persistentes, así que variables estáticas y singletons mantienen su estado entre peticiones. Reinicia explícitamente lo que deba resetarse.
- Extensiones PHP no disponibles: la imagen base de FrankenPHP incluye las extensiones más comunes; si necesitas alguna especial, usa la imagen
dunglas/frankenphp:php8.3como base y añadeinstall-php-extensions. - Puerto 80/443 ocupado: Caddy escucha en 80/443 por defecto. Si tienes nginx u otro servidor, cambia los puertos o usa FrankenPHP detrás de un reverse proxy.
