Tutorial

Laravel Octane en VPS: FrankenPHP, Docker y Traefik

Desarrollo11 min de lectura9 pasos

PHP-FPM lanza un proceso por petición: eficiente para apps pequeñas, pero costoso cuando el tráfico crece. Laravel Octane cambia el paradigma — la aplicación se carga una sola vez en memoria y responde a miles de peticiones sin reinicializarse. La ganancia es real: los benchmarks oficiales de Laravel muestran multiplicadores de 4× a 6× en peticiones/segundo frente a PHP-FPM 8.3. FrankenPHP, el driver Octane recomendado desde Laravel 11, integra el servidor HTTP directamente en el binario PHP — sin necesidad de nginx por delante. En un VPS con acceso root, Docker Compose y Traefik, la migración es limpia y reproducible.

Contenido· PHP-FPM vs Laravel Octane — lo que realmente cambia1/10
  1. 01PHP-FPM vs Laravel Octane — lo que realmente cambia
  2. 02Seis diferencias estructurales a conocer
  3. 03Prerrequisitos antes de empezar
  4. 04Desplegar Laravel Octane + FrankenPHP + Traefik en Docker Compose
  5. 05Lista de compatibilidad antes de pasar a producción
  6. 068 puntos a verificar obligatoriamente
  7. 07FrankenPHP vs Swoole vs RoadRunner: elegir tu driver Octane
  8. 08Monitorizar Octane en producción con octane:status
  9. 09Resolución de problemas: errores frecuentes en producción
  10. 10Próximos pasos

PHP-FPM vs Laravel Octane — lo que realmente cambia

PHP-FPM sigue el modelo "shared nothing": cada petición carga el framework desde cero, ejecuta el código y libera toda la memoria. Es robusto por diseño — una fuga en una petición muere con ella. Octane rompe este contrato. La aplicación se inicializa una vez, luego las peticiones se encadenan en el mismo proceso. La ganancia de rendimiento es masiva, pero la responsabilidad del desarrollador aumenta.

Seis diferencias estructurales a conocer

  • Ciclo de vida compartido — el contenedor IoC de Laravel, los providers y los singletons se instancian una vez y persisten entre todas las peticiones de un worker. Un valor escrito en un singleton en la petición N sigue ahí en la petición N+1.
  • Ganancia de rendimiento documentada — los benchmarks de la documentación oficial de Laravel Octane muestran 4× a 6× más peticiones/segundo frente a PHP-FPM 8.3 en endpoints típicos, al eliminar el bootstrap de PHP en cada petición.
  • Variables estáticas = fugas garantizadas — cualquier propiedad static escrita durante una petición contamina las siguientes. Las Facades de Laravel ocultan referencias estáticas; algunas las limpia Octane, otras no.
  • Archivos estáticos servidos por FrankenPHP — el binario incluye un servidor HTTP completo. En un VPS, no se necesita nginx delante de PHP: FrankenPHP maneja TLS, HTTP/2 y HTTP/3 de forma nativa.
  • Memoria RSS estable ≠ memoria filtrada — la memoria del proceso sube al arrancar y luego se estabiliza. Una fuga real se detecta por crecimiento continuo a lo largo de miles de peticiones, no por el nivel inicial.
  • Workers y concurrencia — Octane arranca --workers=N procesos. Cada worker mantiene su propio estado; no hay memoria compartida entre workers, pero un mismo worker procesa sus peticiones secuencialmente.
  • Compatibilidad Laravel 10 y 11 — Octane 2.x soporta Laravel 10+. FrankenPHP como driver está disponible desde laravel/octane 2.3. PHP 8.2 mínimo, PHP 8.3 recomendado.

Prerrequisitos antes de empezar

Un VPS con 2 GB de RAM mínimo (4 GB recomendados para un stack completo con Redis y queue workers), acceso root, Docker 25+ y Docker Compose v2. La aplicación debe ejecutarse en Laravel 10 o 11, con laravel/octane 2.x instalado (composer require laravel/octane). Traefik 3.x gestionará los certificados Let's Encrypt y el enrutamiento HTTPS. No se necesita nginx adicional: FrankenPHP reemplaza completamente la capa HTTP.

