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.