EngineeringNovember 20257 min read

Laravel to Kubernetes, Simplified with Skyhook

By Skyhook Team

Laravel to Kubernetes, Simplified with Skyhook

Laravel is a PHP heavyweight, but running it in production usually means stitching together many moving parts: runtime, scaling, security, TLS, observability, and secrets. Kubernetes solves the runtime puzzle but introduces a new learning curve: container images, manifests, deployments, scaling, service discovery, and secrets management.

Below are two ways to ship Laravel to Kubernetes:

  • The manual route: containerize the app, write YAML, own every knob.
  • The Skyhook route: let Skyhook automate Day 1 bootstrap and handle Day 2 operations for you.

What you will build

  • Web app: Nginx with PHP-FPM serving Laravel
  • Queue workers: Horizon deployment
  • Scheduler: CronJob running php artisan schedule:run every minute
  • Database migrations: pre-deploy Job
  • Ingress with TLS via cert-manager
  • Autoscaling via HPA, logs to stdout, metrics to Prometheus

Prerequisites

RequirementManual pathSkyhook path
Kubernetes clusterYou create or have oneCreate or connect from Skyhook
DockerNeeded to build the imageNeeded, Skyhook can assist
Container registryNeededBuilt in or external (ECR, GCR, ACR, GHCR)
Git repositoryNeededNeeded

Option 1: Manual deployment

1) Containerize Laravel (PHP-FPM behind Nginx)

PHP-FPM is not an HTTP server. Expose Nginx on port 80 and keep PHP-FPM internal.

Dockerfile (multi-stage)

FROM composer:2 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-interaction --prefer-dist --no-scripts
 
FROM node:22-alpine AS assets
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY resources resources
RUN npm run build
 
FROM php:8.3-fpm-alpine AS app
RUN apk add --no-cache nginx curl libzip-dev icu-dev \
 && docker-php-ext-install pdo_mysql zip intl opcache
WORKDIR /var/www/html
 
# App files
COPY . .
COPY --from=vendor /app/vendor ./vendor
COPY --from=assets /app/public/build ./public/build
 
# Cache config and routes for prod
RUN php artisan config:cache && php artisan route:cache
 
# Nginx
COPY ./infra/nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["sh", "-c", "php-fpm -D && nginx -g 'daemon off;'"]

Minimal Nginx config

events {}
http {
  server {
    listen 80;
    root /var/www/html/public;
    index index.php index.html;
 
    location /health { return 200 "ok"; }
 
    location / {
      try_files $uri $uri/ /index.php?$query_string;
    }
 
    location ~ \.php$ {
      fastcgi_pass 127.0.0.1:9000;
      include fastcgi_params;
      fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
  }
}

Build and push:

docker build -t registry.example.com/laravel-demo:1.0 .
docker push registry.example.com/laravel-demo:1.0

2) Write Kubernetes manifests (abbreviated)

Web Deployment and Service

apiVersion: apps/v1
kind: Deployment
metadata: { name: laravel-web, labels: { app: laravel-web } }
spec:
  replicas: 2
  selector: { matchLabels: { app: laravel-web } }
  template:
    metadata: { labels: { app: laravel-web } }
    spec:
      containers:
        - name: web
          image: registry.example.com/laravel-demo:1.0
          ports: [{ containerPort: 80 }]
          envFrom: [{ secretRef: { name: laravel-env } }]
          readinessProbe: { httpGet: { path: /health, port: 80 }, periodSeconds: 5 }
          livenessProbe: { httpGet: { path: /health, port: 80 }, initialDelaySeconds: 30 }
          resources:
            requests: { cpu: "200m", memory: "256Mi" }
            limits: { cpu: "1", memory: "512Mi" }
          securityContext: { runAsNonRoot: true }
---
apiVersion: v1
kind: Service
metadata: { name: laravel-web }
spec:
  selector: { app: laravel-web }
  ports: [{ port: 80, targetPort: 80 }]

Ingress with TLS via cert-manager

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: laravel
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt
spec:
  tls:
    - hosts: [app.example.com]
      secretName: laravel-tls
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend: { service: { name: laravel-web, port: { number: 80 } } }

Migrations as a pre-deploy Job (works well with Argo CD hooks)

apiVersion: batch/v1
kind: Job
metadata:
  name: laravel-migrate
  annotations:
    argocd.argoproj.io/hook: PreSync
    argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
spec:
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: migrate
          image: registry.example.com/laravel-demo:1.0
          command: ["php", "artisan", "migrate", "--force"]
          envFrom: [{ secretRef: { name: laravel-env } }]

