Deploy a Rails Template with Kamal
Set up and deploy a Rails application template with Kamal.
Untitled is my Rails template that I’ve been maintaining and keeping up-to-date for over four years now. With the release of Rails 8 including Kamal I wanted to document the full setup process, since continuous and local deployments require a bit more work beyond cloning the repository. Follow along to deploy an application to staging and production with database replication for backups and continuous deployment with GitHub Actions.
Preparing the Configuration
To start, we’re going to customize the deployment configuration, set up the GitHub container registry, and set up a required secret.
Setting the Hosts
The template is set up to deploy a production host and optionally a staging host, so let’s customize the deployment configuration files. If you’d like a staging host uncomment and set a host in the config/deploy.yml
file.
proxy: ssl: true- # host: untitled-staging.tristandunn.com+ host: untitled-staging.tristandunn.com healthcheck: path: /health
config/deploy.yml
file.Other destinations in Kamal inherit from the base deployment configuration, so we only need to set the production host there.
proxy: host: untitled.tristandunn.com
config/deploy.production.yml
file.Choosing a Container Registry
By default Kamal uses Docker Hub for the container registry, but I’d prefer to use GitHub instead. If you would like to use GitHub too, you need to create a token with the write:packages
scope. The token goes in our .env
file under the KAMAL_REGISTRY_PASSWORD
variable.
KAMAL_REGISTRY_PASSWORD="GITHUB_TOKEN"
.env
file.If you’d prefer to stick with Docker Hub, or use AWS or GCP, see the official Docker registry documentation for more information.
Defining Local Secrets
We also need a base secret key for Rails, which you can generate by running the bin/rails secret
command and adding it to the same file.
KAMAL_REGISTRY_PASSWORD="GITHUB_TOKEN"SECRET_KEY_BASE="RANDOM_SECRET"
.env
file.That should be enough configuration for you to deploy to a staging host, but if your application requires any other secret environment variables you should add them as well.
Local Deployment
We’re going to start by deploying from a local machine. You’ll need a server to deploy to, which can be any server capable of running Docker. I’m using DigitalOcean since I was deploying with Dokku there already.
The link to DigitalOcean is a referral link which will give you $200 in credit over 60 days and if you eventually spend $25 it will give me $25 in credit.
Creating and Configuring a Server
If you’re only using a production environment then a server with 2GB of memory should work to start, but if you’re running a staging environment on the same machine I’d recommend 4GB of memory or more. With real production traffic, you may also want more memory regardless of the number of environments. If you’re not familiar with creating a server on DigitalOcean, see the quickstart guide for details.
If you’d prefer to automate the creation, the repository includes Terraform configuration in the config/terraform
directory, which can be used via the bin/terraform
executable. The configuration will create a 2GB server in the NYC3 region and create a storage bucket in the same region.
After the server is created, you’ll need to copy the IP address and add it to the config/deploy.yml
file for the web server.
# Deploy to these servers.servers: web: - 192.168.0.1
config/deploy.yml
file.You’ll also want to add DNS entries for your staging and production hosts to point to the same IP address.
Deploying to Staging
With Docker running we can set up our first Kamal application, which will install the required dependencies, such as Docker, on the remote server and deploy the application to the staging host.
$ eval $(cat .env) bin/kamal setup
Note that we’re evaluating the secrets in the .env
file to make them available to Kamal. See the environment variable documentation and how to switch to a secret helper for more information.
Adding Database Replication
Litestream continuously streams SQLite changes to fully replicate the database. It’s basically a drop-in backup solution with litestream-ruby running inside the Puma server. And it works with an assortment of storage providers, including the S3-compatible Spaces from DigitalOcean that we’re going to use.
After creating a bucket take note of the origin endpoint. To access the bucket we need to create an access key which will provide an access key and secret. We then add all three to the .env
file.
KAMAL_REGISTRY_PASSWORD="GITHUB_TOKEN"LITESTREAM_ACCESS_KEY_ID="SPACES_ACCESS_KEY_ID"LITESTREAM_REPLICA_HOST="SPACES_HOSTNAME"LITESTREAM_SECRET_ACCESS_KEY="SPACES_ACCESS_KEY_SECRET"SECRET_KEY_BASE="RANDOM_SECRET"
.env
file.Also, be sure to not include https://
in the host.
Deploying to Production
We’re now ready to deploy to production. If we’ve already set up the staging host we can use the deploy
command, otherwise we need to use the setup
command here with production as the destination.
# If we deployed to staging.$ eval $(cat .env) bin/kamal deploy --destination=production
# If we are only deploying to production.$ eval $(cat .env) bin/kamal setup --destination=production
Continuous Deployment
Manually deploying is nice, but continuously deploying is even nicer. By default the template provides a workflow to deploy to the production destination when merged to the main branch. If you’d like to remove or customize the destination you can update the DESTINATION
environment variable in the workflow.
Secrets for GitHub Actions
We need to mirror the variables in our .env
file so GitHub Actions can build and deploy the project. You can add secrets specifically for your project under Settings -> Secrets and variables -> Actions
.
The first two are to ensure it has access to your server and that Rails has a secret key base.
SSH_PRIVATE_KEY
— A private key with access to your destination server.SECRET_KEY_BASE
— A Rails secret.
If you’re using Litestream you also need to add the following:
LITESTREAM_ACCESS_KEY_ID
— The bucket access key ID.LITESTREAM_REPLICA_HOST
— The bucket hostname.LITESTREAM_SECRET_ACCESS_KEY
— The bucket access key secret.
If you’re using the GitHub Container Registry then it automatically uses the provided GITHUB_TOKEN
for the KAMAL_REGISTRY_PASSWORD
, but if you’re using another registry you should set it as well and update the variable in the .github/workflows/cd.yml
file.
Package Linking
Once you’ve created the package once in the GitHub Container Registry it’s helpful to link it to the repository with administrative access. You should be able to find the package under the “Packages” tab on your profile. Then under the “Package Settings” you can use “Add Repository” to associate your repository and grant it an administrative role. Write access will allow the action to update the package, but it’s helpful to have administrative access if you add a clean up action to delete old images to conserve space.
Deployment
If everything is set up correctly your next push to main should successfully deploy. If you want to try things out in staging first, you can remove the destination and adjust the workflow to trigger on any branch.
Improvements
While we have an application deployed in production, there are a couple of quality-of-life improvements we can make.
Using a Kamal Alias
Loading the environment variables makes the deployment commands pretty verbose. Instead, let’s use a shell alias to handle it all.
# Create a function to ensure Kamal and the environment variables are# present, if an .env file is present.kamal_command() { if [ -f bin/kamal ]; then if [ -f .env ]; then eval $(cat .env) bin/kamal "$@" else bin/kamal "$@" fi else echo "No bin/kamal script found." fi}
# Add a "kamal" alias for the function.alias kamal="kamal_command"
Now deploying is as easy as running kamal deploy
without having to manually include the secrets.
Adding a Litestream Dashboard
You can add a simple Litestream dashboard with a single line, but it’s recommended you add authentication to prevent unauthorized access.
# frozen_string_literal: true
Rails.application.routes.draw do mount Litestream::Engine, at: "/litestream"
# ...end
config/routes.rb
file.Now you can check in on the replication process by visiting /litestream
on your production domain.
Adjusting the Destinations
If we’re never planning to deploy to a production host, we can move the production destination configuration into the config/deploy.yml
file. Now we don’t have to provide a destination when deploying.
One reason to keep it separate even with a single host is that it requires you to be a bit more verbose when manually deploying. Although if we’re using the continuous deployment workflow included that should never be required.
Conclusion
While there’s a bit more setup to start with Kamal compared to Dokku, for some reason it feels like there’s less magic to me. Kamal also comes with added bonus of easier multi-server usage, somewhat having the Rails community behind it, and an easier path forward when you (hopefully) need to scale up.