Desplegar Laravel Octane + FrankenPHP + Traefik en Docker Compose

  1. Instalar laravel/octane y publicar la configuración

    En tu proyecto Laravel:

    composer require laravel/octane
    php artisan octane:install --server=frankenphp

    Esto publica config/octane.php y añade las variables de entorno en .env.example. Verifica que OCTANE_SERVER=frankenphp esté definido.

  2. Escribir el Dockerfile para FrankenPHP

    FROM dunglas/frankenphp:1-php8.3-alpine
    
    RUN install-php-extensions \
        pdo_mysql \
        redis \
        opcache \
        pcntl \
        zip \
        intl
    
    WORKDIR /app
    
    COPY --chown=www-data:www-data . .
    
    RUN composer install --no-dev --optimize-autoloader --no-interaction \
     && php artisan config:cache \
     && php artisan route:cache \
     && php artisan view:cache
    
    EXPOSE 8000
    
    CMD ["php", "artisan", "octane:frankenphp", "--host=0.0.0.0", "--port=8000", "--workers=4", "--max-requests=500"]

    La opción --max-requests=500 reinicia el worker después de 500 peticiones: una red de seguridad contra fugas de memoria residuales.

  3. Escribir el docker-compose.yml completo

    services:
      app:
        build: .
        restart: unless-stopped
        environment:
          APP_ENV: production
          APP_KEY: ${APP_KEY}
          DB_HOST: db
          REDIS_HOST: redis
          CACHE_DRIVER: redis
          SESSION_DRIVER: redis
          QUEUE_CONNECTION: redis
          OCTANE_SERVER: frankenphp
        networks:
          - internal
          - traefik
        labels:
          - "traefik.enable=true"
          - "traefik.http.routers.app.rule=Host(`${DOMAIN}`)"
          - "traefik.http.routers.app.entrypoints=websecure"
          - "traefik.http.routers.app.tls.certresolver=letsencrypt"
          - "traefik.http.services.app.loadbalancer.server.port=8000"
        depends_on:
          db:
            condition: service_healthy
          redis:
            condition: service_healthy
    
      worker:
        build: .
        restart: unless-stopped
        command: ["php", "artisan", "queue:work", "redis", "--sleep=3", "--tries=3", "--max-time=3600"]
        networks:
          - internal
        depends_on:
          db:
            condition: service_healthy
          redis:
            condition: service_healthy
    
      scheduler:
        build: .
        restart: unless-stopped
        command: ["sh", "-c", "while true; do php artisan schedule:run >> /dev/null 2>&1; sleep 60; done"]
        networks:
          - internal
    
      db:
        image: mysql:8.0
        restart: unless-stopped
        volumes:
          - db_data:/var/lib/mysql
        networks:
          - internal
        healthcheck:
          test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
          interval: 10s
          timeout: 5s
          retries: 5
    
      redis:
        image: redis:7-alpine
        restart: unless-stopped
        volumes:
          - redis_data:/data
        networks:
          - internal
        healthcheck:
          test: ["CMD", "redis-cli", "ping"]
          interval: 10s
          timeout: 3s
          retries: 5
    
    volumes:
      db_data:
      redis_data:
    
    networks:
      internal:
      traefik:
        external: true
  4. Desplegar Traefik con certificados automáticos

    docker network create traefik
    mkdir -p /opt/traefik/letsencrypt
    touch /opt/traefik/letsencrypt/acme.json
    chmod 600 /opt/traefik/letsencrypt/acme.json
    docker compose -f /opt/traefik/docker-compose.yml up -d
  5. Preparar el archivo .env de producción

    Variables críticas a definir en el VPS:

    APP_ENV=production
    APP_DEBUG=false
    APP_KEY=base64:...  # generado con php artisan key:generate
    DOMAIN=tu-dominio.com
    DB_DATABASE=laravel
    DB_USERNAME=laravel
    DB_PASSWORD=contraseña_fuerte
    DB_ROOT_PASSWORD=root_contraseña
    OCTANE_SERVER=frankenphp
    CACHE_DRIVER=redis
    SESSION_DRIVER=redis
    QUEUE_CONNECTION=redis

    Nunca hagas commit de .env en git. En el VPS, gestiónalo con scp o un gestor de secretos.

  6. Ejecutar las migraciones sin downtime

    docker compose run --rm app php artisan migrate --force

    Para un despliegue zero-downtime, usa el flag --step en migraciones que añaden columnas NOT NULL.

  7. Arrancar el stack y verificar el healthcheck

    docker compose up -d --build
    docker compose ps

    Espera a que db y redis alcancen el estado healthy antes de que app arranque.

  8. Verificar el estado con octane:status

    docker compose exec app php artisan octane:status

    Este comando muestra el servidor activo (frankenphp), el número de workers en ejecución y el estado global. Una respuesta running confirma que los workers responden.

  9. Configurar recarga en caliente para despliegues

    docker compose build app worker scheduler
    docker compose up -d --no-deps app worker scheduler

    Para recargar workers sin cortar conexiones activas:

    docker compose exec app php artisan octane:reload

Lista de compatibilidad antes de pasar a producción

Migrar a Octane no requiere reescribir la aplicación, pero algunos patrones comunes fallan en silencio. Revisa esta lista en tu código antes del primer despliegue.