Queue workers (Horizon)

apiVersion: apps/v1
kind: Deployment
metadata: { name: laravel-horizon, labels: { app: laravel-horizon } }
spec:
  replicas: 2
  selector: { matchLabels: { app: laravel-horizon } }
  template:
    metadata: { labels: { app: laravel-horizon } }
    spec:
      containers:
        - name: horizon
          image: registry.example.com/laravel-demo:1.0
          command: ["php", "artisan", "horizon"]
          envFrom: [{ secretRef: { name: laravel-env } }]
          resources:
            requests: { cpu: "200m", memory: "256Mi" }
            limits: { cpu: "1", memory: "512Mi" }

Scheduler

apiVersion: batch/v1
kind: CronJob
metadata: { name: laravel-schedule }
spec:
  schedule: "* * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: Never
          containers:
            - name: schedule
              image: registry.example.com/laravel-demo:1.0
              command: ["php", "artisan", "schedule:run"]
              envFrom: [{ secretRef: { name: laravel-env } }]

External Secrets example

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata: { name: laravel-env }
spec:
  refreshInterval: 1h
  secretStoreRef: { name: gcp-sm, kind: ClusterSecretStore }
  target: { name: laravel-env }
  data:
    - secretKey: APP_KEY
      remoteRef: { key: projects/123/secrets/laravel-app-key }
    - secretKey: DB_HOST
      remoteRef: { key: projects/123/secrets/laravel-db-host }
    - secretKey: REDIS_HOST
      remoteRef: { key: projects/123/secrets/laravel-redis-host }

HPA for web

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata: { name: laravel-web }
spec:
  scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: laravel-web }
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Resource
      resource: { name: cpu, target: { type: Utilization, averageUtilization: 60 } }

Key Laravel env tips

APP_ENV=production
APP_DEBUG=false
APP_KEY=base64:...     # generate once and store in your cloud secrets manager
SESSION_DRIVER=redis
CACHE_DRIVER=redis
QUEUE_CONNECTION=redis
LOG_CHANNEL=stderr

Apply your YAML:

kubectl apply -f k8s/

You will still need to install and wire cert-manager, External Secrets, Prometheus, Grafana, and logging, and you will own upgrades, rollouts, and drift.

Skyhook supports a hybrid workflow. You keep your repos and clusters. Skyhook automates Day 1 bootstrap and Day 2 operations.

Day 1: Bootstrap with Terraform (one click)

  • Click Create Cluster in the Skyhook console and choose cloud and region.
  • Skyhook commits a ready made Terraform module to your infra repo and triggers CI.
  • Result: VPC, managed Kubernetes, node pools, OIDC, IAM roles, Argo CD, cert-manager, External Secrets, Grafana, Prometheus, and Loki.

You can also bring your own Terraform and clusters.

Day 2: Deploy Laravel from the Skyhook UI

  • Create a Service by pointing Skyhook to your Git repo.
  • Build and push using your Dockerfile or Buildpacks. Skyhook handles registry credentials.
  • Environment variables and secrets mapped through your cloud secrets manager via External Secrets.
  • Add ons with a click such as cert-manager for automatic TLS, External Secrets for rotation, Redis or RabbitMQ charts for queues.
  • Deploy which generates an Github Actions flow or an Argo CD Application, commits it to your apps repo, and rolls it out.
  • Observe and scale a full open source observability setup with dashboards and autoscaling policies.

You don’t have to deal with YAML and kubectl apply, but they’ll always be available for you if you want to dig in yourself.

Skyhook value recap

BenefitWhat it means for Laravel teams
Best practices out of the boxProduction networking, probes, HPA, log aggregation, and TLS configured for you
No lock inManifests live in your Git repos and run on any CNCF conformant Kubernetes
Plug and play setupCluster up and add ons installed in minutes, no scratch Helm or HCL
Empower developersSelf service deploy, debug, and rollback without waiting on DevOps
Designed to scaleMulti cluster support, RBAC, and policy controls that grow with you

Conclusion

You can deploy Laravel on Kubernetes by hand, but teams usually rebuild the same pieces: web, workers, scheduler, migrations, TLS, secrets, and safe rollouts. Skyhook gives you those pieces out of the box, commits them to your Git repos, and lets you operate them from a single UI. Spin up a free Skyhook workspace, point it at your repo, and ship web, workers, scheduler, and migrations behind TLS in minutes.