Buy my course: Security for Rails Developers.
Kamal 2 was released recently and it brings a few singnificant changes. Traefik is replaced by kamal-proxy, Kamal runs all containers in a custom Docker network and secrets are passed differently to new containers. All these changes mean that the upgrade is not simple, but in this article I will walk you through an example to help with the process.
To make sure you can roll back if needed, it is advised to upgrade to Kamal 1.9 first, and make sure you app deploys with that version. It is a simple process, you just need to install the gem:
gem install kamal --version 1.9.0
And run kamal deploy
to deploy the app.
I wrote an article about deploying a Rails app with Kamal before and we will use the same deploy.yml
file as a starting point:
# Name of your application. Used to uniquely configure containers.
service: my_awesome_app
# Name of the container image.
image: container_registry/my_awesome_app
# Deploy to these servers.
servers:
web:
hosts:
- 111.11.111.11
labels:
traefik.http.routers.domain.rule: Host(`yourdomain.com`)
traefik.http.routers.domain.entrypoints: websecure
traefik.http.routers.domain.tls.certresolver: letsencrypt
options:
"add-host": host.docker.internal:host-gateway
cmd: "./bin/rails server"
job:
hosts:
- 111.11.111.11
options:
"add-host": host.docker.internal:host-gateway
cmd: "bundle exec sidekiq -C config/sidekiq.yml -v"
# Credentials for your image host.
registry:
# Specify the registry server, if you're not using Docker Hub
server: registry.digitalocean.com
username:
- KAMAL_REGISTRY_PASSWORD
# Always use an access token rather than real password when possible.
password:
- KAMAL_REGISTRY_PASSWORD
# Inject ENV variables into containers (secrets come from .env).
env:
clear:
REDIS_URL: "redis://host.docker.internal:36379/0"
secret:
- RAILS_MASTER_KEY
- DATABASE_PASSWORD
- SMTP_USER
- SMTP_PASSWORD
- DO_BUCKET_KEY
- DO_BUCKET_SECRET
- DO_BUCKET
- SIDEKIQ_USERNAME
- SIDEKIQ_PASSWORD
builder:
multiarch: false
# Use accessory services (secrets come from .env).
accessories:
redis:
image: redis:latest
roles:
- web
port: "36379:6379"
volumes:
- /var/lib/redis:/data
# Configure custom arguments for Traefik
traefik:
# host_port: 8080
options:
publish:
- "443:443"
volume:
- "/letsencrypt/acme.json:/letsencrypt/acme.json"
args:
entryPoints.web.address: ":80"
entryPoints.websecure.address: ":443"
entryPoints.web.http.redirections.entryPoint.to: websecure
entryPoints.web.http.redirections.entryPoint.scheme: https
entryPoints.web.http.redirections.entrypoint.permanent: true
entrypoints.websecure.http.tls: true
entrypoints.websecure.http.tls.domains[0].main: "yourdomain.com"
certificatesResolvers.letsencrypt.acme.email: "youremail@domain.com"
certificatesResolvers.letsencrypt.acme.storage: "/letsencrypt/acme.json"
certificatesResolvers.letsencrypt.acme.httpchallenge: true
certificatesResolvers.letsencrypt.acme.httpchallenge.entrypoint: web
The above config will deploy a Rails app to a VPS, get an SSL certificate with Traefik, use a managed database, run Redis as an accessory and a worker to process jobs.
Let’s upgrade this to Kamal 2. The first step is to move the secrets to the new
location in .kamal/secrets
. If you don’t use the secrets from your .env
file
anywhere else, you can just move the file to the new location:
mkdir .kamal && mv .env .kamal/secrets
If you use the .env
file somewhere else, you can just reference the
environment variables in the new secrets file, or use command substitution to
get the values:
# .kamal/secrets
KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
RAILS_MASTER_KEY=$(cat config/master.key)
To keep this example simple, we just copy the .env
file to .kamal/secrets
.
To make sure you don't commit secrets to your git repository accidentally, add `.kamal/secrets` to your `.gitignore` file!
Once the secrets are sorted, we can start to change the configuration file. First thing, we can drop the Traefik related config:
# Name of your application. Used to uniquely configure containers.
service: my_awesome_app
# Name of the container image.
image: container_registry/my_awesome_app
# Deploy to these servers.
servers:
web:
hosts:
- 111.11.111.11
options:
"add-host": host.docker.internal:host-gateway
cmd: "./bin/rails server"
job:
hosts:
- 111.11.111.11
options:
"add-host": host.docker.internal:host-gateway
cmd: "bundle exec sidekiq -C config/sidekiq.yml -v"
# Credentials for your image host.
registry:
# Specify the registry server, if you're not using Docker Hub
server: registry.digitalocean.com
username:
- KAMAL_REGISTRY_PASSWORD
# Always use an access token rather than real password when possible.
password:
- KAMAL_REGISTRY_PASSWORD
# Inject ENV variables into containers (secrets come from .env).
env:
clear:
REDIS_URL: "redis://host.docker.internal:36379/0"
secret:
- RAILS_MASTER_KEY
- DATABASE_PASSWORD
- SMTP_USER
- SMTP_PASSWORD
- DO_BUCKET_KEY
- DO_BUCKET_SECRET
- DO_BUCKET
- SIDEKIQ_USERNAME
- SIDEKIQ_PASSWORD
builder:
multiarch: false
# Use accessory services (secrets come from .env).
accessories:
redis:
image: redis:latest
roles:
- web
port: "36379:6379"
volumes:
- /var/lib/redis:/data
The builder configuration changed, so we need to adapt that to the new structure:
...
builder:
arch: amd64
...
The next part we need to configure is the proxy. We will tell it to use SSL, the hostname of the application, the port the app is running on and the healthcheck path, interval and timeout:
proxy:
ssl: true
host: yourdomain.com
app_port: 3000
healthcheck:
path: /up
interval: 3
timeout: 30
As I mentioned earlier the networking also changed, so the final change is to cleanup that part of the config and we will end up with the following config file:
# Name of your application. Used to uniquely configure containers.
service: my_awesome_app
# Name of the container image.
image: container_registry/my_awesome_app
# Deploy to these servers.
servers:
web:
hosts:
- 111.11.111.11
cmd: "./bin/rails server"
job:
hosts:
- 111.11.111.11
cmd: "bundle exec sidekiq -C config/sidekiq.yml -v"
# Credentials for your image host.
registry:
# Specify the registry server, if you're not using Docker Hub
server: registry.digitalocean.com
username:
- KAMAL_REGISTRY_PASSWORD
# Always use an access token rather than real password when possible.
password:
- KAMAL_REGISTRY_PASSWORD
# Inject ENV variables into containers (secrets come from .env).
env:
clear:
REDIS_URL: "redis://my_awesome_app-redis:6379/0"
secret:
- RAILS_MASTER_KEY
- DATABASE_PASSWORD
- SMTP_USER
- SMTP_PASSWORD
- DO_BUCKET_KEY
- DO_BUCKET_SECRET
- DO_BUCKET
- SIDEKIQ_USERNAME
- SIDEKIQ_PASSWORD
builder:
arch: amd64
# Use accessory services (secrets come from .env).
accessories:
redis:
image: redis:latest
roles:
- web
volumes:
- /var/lib/redis:/data
To verify the config file, you can run the kamal config
command and if that
doesn’t give you any errors, you can run the kamal upgrade
command and if
that’s successful, you can run kamal accessory redis reboot
to restart the
Redis accessory.
Now you just need to make sure everything in your app works as expected and you can give yourself a high five!
Or follow me on Twitter
I run an indie startup providing vulnerability scanning for your Ruby on Rails app.
It is free to use at the moment, and I am grateful for any feedback about it.If you would like to give it a spin, you can do it here: Vulnerability Scanning for your Ruby on Rails app!