8 puntos a verificar obligatoriamente

  • Singletons con estado mutable — cualquier servicio registrado con $this->app->singleton() que almacene datos específicos de una petición debe reiniciarse en un OctaneServiceProvider. Auditoría: grep -r 'singleton' app/Providers.
  • Sesiones — con SESSION_DRIVER=redis las sesiones sobreviven correctamente. El driver file puede crear condiciones de carrera con múltiples workers. Migra a redis o database antes de cambiar a Octane.
  • Conexiones de base de datos — Octane mantiene conexiones DB persistentes por worker. Verifica que MySQL acepte max_connections ≥ workers × replicas.
  • Variables estáticas en modelos o servicios — static $cache = [] en un modelo Eloquent persiste entre peticiones. Reemplaza con inyección de dependencia con alcance de petición.
  • Event listeners no idempotentes — un listener registrado en boot() se registra solo una vez, pero se ejecuta en cada petición que dispara el evento. Problema solo si acumula estado global.
  • Jobs y colas — los Jobs no corren en el proceso principal de Octane sino en el contenedor worker separado. No se requieren adaptaciones para el código de los Jobs.
  • Archivos subidos y almacenamiento — monta un volumen para /app/storage para que los archivos persistan entre reconstrucciones de imagen.
  • Paquetes de terceros — verifica la compatibilidad con Octane de cada paquete. Desactiva Debugbar y Telescope en producción.

FrankenPHP vs Swoole vs RoadRunner: elegir tu driver Octane

Desplace la tabla

CriterioFrankenPHPSwooleRoadRunner
InstalaciónImagen Docker oficial `dunglas/frankenphp` — sin extensiones PECL a compilarExtensión PECL `swoole` — requiere `pecl install swoole` o imagen personalizadaBinario Go separado (`roadrunner`) — descargado vía Composer (`spiral/roadrunner`)
HTTP/2 y HTTP/3Nativo — FrankenPHP está construido sobre Caddy que soporta HTTP/3 (QUIC) de serieHTTP/2 soportado, HTTP/3 no nativoHTTP/2 vía plugin, HTTP/3 no soportado nativamente
Compatibilidad con extensiones PHPMuy amplia — basado en PHP-FPM oficial, la mayoría de extensiones se instalan normalmenteIncompatibilidades conocidas con algunas extensiones bloqueantesAmplia, pero algunas extensiones PECL complejas pueden necesitar adaptaciones
Recomendación oficial LaravelDriver recomendado desde Laravel 11 (2024), listado primero en la documentación de OctanePrimer driver histórico, aún soportado pero menos destacadoSoportado, alternativo a Swoole, común en ecosistemas Symfony/Spiral
Complejidad operacionalBaja — un solo contenedor, Dockerfile mínimo, sin demonio separadoMedia — la extensión swoole debe compilarse con la versión PHP correctaMedia — el binario RoadRunner debe sincronizarse con la versión del paquete Composer

Monitorizar Octane en producción con octane:status

El comando php artisan octane:status consulta el servidor FrankenPHP en tiempo real y muestra el número de workers activos y el estado global. Intégralo en tu monitorización:

docker compose exec -T app php artisan octane:status | grep -q 'running' && echo OK || echo FAIL

Para recargar workers sin cortar el tráfico:

docker compose exec app php artisan octane:reload

Sigue la memoria RSS del proceso principal: un crecimiento continuo tras miles de peticiones indica una fuga. El comando docker stats app da la memoria en tiempo real.

Resolución de problemas: errores frecuentes en producción

Fuga de memoria progresiva: la memoria del contenedor app crece sin bajar nunca. Causa probable: un singleton acumula datos por petición. Reduce --max-requests a 100 mientras identificas el origen.

502 Bad Gateway de Traefik: verifica que el contenedor esté en la red traefik, que la etiqueta traefik.http.services.app.loadbalancer.server.port=8000 corresponda al puerto expuesto, y que el contenedor esté healthy.

Worker que reinicia en bucle: busca FATAL en docker compose logs app. Causa más común: una migración no ejecutada que lanza una QueryException al arrancar.

Sesiones perdidas entre peticiones: cambia a SESSION_DRIVER=redis.

Zona horaria incorrecta: añade APP_TIMEZONE en .env y RUN apk add --no-cache tzdata en el Dockerfile.

Próximos pasos

Ya tienes un stack Laravel Octane funcional en VPS. Los siguientes artículos cubren los pasos naturales: [Desplegar Laravel en un VPS](/blog/deployer-laravel-vps), [Desplegar con Traefik](/blog/deployer-avec-traefik), [Docker Compose production checklist](/blog/docker-compose-production-checklist), [Alojar Redis en VPS](/blog/heberger-redis-vps), [PHP 8.2 fin de vida — checklist de migración](/blog/php-82-fin-de-vie-checklist-migration).

Despliegue su infraestructura hoy mismo

VPS Cloud. Entornos preconfigurados disponibles.

¿Necesita ayuda?

Consulte nuestro centro de ayuda y nuestra FAQ, o contacte con nuestro equipo: llamada, WhatsApp o correo electrónico. Soporte en francés, inglés y árabe.

Escribir por WhatsAppse abre en una pestaña nueva