Upgrading to Kamal 2

Kamal

Back in February I wrote about an exciting new deployment tool called Kamal announced by DHH and 37signals. It really was a great gift and I quickly switched my personal projects over to Kamal and hosting on cheap VPS’s a Hetzner. The cool-aid was flowing and it tasted.. well, great!

It was a bit fiddly to get working and had a few limitations - such as deploying multiple apps on one host was a challenge - but for my use case it was perfect. Been using it ever since.

Kamal 2

Just in time for Rails World 2024, Donal from 37signals announced Kamal 2 which introduced some key enhancements:

  • kamal-proxy that replaces traefik
  • automatic HTTPS and provisioning with Let’s Encrypt
  • support for multiple apps on one server
  • command aliases
  • move away from .env for secrets

Upgrading to Kamal 2

I spent some time cautiously setting up a beta subdomain and deployed a second instance of my app to fresh servers just for testing the upgrade, but it was unbelievably straightforward. So much so, I was convinced it hadn’t changed anything but yep, it was that seamless!

The official upgrade guide is likely all you’ll need but I made the following brief notes to capture some nuances that tripped me up, and mainly to remind future me, but hopefully may help someone who stumbles on this page in the future.

1. Upgrade the Kamal gem

First things first, we need to update the gem! The guide tells us to make sure we have v1.9.0 or install if not, and then confirm we can do a deploy, since this is the first version that can reverse the upgrade we’re about to perform.

gem install kamal --version 1.9.0

Once we’ve deployed to confirm its all still working, we now can upgrade kamal to the latest. At the time of writing this is 2.1.1 but lets get the latest

gem install kamal

2. Deploy.yml Configuration Changes

OK! Now we’ve tested 1.9.0 and have a way back, and then installed the latest 2.x version of Kamal we’re ready to update the configurations.

Firstly we need to specify the architecture we’re building for. Remember Kamal uses Docker, and in my case its amd64. You can still do multi-architecture builds by adding another attribute eg arm64 but in my case, I dont need that.

builder:
  arch: amd64

The big change in Kamal 2 is kamal-proxy coming in to replace Traefik. So, we can remove all the previous configurations we had for traefik which in my case looked like this:

# Remove everything under `traefik:`
# traefik:
#   options:
#     publish:
#       - 443:443
#   args:
#     entryPoints.websecure.address: ":443"
#     api: true
#   labels:
#     traefik.http.routers.dashboard.rule: ...

Having removed the traefik config above, we need to configure kamal-proxy. Fortunately this is super simple. Add the following, ensuring to substitute in your-domain of course:

proxy:
  ssl: true
  host: your-domain.com

Some notes here. The docs were a bit confusing since setting ssl: true seemingly sets up Let’s Encrypt to issue an SSL cert, but only when you have one server.

In my case I’m using Cloudflare for SSL/TLS encryption, and despite reading I should set ssl: false, after much fiddling around, I found I did indeed need to set it to true and make sure Cloudflare Encryption Mode was set to “Full”.

The other gotcha for me was with Kamal 1.x traefik accepted the incoming connections on port 443 and expected the Rails server to be running on port 3000.

The default behaviour for Kamal 2.x and kamal-proxy is it to connect to port 80 on the Rails server. This is due to the addition of Thruster - more on that in a minute - but for now just know if you don’t plan to add Thruster, you need to tell the proxy to use port 3000 by adding app_port: 3000 under the proxy: key.

Lastly for the deploy.yml, you can optionally add aliases in Kamal 2.x. This is pretty handy. I was always looking up how to connect to the rails console on my app server for example. These are the defaults, but feel free to add your own too as needed

aliases:
  console: app exec --interactive --reuse "bin/rails console"
  shell: app exec --interactive --reuse "bash"
  logs: app logs -f
  dbc: app exec --interactive --reuse "bin/rails dbconsole"

So now you can just enter kamal console on the command line and boom, you’re dropped right into the console!

3. Move .env to .kamal/secrets

Next we need to update our secrets! Previously in Kamal 1.x it simply used the .env file for secrets which is really convenient but somewhat fragile as they serve other purposes too.

In Kamal 2.x a new .kamal/secrets file was introduced specifically for your secrets. The easiest way if you’re upgrading is to reference the env vars in .env that you need for your app. Create the file .kamal/secrets and add the following:

KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
RAILS_MASTER_KEY=$(cat config/master.key)
POSTGRES_PASSWORD=$POSTGRES_PASSWORD
S3_ACCESS_KEY_ID=$S3_ACCESS_KEY_ID
S3_SECRET_ACCESS_KEY=$S3_SECRET_ACCESS_KEY

One gotcha: I found I needed to prefix kamal commands with dotenv now to have the secrets available, so just fyi if you have issues with secrets, it may be this! eg. dotenv kamal deploy

4. Thruster

Thruster wraps Puma and provides some great features like Basic HTTP Caching, X-Sendfile support and compression, HTTP/2 support and Automatic TLS certificate management with Let’s Encrypt

It so simple to add, and since it default in Rails 8, lets go for it. First we need to add the gem:

bundle add thruster

Next, if you’re not using Rails 8 yet, you’ll need to add the command script, so create a new file bin/thrust and copy in the following, being sure to set the permission on that file after creating it with chmod 755 bin/thrust from your root directory:

#!/usr/bin/env ruby
require "rubygems"
require "bundler/setup"

load Gem.bin_path("thruster", "thrust")

Lastly we need to configure Docker to fire up Thruster by updating Dockerfile as below. Replace the first three lines with the second three lines:

- # Start the server by default, this can be overwritten at runtime
- EXPOSE 3000
- CMD ["./bin/rails", "server"]
+ # Start server via Thruster by default, this can be overwritten at runtime
+ EXPOSE 80
+ CMD ["./bin/thrust", "./bin/rails", "server"]

As you can see, this is where we’re setting Rails to run on port 80 as mentioned in step 2 above.

5. Run the upgrade command

OK!! Now we’re ready to rumble. WARNING run this on a test server first!

Here we go. Simply run the following to initiate the in-place upgrade process

kamal upgrade

This will remove traefik from your containers, add kamal-proxy and then reboot the current app.

Amazingly in my case, it Just Worked(TM)! I was so surprised I had to double check processes on the server to make sure, but yes, worked first time!

6. Rolling Back

If things didn’t go to plan, since we installed 1.9.0 (hopefully you did) then we can rollback.

Firstly, lets uninstall kamal 2.x. Make sure to set the version to the one you installed. In my case it was 2.1.1

gem uninstall kamal -v 2.1.1

Now lets check you have kamal 1.9.0 ready. Running the command below should return the output 1.9.0

kamal version

Next you need to revert the changes to deploy.yml and Dockerfile from steps 2 and 3 above, and then finally we can run the downgrade command that removes kamal-proxy and adds traefik back to your containers

kamal downgrade

Wrapping up

This is a really niche post. Only helpful if you a) installed Kamal 1.0 and b) for some reason don’t want to follow the official guides, but I like to document this stuff if only for myself. If it helps someone else. That’s a bonus!

As always, any questions of feedback, don’t hesitate to drop me a note on twitter or via any other channels listed here