Joshua Pangborn

October 4, 2024

Deploying Kirby CMS with Kamal 2

Kamal 2 was recently released. I’ve been deploying my containerized Kirby CMS website with Kamal for several months. It is a simple method for provisioning a server and deploying an application or website. Kamal 2 makes that even simpler for single server websites and I’ve just completed updating to Kamal 2. We’ll walk through how to containerize a Kirby site and deploy it via Kamal 2. 

Containerizing Kirby

Kirby is a web-based CMS built in PHP, so it is relatively easy to containerize. As a base, we’ll begin with the excellent ServerSideUP Docker images for PHP. These are production ready, tested Docker images. SeverSideUP has PHP Docker images using Apache, NGINX, and Unit. I’ve chosen Unit due to the slightly simpler setup because of not needing to deal with the image managing two key processes (the webserver and PHP-FPM). Below is the Dockerfile, I use for containerizing my Kirby sites. We’ll walkthrough the file to explain it.

FROM serversideup/php:8.3-unit

ENV SSL_MODE=off
ENV PHP_OPCACHE_ENABLE=1
ENV UNIT_WEBROOT=/var/www/html

USER root

# Install the intl extension with root permissions
RUN install-php-extensions gd

# Drop back to our unprivileged user
USER www-data

COPY --chmod=755 ./entrypoint.d/ /etc/entrypoint.d/
COPY ./config.d/ /etc/unit/config.d/
COPY --chown=www-data:www-data . /var/www/html

Base Image
The first line starts us off by stating what base image we will be using. As previously, stated, We are using the ServerSideUP Unit image for PHP 8.3.

Configuration Image
The ServerSideUP images provide a bunch of variables that can be used to controll the behavior of the image. For our purposes we are use three variables to make a couple of changes to the default behavior of the image.

  • First, we are turning SSL mode off. Kamal deploys apps behind a proxy in order to provide gapless deployments. Because of this, Kamal will handle the SSL at the proxy level, so it is unneeded on the application container.
  • Next, since this is a production deployment, we will enable the PHP opcache in order to enhance the performance.
  • Finally, Kirby has a bit of different directory structure than other applications, so we will need to change the location of the webroot for Unit.

PHP Extensions
There is one PHP extension that Kirby requires that is not included by default in the ServerSideUP image. But they have provided a simple method for installing additional PHP extensions. So the next three lines will switch to the root user, install the gd extension, and switch back to the unprivileged www-data user.

Copying Files
Finally, we will copy over the necessary files from our local machine into the image. There are three groups of files that we are copying into the image: entrypoint scripts, Unit configuration templates, and the application files.

The entrypoint scripts are scripts that can be used to customize the Docker image during the build process. The ServerSideUP image provides a mechanism for executing these scripts at various points in the build process. The only script that is needed in my Kirby deployment is a script to install the Composer dependencies. The contents of that script are below.

#!/bin/bash
set -e

echo "Install Composer Dependencies"

cd /var/www/html
composer install

The next group of files to copy is the Unit configuration template. The ServerSideUP image comes with a few base Unit configuration files depending on needs. Since we do not need Unit to handle the SSL, we would use the ssl-off.json configuration. However, Kirby has some specific configuration needs that should be done at the webserver level to block access to specific directories. So we will need to provide a custom Unit configuration that includes the needed changes. We will base our configuration on the template provided by the ServerSideUp image with the following addition:

