Answers · Deploying

How do I deploy a Rails 8 app without Heroku?

Use Kamal, which ships with Rails 8. You give it a server over SSH and a container registry in config/deploy.yml, run kamal setup once, and kamal deploy after that. It builds an image, pushes it, and swaps containers with no downtime. A $5 VPS replaces the PaaS.

What Kamal actually needs

Three things, and nothing else: a server you can SSH into as root or a sudoer, somewhere to push a container image (Docker Hub, GitHub Container Registry, anything), and Docker installed on that server. Kamal installs Docker for you on first run if it is missing.

The whole deployment target is one file, config/deploy.yml:

bin/kamal setup     # first time only
bin/kamal deploy    # every time after that

setup is the one that does the heavy lifting. deploy builds the image, pushes it to the registry, pulls it on the server, boots the new container, waits for /up to answer, and only then moves traffic. If the health check never passes, the old container keeps serving and the deploy fails loudly instead of taking the site down.

Secrets do not live in the config file

config/deploy.yml is committed. Secrets are read from .kamal/secrets, which is not. In One Shot the only one that must exist before production will boot is RAILS_MASTER_KEY:

kamal secrets print   # names only, no values

Everything else the app reads is optional and falls back to a local fake. See Bring Your Own Keys for the full list.

What you give up

Kamal is not a PaaS, and the gap is real. There is no managed database, no automatic TLS unless you let Kamal's proxy handle it, no dashboard, and no one to page at 3am. You own the box. For a single app on SQLite, which is what One Shot ships, that trade is usually worth it: the app and its database live on one disk, backups are a file copy, and the whole bill is the VPS.

It stops being worth it when you need more than one machine writing to the database. See Production Database and Storage for where that line sits.

Check it worked

curl -sI https://yourdomain.com/up | head -1
bin/kamal app logs --lines 50

A 200 from /up means Rails booted and the proxy is routing. If the deploy hangs on the health check instead, the container is starting and dying; kamal app logs shows why, and the answer is almost always a missing RAILS_MASTER_KEY.

Related questions

How do I deploy a Rails app from GitHub Actions?

Run kamal deploy as a workflow step. Kamal needs three things in the runner: an SSH key that can reach the server, registry credentials, and RAILS_MASTER_KEY. All three go in as repository secrets and are read by .kamal/secrets. Gate the deploy job behind the test job so a red build never ships.

How do I fix no space left on device on a deploy server?

Old container images are the cause, not your app. Every deploy pushes a new image and the server keeps the old ones. Run docker image prune -a -f to reclaim the space, then check with df -h. On a 25GB VPS this usually returns several gigabytes, and it is worth doing before you are locked out rather than after.

How do I open a Rails console on a production server?

Run kamal app exec --interactive --reuse 'bin/rails console'. That attaches to the running container rather than booting a new one, so you see live data. One Shot ships no admin UI on purpose, so the console is the supported way to inspect and fix records, and everything you touch must go through an Account.