Los Angeles AI Apps logo

Deploying Wordpress with Kamal 2

By Joseph Izaguirre

Image for Deploying Wordpress with Kamal 2

🔥 If you’d like a custom web application with generative AI integration, visit losangelesaiapps.com

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.