WordPress ❤️ Kamal
WordPress is a great way to create and publish a website. It’s open source, there’s tons of free plugins, and it lets you design your website block by block. What’s not to like?
The issue is with deployment. Using a managed hosting provider like Siteground let’s you get started quickly, at a cost. Siteground’s cheapest plan starts at $2.99 per month, for the first year. After which, it jumps to $17.99 per month! WordPress.com has a plan starting at $4 per month, which sounds cheap, until you realize that you can’t install any plugins! You have to play $25 per month for that privilege.
You could do a DigitalOcean WordPress 1-Click install. This let’s you spin up a Droplet with WordPress pre-installed in 1 click (theoretically). But it comes with the default LAMP stack (Linux, Apache, MySQL and PHP). Wanna try Nginx, rather than Apache? Good luck! Want to deploy multiple WordPress sites to one Droplet? That’ll require a lot of tinkering, and what if something goes wrong?
There’s another way: Kamal. Kamal is an open source deployment tool developed by the team at 37Signals, and is used by them (and many others!) for deploying anything that can be containerized. It comes with zero downtime deploys on anything from cloud VMs to your own bare metal. It’s extremely flexible, allowing you to describe how many servers to deploy to and which accessories to attach like MySQL or Redis. The CLI is amazing as well, allowing for rollbacks to previous containers, checking server logs and, of course, deploying. Kamal 2 comes with even more features, such as automatic HTTPS and deploying multiple applications to one server.
Perhaps best of all, you learn how to self deploy your own web apps. Kamal works with any web app that can be containerized, from Rails to Laravel. Once you learn how deployment works, you can take your learnings with you on your journey to mastering web development. Take the fear out of self-hosting!
Let’s deploy a vanilla LAMP stack WordPress application with Kamal. This tutorial is for those with a basic understanding of SSH, Docker, and Bash.
1) Before getting started
There are a few things you need to ensure before getting started:
-
Spin up a VM from your favorite provider. I prefer Hetzner because it’s cheap, but DigitalOcean works fine as well. Don’t touch your server, Kamal will handle everything for us.
-
Buy a domain and point it to your server by setting the appropriate A record
-
Set up a container registry. I use Dockerhub, but anything works. 37signals is actively working on removing this requirement, so hopefully Kamal 3 deployments will be even more seamless. 🤞🏽
2) Install Kamal
Back on your laptop: Install Kamal. You’re gonna need a Ruby environment for this. I use Mise, but you could use Rbenv as well.
gem install kamal
Check the docs for more details: https://kamal-deploy.org/docs/installation/
3) Set up your project
Once installed, let’s get started by creating our new project. You can call it whatever you want, I’ll go with kamalpress
mkdir kamalpress
cd kamalpress
git init
kamal init
The kamal init command will automatically create a deploy.yml file and a .kamal/secrets file. Here’s a minimal deploy.yml for deploying Wordpress:
# config/deploy.yml
# Replace with your app name
service: kamalpress
# Replace with your Dockerhub/Registry username
# And your app name
image: username/kamalpress
servers:
web:
# Replace with the IP address of your server
- 192.168.0.1
proxy:
# Replace with your domain name
host: example.com
ssl: true
healthcheck:
# Kamal checks for /up by default, this fixes it.
path: /
registry:
# Replace with your Dockerhub/Registry username
username: username
password:
- KAMAL_REGISTRY_PASSWORD
builder:
# This depends on your computer. Use arm64 if using arm
arch: amd64
env:
clear:
WORDPRESS_DB_HOST: kamalpress-db
# Can replace if you want to use a different user
WORDPRESS_DB_USER: root
WORDPRESS_DB_NAME: kamalpress_production
secret:
- WORDPRESS_DB_PASSWORD
volumes:
- "wordpress:/var/www/html"
accessories:
db:
image: mysql:8.4
# Replace with the IP of your server
host: 192.168.0.1
env:
secret:
- MYSQL_ROOT_PASSWORD
files:
- db/production.sql:/docker-entrypoint-initdb.d/setup.sql
directories:
- data:/var/lib/mysql
A few notes:
-
Make sure you replace with your own info: app name, ip address, domain, etc.
-
Don’t expose a port mapping for your MySQL database! You don’t want to expose your database to the outside world. The web app container can simply refer to the MySQL container via it’s container name, set automatically by Kamal: kamalpress-db.
-
We’re using a persistent volume for Wordpress’s data at /var/www/html
Let’s set the environment variables needed for the containers. Open up the autogenerated .kamal/secrets file and set the following variables:
# .kamal/secrets
KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
MYSQL_ROOT_PASSWORD=$MYSQL_ROOT_PASSWORD
WORDPRESS_DB_PASSWORD=$MYSQL_ROOT_PASSWORD
In order to deploy, you need to set two secret environment variables in your local environment. Don’t commit these to version control!
-
KAMAL_REGISTRY_PASSWORD: The password or token used to log in to your container registry.
-
MYSQL_ROOT_PASSWORD: The password you want to set for the root user on creation of the MySQL database. You can use a secure password generator for this. You don’t have to use the root user. If you’d like, you can create and use a user with less privileges.
You can set these environment variables however you like. I use Mise, but you can use dotenv or even a secrets manager like 1Password.
Let’s examine these two lines from the deploy.yml:
# config/deploy.yml
# ...
files:
- db/production.sql:/docker-entrypoint-initdb.d/setup.sql
# ...
The very first time you deploy, when setting up the database, this copies db/production.sql over to the MySQL docker container for database initialization. Create the following file:
# db/production.sql
CREATE DATABASE kamalpress_production;
This simply creates the kamalpress_production database. If you need any MySQL plugins on database initialization, this is the place to put them!
Lastly, let’s create a Dockerfile at the project root. This is the Dockerfile that Kamal will use to create an image for your project:
# Dockerfile
FROM wordpress:6.7.1-apache
EXPOSE 80
We’re simply extending the official Wordpress base image and exposing port 80. You can go fancy here and specify the PHP version, or even extend the PHP base image and install some custom packages.
4) Deploy Time 🚀
Time to ship! Make sure you have your server running with your SSH key and a domain pointing to the server. Commit everything to Git, then run kamal setup. This will do the following:
-
SSH into your remote server and install Docker.
-
Build and run a container for your MySQL database. This will pull the MySQL image into your container and run the production.sql file that we wrote earlier. The container will run with the environment variables specified in the db section of your deploy.yml.
-
Build your Dockerfile locally, push the image to your container registry, pull the image onto your server and run your Wordpress container with port 80 exposed. This container will communicate with the MySQL container using the container name kamalpress-db.
-
Create a 3rd container for kamal-proxy. This proxy provides automatic HTTPS and routes requests to the WordPress container.
The whole process takes under 1 minute on my MacBook, even faster once the cache is warm. Future deploys should use kamal deploy, not kamal setup. kamal setup is used when deploying for the very first time (installing Docker, etc), kamal deploy for every time after. Once it’s done running, navigate to your domain and set up WordPress!
What if you wanted to deploy another WordPress site to the same box?
-
Copy the entire directory for your project.
-
Replace every instance of kamalpress with the name you want to give your new WordPress site.
-
Change the domain name in deploy.yml
-
(Optional) Change the root MySQL password. Might not be a good idea to have all of your sites using the same root password.
-
Run kamal setup, and profit!
Bonus: Server Hardening 101
If you’re gonna be deploying to your own hardware, you should know some basic server hardening. SSH into your server (or use the built-in console by DigitalOcean or Hetzner) and make the following changes. I’m using vi in this example, but you can use nano if you’re more comfortable with that. Careful here: go step by step, you don’t want to lock yourself out of your own server!
vi /root/.ssh/authorized_keys
→ Add the public key for your laptop/computer
vi /etc/ssh/sshd_config
→ PasswordAuthentication no
systemctl restart ssh
ufw allow OpenSSH
ufw default deny incoming
ufw enable
Briefly:
-
Adds your SSH key to your server. DigitalOcean and Hetzner can automate this when creating a new server.
-
Edit your sshd_config to disable password authentication. This prevents hackers from brute-forcing their way into your server.
-
Sets up a firewall so that the server can only be accessed by SSH. All other ports are denied by default.
If using a cloud provider, go ahead and throw on the cloud firewall as well, allowing only ports 22 (SSH), 80 (HTTP) and 443 (HTTPS).
That’s all for a very basic setup. You could go even further, such as scaling Wordpress to multiple boxes with a load balancer in front, and install custom packages in your Dockerfile. Wanna get ultra-lightweight and swap MySQL with SQLite, and Apache for Nginx? You can! With Kamal, you have complete control over your Wordpress installation, while remaining remarkably simple to deploy.