Deploy Rails with Kamal on a VPS
Two of my apps, Abide and Situation, run on one Hetzner box that costs $11.49 a month. No platform console. No per-app bill. One command ships a release. The tool is Kamal, from the people who make Rails, and this episode is the whole path from empty server to live app.
Five minutes if you would rather watch it. The commands are below.
What you need
Four things, and the expensive one is a domain.
- A virtual server. Ubuntu, a public IP, nothing else installed. Kamal brings its own luggage.
- A domain, with one A record pointing at the server.
- Docker on the box. Kamal installs it on the first run, so that one is free in every sense.
- A Rails app with a Dockerfile. Rails 7.1 and later generate one. It is already sitting there.
The config
kamal init writes config/deploy.yml. For one server, the lines that matter fit on a napkin. This is Abide's, minus the comments:
service: abide
image: abide
servers:
web:
- 203.0.113.10
proxy:
ssl: true
host: abide.dividendsolo.com
registry:
server: localhost:5555
env:
secret:
- RAILS_MASTER_KEY
Service and image name the app. Servers is the box. Proxy is the SSL story, and it is three lines. The registry is a local one on the box itself, because I am not paying Docker Hub to hold images of my own Bible reader.
Secrets
Rails keeps production secrets behind config/master.key. Kamal cannot see that file and it is not going to guess. The env.secret list above says which names go to the container, and the values come from .kamal/secrets, which Kamal reads at deploy time:
RAILS_MASTER_KEY=$(cat config/master.key)
This is where first deploys die. One missing key, the container boots, Rails crashes, and the log says nothing useful. Put the key in before the first deploy, not after the first crash.
The first deploy
kamal setup
It installs Docker on the empty server, builds the image, pushes it, boots the app, and starts kamal-proxy. When it fails the first time, and it usually does, it is one of two things: the secret is not there, or port 443 is not open and Let's Encrypt cannot reach the box. Fix the one it names and run it again.
SSL
proxy.ssl: true plus a host. That is the entire certificate story. kamal-proxy talks to Let's Encrypt, issues the certificate, and renews it, which is more than I can say for most things. Two prerequisites: the host resolves to the server, and 443 is open. Get those right once and you never think about certificates again.
Day two
After setup, almost every day on the box is three commands.
kamal deploy # ship the new release
kamal app logs -f # follow the app when something misbehaves
kamal rollback <version> # back one release, the fastest unbreak button you can own
That is the operating rhythm. You, a terminal, and a server you rent by the month.
The money
The video says five dollars, and a five dollar box will run a Rails app. Mine is a Hetzner CPX21, three cores and four gigabytes, at $11.49 a month, because it also runs a budget app, an agent, and a Postgres nobody else pays for. Two Rails apps, SSL for both, one bill. The platform I left charged per app. Funny how that works.
If you are still paying platform prices for a Rails app that would be happy on a small box, this is the way off.