DevOps

Laravel + Kubernetes: despliegue de aplicaciones PHP escalables desde cero en 2026

Ruslan Ismailov Publicado 12 min de lectura
L

Introducción: por qué Kubernetes se convirtió en el estándar de despliegue para aplicaciones PHP en 2026

Si en 2020 Kubernetes se percibía como una herramienta para grandes empresas, en 2026 es el estándar de facto para cualquier aplicación PHP que deba soportar carga y escalar fácilmente. Los clústeres gestionados de GKE, EKS y AKS se han vuelto más económicos y sencillos de configurar. El ecosistema alrededor de Kubernetes —Helm, Argo CD, Kustomize— ha alcanzado su madurez. Y lo más importante: Laravel como framework se ha adaptado considerablemente mejor al entorno cloud-native gracias a Octane, el soporte nativo de colas Redis y el escalado horizontal de workers.

En este artículo recorreremos todo el camino: desde preparar una aplicación Laravel para una arquitectura stateless hasta escribir manifiestos Kubernetes listos para producción, configurar HPA y organizar las migraciones. El público objetivo son desarrolladores PHP middle y senior, e ingenieros DevOps que ya trabajan con Docker y quieren dar el salto a Kubernetes.

1. Preparación de la aplicación Laravel para trabajar en Kubernetes

Arquitectura stateless

Kubernetes asume que los pods pueden recrearse en cualquier momento. Esto significa que la aplicación Laravel no debe almacenar estado localmente. Verifica los siguientes puntos:

  • Sesiones — migra del driver file a redis o database.
  • Caché — usa Redis o Memcached en lugar del caché en archivo.
  • Colas — Redis o Amazon SQS, no el driver síncrono.
  • Almacenamiento de archivos — almacenamiento compatible con S3 (AWS S3, MinIO), no el disco local.

Variables de entorno

En Kubernetes la configuración se pasa a través de ConfigMap y Secret, no mediante el archivo .env. Asegúrate de que la aplicación lea todos los ajustes a través de env() y config(), y no de valores codificados de forma fija. El archivo .env no debe incluirse en la imagen Docker.

Health Checks

Kubernetes requiere endpoints para verificar el estado del contenedor. Añade un health check sencillo a las rutas:

// routes/web.php\nRoute::get('/healthz', function () {\n    return response()->json(['status' => 'ok']);\n});

Para una verificación más profunda (conexión a la BD, Redis) utiliza el paquete spatie/laravel-health o escribe tu propio controlador que compruebe todas las dependencias y devuelva HTTP 200 o 503.

2. Creación de la imagen Docker para Laravel con compilación multietapa

La compilación multietapa (multi-stage build) permite obtener una imagen de producción compacta sin dependencias de desarrollo ni herramientas de compilación. A continuación, un Dockerfile funcional para Laravel en 2026:

# ---- Stage 1: Node build (para compilar assets) ----\nFROM node:20-alpine AS node-builder\nWORKDIR /app\nCOPY package*.json ./\nRUN npm ci\nCOPY . .\nRUN npm run build\n\n# ---- Stage 2: Composer dependencies ----\nFROM composer:2.7 AS composer-builder\nWORKDIR /app\nCOPY composer.json composer.lock ./\nRUN composer install \\\n    --no-dev \\\n    --no-interaction \\\n    --prefer-dist \\\n    --optimize-autoloader\n\n# ---- Stage 3: Production image ----\nFROM php:8.3-fpm-alpine\n\n# Dependencias del sistema\nRUN apk add --no-cache \\\n    nginx \\\n    supervisor \\\n    libpq-dev \\\n    libzip-dev \\\n    oniguruma-dev \\\n    && docker-php-ext-install \\\n        pdo_pgsql \\\n        pdo_mysql \\\n        zip \\\n        opcache \\\n        pcntl\n\n# Copiamos la aplicación\nWORKDIR /var/www/html\nCOPY --from=composer-builder /app/vendor ./vendor\nCOPY --from=node-builder /app/public/build ./public/build\nCOPY . .\n\n# Configuración de permisos\nRUN chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache\n\n# Optimización de Laravel\nRUN php artisan config:cache \\\n    && php artisan route:cache \\\n    && php artisan view:cache\n\nEXPOSE 9000\nCMD [\"php-fpm\"]

Ten en cuenta que ejecutar config:cache durante la compilación solo tiene sentido si las variables de entorno no cambian entre entornos. En la mayoría de los casos es mejor llamar al cacheo de configuración en el script de entrypoint, después de inyectar las variables desde Kubernetes.

3. Escritura de manifiestos Kubernetes

ConfigMap

Las variables de entorno no secretas las trasladamos al ConfigMap:

apiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: laravel-config\n  namespace: production\ndata:\n  APP_ENV: production\n  APP_DEBUG: "false"\n  LOG_CHANNEL: stderr\n  CACHE_DRIVER: redis\n  SESSION_DRIVER: redis\n  QUEUE_CONNECTION: redis\n  REDIS_HOST: redis-service\n  DB_CONNECTION: pgsql\n  DB_HOST: postgres-service\n  DB_PORT: "5432"\n  DB_DATABASE: laravel_db

Secret

Los secretos (contraseñas, claves) van en Kubernetes Secret (en producción utiliza External Secrets Operator o Vault):

apiVersion: v1\nkind: Secret\nmetadata:\n  name: laravel-secrets\n  namespace: production\ntype: Opaque\nstringData:\n  APP_KEY: base64:TU_CLAVE\n  DB_PASSWORD: TU_CONTRASEÑA\n  REDIS_PASSWORD: TU_CONTRASEÑA_REDIS

Deployment

apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: laravel-app\n  namespace: production\nspec:\n  replicas: 3\n  selector:\n    matchLabels:\n      app: laravel-app\n  template:\n    metadata:\n      labels:\n        app: laravel-app\n    spec:\n      containers:\n        - name: php-fpm\n          image: your-registry/laravel-app:1.2.3\n          ports:\n            - containerPort: 9000\n          envFrom:\n            - configMapRef:\n                name: laravel-config\n            - secretRef:\n                name: laravel-secrets\n          resources:\n            requests:\n              cpu: 250m\n              memory: 256Mi\n            limits:\n              cpu: 1000m\n              memory: 512Mi\n          livenessProbe:\n            httpGet:\n              path: /healthz\n              port: 8080\n            initialDelaySeconds: 10\n            periodSeconds: 15\n          readinessProbe:\n            httpGet:\n              path: /healthz\n              port: 8080\n            initialDelaySeconds: 5\n            periodSeconds: 10

Service e Ingress

apiVersion: v1\nkind: Service\nmetadata:\n  name: laravel-service\n  namespace: production\nspec:\n  selector:\n    app: laravel-app\n  ports:\n    - port: 80\n      targetPort: 8080\n---\napiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: laravel-ingress\n  namespace: production\n  annotations:\n    nginx.ingress.kubernetes.io/proxy-body-size: "50m"\n    cert-manager.io/cluster-issuer: letsencrypt-prod\nspec:\n  ingressClassName: nginx\n  tls:\n    - hosts:\n        - yourdomain.com\n      secretName: laravel-tls\n  rules:\n    - host: yourdomain.com\n      http:\n        paths:\n          - path: /\n            pathType: Prefix\n            backend:\n              service:\n                name: laravel-service\n                port:\n                  number: 80

4. Escalado horizontal (HPA) para workers PHP y colas

HPA (Horizontal Pod Autoscaler) modifica automáticamente el número de réplicas en función de la carga. Para una aplicación Laravel se configura HPA por CPU, y para los Queue Workers, por la longitud de la cola mediante KEDA (Kubernetes Event-Driven Autoscaling).

HPA por CPU para la aplicación web

apiVersion: autoscaling/v2\nkind: HorizontalPodAutoscaler\nmetadata:\n  name: laravel-hpa\n  namespace: production\nspec:\n  scaleTargetRef:\n    apiVersion: apps/v1\n    kind: Deployment\n    name: laravel-app\n  minReplicas: 3\n  maxReplicas: 20\n  metrics:\n    - type: Resource\n      resource:\n        name: cpu\n        target:\n          type: Utilization\n          averageUtilization: 60

Queue Workers con KEDA

Para escalar los workers de Laravel Queue utiliza KEDA con un ScaledObject que reacciona a la longitud de la cola en Redis:

apiVersion: keda.sh/v1alpha1\nkind: ScaledObject\nmetadata:\n  name: laravel-queue-scaler\n  namespace: production\nspec:\n  scaleTargetRef:\n    name: laravel-queue-worker\n  minReplicaCount: 1\n  maxReplicaCount: 10\n  triggers:\n    - type: redis\n      metadata:\n        address: redis-service:6379\n        listName: queues:default\n        listLength: "50"

El Deployment del Queue Worker debe usar el comando php artisan queue:work con el flag --max-time o --max-jobs, para que el worker se reinicie correctamente durante las actualizaciones.

5. Gestión de migraciones de base de datos en Kubernetes

Ejecutar php artisan migrate en Kubernetes no es trivial. Existen dos enfoques:

Kubernetes Job

El Job se ejecuta una vez y finaliza. Es adecuado para pipelines de CD donde la migración se realiza antes de actualizar el Deployment:

apiVersion: batch/v1\nkind: Job\nmetadata:\n  name: laravel-migrate\n  namespace: production\nspec:\n  template:\n    spec:\n      restartPolicy: Never\n      containers:\n        - name: migrate\n          image: your-registry/laravel-app:1.2.3\n          command: ["php", "artisan", "migrate", "--force"]\n          envFrom:\n            - configMapRef:\n                name: laravel-config\n            - secretRef:\n                name: laravel-secrets

Init Container

El Init Container se ejecuta antes del contenedor principal del pod. Es conveniente para aplicar migraciones automáticamente durante el despliegue:

initContainers:\n  - name: migrate\n    image: your-registry/laravel-app:1.2.3\n    command: ["php", "artisan", "migrate", "--force"]\n    envFrom:\n      - configMapRef:\n          name: laravel-config\n      - secretRef:\n          name: laravel-secrets

Importante: al usar Init Container, la migración se ejecutará para cada pod. Para evitar conflictos durante el despliegue paralelo de varias réplicas, asegúrate de que tus migraciones sean idempotentes, o usa Job + PreSync Hook en Argo CD.

6. Monitoreo de la aplicación Laravel en el clúster

Para una observabilidad completa de la aplicación PHP en Kubernetes utiliza el siguiente stack:

  • Prometheus + Grafana — recopilación de métricas del clúster y la aplicación. Usa el paquete spatie/laravel-prometheus o exporta métricas a través del endpoint /metrics.
  • Loki — agregación de logs. Configura Laravel para enviar logs a stderr (LOG_CHANNEL=stderr), y Loki los recopilará automáticamente a través de Promtail.
  • OpenTelemetry — trazado distribuido de solicitudes. El paquete open-telemetry/opentelemetry-php se integra con Laravel y envía trazas a Jaeger o Tempo.
  • Kubernetes Events + Alertmanager — alertas ante caídas de pods, OOMKill y errores de liveness probe.

Configura un dashboard de Grafana con las métricas clave: RPS, latencia p95/p99, cantidad de errores 5xx, uso de memoria de PHP-FPM y longitud de la cola Redis.

7. Errores habituales y cómo evitarlos

  • Almacenar sesiones en archivos — al reiniciarse el pod, los usuarios pierden sus sesiones. Solución: driver Redis.
  • APP_KEY no definida o diferente en distintas réplicas — los datos cifrados en un pod no podrán descifrarse en otro. Solución: un único Secret con APP_KEY.
  • config:cache durante la compilación de la imagen — la configuración cacheada contiene los valores del entorno de compilación, no de producción. Solución: cachear la configuración en el entrypoint o no cachearla en absoluto.
  • Ausencia de resource limits — un pod puede consumir todos los recursos del nodo. Define siempre requests y limits.
  • Queue workers sin graceful shutdown — al actualizar el deployment, el worker se mata a mitad de una tarea. Solución: terminationGracePeriodSeconds: 60 y el flag --stop-when-empty.
  • Sin readiness probe — el tráfico llega a un pod que aún no está listo. Configura siempre readinessProbe de forma independiente a livenessProbe.
  • Imagen sin etiquetas fijas (pinned tags) — usar la etiqueta latest hace el despliegue impredecible. Etiqueta siempre las imágenes con un hash o versión específica.

Conclusión y recomendaciones finales

Desplegar Laravel en Kubernetes en 2026 no es complicado si se aborda de forma sistemática. Aquí tienes una lista de verificación para un despliegue listo para producción:

  1. Migra la aplicación a una arquitectura stateless: Redis para sesiones/caché/colas, S3 para archivos.
  2. Construye una imagen Docker compacta con multi-stage build basada en PHP 8.3 FPM Alpine.
  3. Separa la configuración en ConfigMap (datos no secretos) y Secret (contraseñas, claves).
  4. Configura livenessProbe y readinessProbe para todos los contenedores.
  5. Define resource requests y limits para una planificación predecible de los pods.
  6. Usa HPA para las réplicas web y KEDA para los queue workers.
  7. Ejecuta las migraciones mediante Job (en CI/CD) o Init Container con migraciones idempotentes.
  8. Configura la recopilación de logs en stderr, métricas a través de Prometheus y trazado con OpenTelemetry.
  9. Usa Argo CD o Flux para el despliegue GitOps: nada de kubectl apply manual en producción.

Kubernetes proporciona a la aplicación Laravel escalado horizontal, autorrecuperación y despliegues predecibles. La inversión en una configuración inicial correcta se amortiza en el primer pico de carga. Comienza con un clúster local usando kind o k3d, migra un microservicio o un monolito Laravel, y construye progresivamente un pipeline de producción completo.

Tecnologías

Etiquetas

Ruslan Ismailov

Desarrollador Senior Web / Backend. Desarrollador senior web/backend con 9 años de experiencia. Stack: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, microservicios, CI/CD. Más sobre mí →