Docs · Deployment

Deploying with Kamal

One-command Docker deploys with no PaaS, and the deploy topology.

One Shot deploys with Kamal: one command, any Docker host you control, no proprietary platform-as-a-service subscription in the loop. This site's own production deployment, shiponeshot.com, runs this exact setup on a small server it shares with another unrelated app, each fully independent with its own service name, image, volume, and port.

Why Kamal, and what that trades away

Kamal builds a Docker image, ships it to any server with Docker installed, and runs it there, handling zero-downtime restarts and basic proxying itself. Compared to a managed PaaS, you give up one-click scaling and a managed database in exchange for running on whatever server you already have (or a $5 to $20/month VPS), with no per-seat or per-request platform fee layered on top of your infrastructure cost. This fits a kit built around SQLite and Solid Queue: the whole app, including its background jobs and its database, lives inside one container, on one machine, with no separate services to provision.

First deploy

bin/kamal setup

This builds the image and boots the app for the first time. Verify it worked:

bin/kamal logs -f

Then confirm the app answers on its own, and through whatever's in front of it (see Edge and DNS Setup):

curl -sI http://127.0.0.1:<your-port>/up   # on the server itself
curl -sI https://yourdomain.com/up          # through your public edge

Every deploy after the first

bin/kamal deploy

A pre-app-boot hook removes the previous container before starting the new one, since the host port is fixed; expect a few seconds of downtime on each deploy, not a rolling zero-downtime handoff. For an app with Solid Queue running in-process, this also means jobs pause briefly during that window rather than continuing on a separate worker process.

Checking which release is live

Each image carries the commit it was built from: Kamal passes it as the GIT_SHA build arg (builder.args in config/deploy.yml), and /version returns it as plain text. After a deploy, check that it matches the commit you meant to ship:

curl -s https://yourdomain.com/version

"unknown" means the image was built without GIT_SHA. The response is never cached, so a stale edge can't report the previous release.

Useful commands

bin/kamal logs -f                              # tail production logs
bin/kamal console                              # a Rails console on the server
bin/kamal app exec "bin/rails db:migrate"      # run a one-off command

CI/CD

A GitHub Actions workflow can mirror bin/check exactly, running the same bin/* wrappers (Brakeman, bundler-audit, gitleaks, importmap audit, RuboCop, RSpec) as separate parallel jobs, so a green bin/check locally predicts a green pipeline. Because Kamal deploys need direct access to your server, the deploy job itself typically runs on a self-hosted runner with network access to that server, rather than GitHub's own hosted runners, and triggers kamal deploy automatically on a green main.

Next

Before your first deploy, work through the checklist: First Deploy Checklist.

Common questions

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.

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.

How do I roll back a bad deploy without losing data?

Run kamal rollback to boot the previous image, which is still on the server. It takes seconds because nothing is rebuilt or pulled. Your code goes back; your database does not. A deploy that ran a destructive migration cannot be undone this way, which is why additive migrations matter more than the rollback command.

How do I see production logs for a deployed Rails app?

Run kamal app logs --follow to stream, or kamal app logs --lines 200 --grep Error to search what already happened. Rails 8 logs one line per request in production, so the useful move is filtering by request id and following that one request through the job and mailer lines it produced.