FrankenPHP: el nuevo runtime PHP escrito en Go con workers, HTTP/3 y Mercure integrado

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.3 como base y añade install-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.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP