Laravel + Kubernetes: despliegue de aplicaciones PHP escalables desde cero en 2026
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
filearedisodatabase. - 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_dbSecret
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_REDISDeployment
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: 10Service 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: 804. 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: 60Queue 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-secretsInit 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-secretsImportante: 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-prometheuso 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-phpse 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
requestsylimits. - Queue workers sin graceful shutdown — al actualizar el deployment, el worker se mata a mitad de una tarea. Solución:
terminationGracePeriodSeconds: 60y 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
latesthace 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:
- Migra la aplicación a una arquitectura stateless: Redis para sesiones/caché/colas, S3 para archivos.
- Construye una imagen Docker compacta con multi-stage build basada en PHP 8.3 FPM Alpine.
- Separa la configuración en ConfigMap (datos no secretos) y Secret (contraseñas, claves).
- Configura livenessProbe y readinessProbe para todos los contenedores.
- Define resource requests y limits para una planificación predecible de los pods.
- Usa HPA para las réplicas web y KEDA para los queue workers.
- Ejecuta las migraciones mediante Job (en CI/CD) o Init Container con migraciones idempotentes.
- Configura la recopilación de logs en stderr, métricas a través de Prometheus y trazado con OpenTelemetry.
- Usa Argo CD o Flux para el despliegue GitOps: nada de
kubectl applymanual 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í →