Running Laravel on Kubernetes should be relatively straightforward for someone comfortable with both components (especially if you've been running Laravel in containers for some time).

For better or for worse, I made the leap from Laravel on VMs to Laravel in Containers and on Kubernetes at the same time, so there were some extra hoops to jump through.


Dockerizing Laravel

The first thing to do was to set up the base docker container for Laravel with all of the dependencies it would need to run. Below is the .dockerignore and Dockerfile I built that provides all of the required packages (plus ImageMagick) that Laravel needs to run successfully.

.git
.idea
.env
node_modules
storage/framework/cache/**
storage/framework/sessions/**
storage/framework/views/**
FROM php:7.3-fpm

WORKDIR /var/www

RUN apt-get update && apt-get install -y libmcrypt-dev zip unzip git \
    libmagickwand-dev --no-install-recommends \
    && pecl install imagick \
    && docker-php-ext-enable imagick \
    && docker-php-ext-install pdo_mysql pcntl bcmath

COPY . /var/www

RUN chown -R www-data:www-data \
        /var/www/storage \
        /var/www/bootstrap/cache

RUN mkdir -p /tmp/storage/bootstrap/cache \
    && chmod 777 -R /tmp/storage/bootstrap/cache

The base container I am using is the PHP-FPM image, which isn't a web server, but rather is the process manager generally seen as the defacto for modern deployments – this container will be coupled with an Nginx container which will be actual web serving component of this.


Dockerizing Nginx

The Nginx container is just the alpine container and a custom server configuration block.

The only "interesting" configuration here is the change from the default buffer configuration (fastcgi_buffers and fastcgi_buffer_size) to account for Laravel's large header payloads.

For instructions on how to run TLS to the NGINX container, check out my End-to-End Encryption with Nginx and Kubernetes blog post.

FROM nginx:alpine

ADD vhost.conf /etc/nginx/conf.d/default.conf
server {
    listen 80;
    index index.php index.html;
    root /var/www/public;

    location / {
        try_files $uri /index.php?$args;
    }

    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass 127.0.0.1:9000;
        fastcgi_index index.php;
        fastcgi_buffers 16 16k; 
        fastcgi_buffer_size 32k;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
    }
}

Deploying on Kubernetes

Now that it's containerized, built, and hopefully stored someplace, you can deploy it to Kubernetes. The following is an example Deployment, PodAutoscaler, and LoadBalancer for the service.

You should now have your "Hello World" application up and running.

---
apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 3
  strategy:
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1
  minReadySeconds: 5
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: laravel
          image: ##CONTAINER_IMAGE##
          ports:
            - containerPort: 9000
          resources:
            requests:
              cpu: 250m
            limits:
              cpu: 500m
        - name: nginx
          image: nginx:alpine
          ports:
            - containerPort: 80
---
apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
  name: web
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: web
  minReplicas: 3
  maxReplicas: 20
  targetCPUUtilizationPercentage: 50
---
apiVersion: v1
kind: Service
metadata:
  name: loadbalancer
spec:
  type: LoadBalancer
  ports:
    - port: 80
  selector:
    app: web

Database Migrations

To get database migrations running, you can use the following Kubernetes Job.

Note: Unfortunately, this implementation isn't particularly ideal because it requires you to first delete the job as part of your CI/CD process before it can be redeployed. Please let me know if you have a better way to do it.

---
apiVersion: batch/v1
kind: Job
metadata:
  name: artisan-migration
spec:
  ttlSecondsAfterFinished: 300
  backoffLimit: 3
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: laravel
          image: ##CONTAINER_IMAGE##
          envFrom:
            - configMapRef:
                name: laravel-envvars
          command: ["/usr/local/bin/php", "artisan", "migrate", "--force"]

Scheduled Tasks

Most applications will require some scheduled tasks to run, which can be easily set up with a Kubernetes Cronjob.

---
apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: cronjob
spec:
  schedule: "*/1 * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: laravel
              image: ##CONTAINER_IMAGE##
              command: ["/usr/local/bin/php", "artisan", "schedule:run"]

Laravel Horizon

When your use case needs event-driven processing, and you want to use Laravel Horizon, you can quickly get it up and running with just another Kubernetes Deployment.

---
apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: horizon-worker
spec:
  replicas: 3
  template:
    metadata:
      labels:
        app: horizon-worker
    spec:
      containers:
        - name: laravel
          image: ##CONTAINER_IMAGE##
          command: ["/usr/local/bin/php", "artisan", "horizon
          lifecycle:
            preStop:
              exec:
                command: ["/usr/local/bin/php", "artisan", "horizon:terminate"]

Last, and certainly not least, I want to touch on Secrets management very briefly. It's a vast topic that warrants it's own blog post, but there are many ways to plug environment variables into the Kubernetes runtime. Still, I will emphasize that you should NEVER copy the production .env file into your container.