{
    "match": {
        "uri": [
	    "/content/*",
	    "/site/*",
	    "/kirby/*",
	    "/config/*",
	    "/entrypoint.d/*",
	    "/config.d/*",
	    "/.git/*",
	    "/.kamal/*"
	]
    },
    "action": {
        "return": 403
    }
},
```

This will need to be added to the routes array of the ssl-off.json.template. You can find the base configuration at:

https://github.com/serversideup/docker-php/blob/main/src/variations/unit/etc/unit/config.d/ssl-off.json.template

Once you have updated the template, save it in the config.d folder with the same name: ssl-off.json.template.

The last group of files to copy is the application files into the webroot of the container. The copy command also handles setting the correct owner and group for the copied files. When copying files into the container, Docker will ignore files specified in the .dockerignore file. Here is the .dockerignore contents that I use for Kirby sites.

/vendor/
/node_modules/
/.env
/.kamal/
/.nova/

At this point, we have a containerized Kirby application ready for deployment. 

Deploying with Kamal 2

With the container ready, we can setup the Kamal deployment. For the Kirby, I generally deploy only to a single server, so that is what the deployment will focus on. Make sure that you have installed Kamal according to the instructions. Once installed, run `kamal init` to create the necessary files for Kamal. Below is the template of the deploy.yml that we will need in order to deploy the Kirby container we have setup. Again, we will work through each section.

# Name of your application. Used to uniquely configure containers.
service: APPNAME

# Name of the container image.
image: USERNAME/APPNAME

builder:
  arch: amd64

# Deploy to these servers.
servers:
  - IPADDRESS

# Credentials for your image host.
registry:
  username: USERNAME
  password:
    - KAMAL_REGISTRY_PASSWORD

proxy:
  app_port: 8080
  host: APPURL
  ssl: true
  healthcheck:
    path: /healthcheck

In the deploy.yml, you will need to replace some of the place holders with the values relevant to you.

* APPNAME - This is the name of your application.
* USERNAME - This is the user name of your account to your container registry. By default, Kamal uses DockerHub. So you will either need to create an account or look in the Kamal documentation for instructions on using a different container registry.
* IPADDRESS - The IP address of the server you are deploying to.
* APPURL - This is the base URL of your application for the purpose of provisioning an SSL certificate.

Naming
The first two lines are simple naming lines to identify your application, used for naming the containers and pulling the build Docker image from the registry.

Builder
The builder section defines the architecture that the container should target. This will depend on the server you are planning to deploy to. I am using AMD processors in my servers, so it is set to amd64

Servers
Here you will define the servers you are deploying to. In this tutorial, we are deploying to one server. But it is possible to list multiple servers if they are behind a load balancer. We have also left out the role, so Kamal assumes the role is web. It is possible to have servers with different roles, but that is outside the scope of this deployment.

Registry
Here you define your credentials for connecting to the container registry. We already handled the user name, but we still need to discuss the password. Kamal uses a secrets file to define the variables that will be used during the deployment. That file was created with you ran kamal init and in located at .kamal/secrets.  Here is the contents necessary for this deployment.

KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD

This may look redundant, but it simply setting the Kamal Registry Password variable from the local environment variable. I store the local environment variable in the .env file in the root of my project. If you are not using some tool to automatically load the .env file, you can run the following command to do so before running any of the Kamal deployment commands:

set -a; source .env; set +a

Proxy
The final section relates to the Kamal proxy. This handles routing traffic to the applicaiton container, provisioning the SSL certificate, and provides gapless deployment by making sure the new container is healthy before switching to the new version.

The app_port line tells the proxy what port on the application container to route traffic to. The ServerSideUP container uses port 8080, so that is set here.

The host and the ssl entries tell the proxy to provision a SSL certificate from Let’s Encrypt. For this to be successful, you must have DNS entries for the URL that point to the IP address of the server.

The healthcheck entry tells the proxy what the URI of the health check route that is used to verify that the container is up and running correctly. The ServerSideUP container provides the /healthcheck route that we are able to use for this purpose.

That completes the setup. We’re now ready to deploy. When you first provision a server, you will need to run the Kamal setup process to make sure the server is ready to go.

kamal setup

You should have a working server with a deployed application at this point. Once you make future changes to you site and commit it to git, just trigger the deployment process:

kamal deploy

That’s it. I’ve already done close to a dozen deployments with Kamal 2. It’s fast and consistent everytime. So easy. And I know that if I ever need another server, It will be extremely simple to get started. Hope you have the same experience. If you have any questions, reach out on Twitter to @joshua_pangborn.