Post

Laravel to MicroK8s

Kubernetes can be difficult to learn. In this post I'll help you to get your hands dirty by deploying a Laravel application and to a MicroK8s cluster. From there you've got a nice entrance into exploring the vast world of Kubernetes.

Introduction

I recently moved from Enschede to Groningen. This introduced a nice opportunity for me to speak at the Groningen PHP meetup about deploying a PHP application to a MicroK8s cluster. I thought it would be nice to also turn it into a blog post.

We will explore the process of deploying a Laravel application to MicroK8s, a lightweight Kubernetes distribution ideal for development environments. MicroK8s offers several advantages, including ease of setup, low resource consumption, and seamless integration with various Kubernetes tools. By the end of this guide, you’ll understand how to containerize your Laravel app using Docker, configure it for Kubernetes, and deploy it with Helm

Find the source code on GitLab.

Prerequisites

Before we start, ensure you have the following installed and configured:

MicroK8s:

Docker:

  • Docker Installation Guide
  • Verify installation: docker --version
    • For Linux users: Set up Docker Buildx to enable multi-platform builds:
      • Install Buildx: sudo apt install docker-buildx-plugin
      • Verify Buildx: docker buildx version

Creating our docker images

App with PHP-FastCGI Process Manager

Now we need to create the docker images. To run our PHP code we’ll use the PHP FastCGI (Common Gateway Interface) Process Manager better known as PHP-FPM. FPM is used to dramatically improve the performance of your web server. It does so by working as a process manager which spawns multiple processes that can handle your PHP requests concurrently. This leads to a significant reduction in latency for the user. Let’s have a look at its docker file

1
2
3
4
5
6
7
8
9
10
11
12
13
FROM php:8.3.8-fpm-alpine3.20

WORKDIR /var/www/app

COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer

COPY --chown=www-data:www-data . /var/www/app

RUN composer install

RUN rm /usr/local/bin/composer

EXPOSE 9000

NGINX Proxy

In order to expose our PHP-FPM instance to the internet we need something to funnel the network traffic into our application. In our case we’ll use NGINX which proxies the traffic using the fastcgi_pass proxy method. Below is the configuration for our NGINX server:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
server {
    listen 80;
    server_name app.localhost;
    root /var/www/app/public;

    index index.html index.htm index.php;

    charset utf-8;

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

    error_page 404 /index.php;

    location ~ \.php$ {
        fastcgi_pass app:9000;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }
}

Now that we have a basic configuration in place we can create a docker image that uses it:

1
2
3
4
5
6
7
FROM nginx:1.26.1-alpine3.19

COPY ./deployments/docker/proxy/default.conf /etc/nginx/conf.d/default.conf

COPY . /var/www/app

EXPOSE 80

Pushing the images to MicroK8s

Now we need to make our docker images available inside the MicroK8s cluster. We can do this by enabling the registry plugin. Doing so allows us to push our images to the registry localhost:32000.

1
microk8s enable registry

Now that we have two docker files aand somewhere to push them to it’s time to bring the docker part together. To easily build multiple targets we can create a bake file which contains all the information of our targets.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
group "default" {
    targets = [
        "proxy",
        "app"
    ]
}

variable "TAG" {
    default = formatdate("YYYYMMDDhhmm", timestamp())
}

variable "REGISTRY" {
    default = "localhost:32000"
}

function "tags" {
    params = [name]
    result = [
        "${REGISTRY}/${name}:${TAG}",
        "${REGISTRY}/${name}:latest"
    ]
}

target "app" {
    context = "."
    dockerfile = "deployments/docker/app/Dockerfile"
    tags = tags("app")
}

target "proxy" {
    context = "."
    dockerfile = "deployments/docker/proxy/Dockerfile"
    tags = tags("proxy")
}

Helm is a Last but not least execute the following command to build and push your image to the MicroK8s image registry:

1
docker buildx bake -f deployment/docker/bake.hcl --push

Helm templates

Now that the images are available to our MicroK8s cluster we can start tinkering. For the most common commands the Kubernetes docs contains a nice quick reference section with commonly used commands.

To make our lives easier we’ll make use of Helm. Helm is a templating language to manage bundling of kubernetes resources. Let’s create a separate helm template for both the app and our proxy layer:

1
helm create app && helm create proxy

Now we need to make some minor changes to the values file of app:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
...

image:
--- repository: nginx
+++ repository: localhost:32000/app
---  pullPolicy: Always
+++  pullPolicy: IfNotPresent
---  tag: ""
+++  tag: "latest"

...

service:
  type: ClusterIP
---  port: 80
+++  port: 9000

...

In order for our cluster to accept traffic we’ll need to enable MicroK8s ingress:

1
microk8s enable ingress

And let’s also update our NGINX helm chart to point to the right images. We’ll also enable the ingress to allow traffic to enter our proxy layer.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
...

image:
---  repository: nginx
+++  repository: localhost:32000/proxy
---  pullPolicy: IfNotPresent
+++  pullPolicy: Always
  # Overrides the image tag whose default is the chart appVersion.
---  tag: ""
+++  tag: "latest"

...

ingress:
---  enabled: false
+++  enabled: true
  className: ""
  annotations: {}
    # kubernetes.io/ingress.class: nginx
    # kubernetes.io/tls-acme: "true"
  hosts:
---    - host: chart-example.local
+++    - host: app.localhost
      paths:
        - path: /
          pathType: ImplementationSpecific

...

Deploying! 🎉

Now let’s deploy our application using the following commands in the app and proxy directories respectively:

1
2
3
# Execute from the project root
microk8s helm upgrade -i -f deployments/helm/app/values.yaml app deployments/helm/app 
microk8s helm upgrade -i -f deployments/helm/proxy/values.yaml proxy  deployments/helm/proxy

We can now check on our application by running:

1
2
mirok8s kubectl get pods app
mirok8s kubectl get pods proxy

And we can check the logs by running:

1
2
mirok8s kubectl logs app
mirok8s kubectl logs proxy

Finally navigating to app.localhost should show us our laravel default page.

Wrapping Up

We’ve taken an empty Laravel project and written Dockerfiles for it. We added a docker bake file to it to easily manage multiple images. This docker bake files pushes our images to the MicroK8s single node local kubernetes cluster. From there we’ve written helm files that allow us to easily deploy our application with all the necessary requirements. Finally, we deployed the application and checked if it was healthy. Now it’s time to tinker! If you run into any problems comment below, and I’ll do my best to help out!

In the next part I’ll add vault to our project and retrieve secrets from it.

This post is licensed under CC BY 4.0 by the author.