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.
What the runner needs
| Secret | Used for |
|---|---|
SSH_PRIVATE_KEY |
Reaching the server. Give it its own key, not your personal one. |
KAMAL_REGISTRY_PASSWORD |
Pushing and pulling the image. |
RAILS_MASTER_KEY |
Decrypting credentials at boot on the server. |
Those names are read by .kamal/secrets, which is the only file that maps environment variables to
what Kamal sends. It is not committed, so whatever CI exports has to match what that file expects.
The order that matters
Test first, deploy second, in the same workflow with a needs: dependency. A separate workflow that
triggers on push deploys in parallel with the test run, which means a broken build can reach
production while its own test job is still red.
bin/check
bin/kamal deploy
bin/check is the whole gate: RSpec, RuboCop, Brakeman, bundler-audit, the importmap audit and a
secret scan. If it passes locally it passes in CI, because it is the same script.
Build on the runner or on the server
Kamal builds the image wherever it runs. On a GitHub-hosted runner that means a cold Docker layer cache on every run, which is slow. Two ways out: enable a registry cache so layers are reused between runs, or use a self-hosted runner on the deploy server itself, where the cache is already warm and the push never leaves the machine.
Do not deploy every branch
Guard the job on the default branch. A workflow that deploys on any push turns a draft pull request into a production release:
if: github.ref == 'refs/heads/main'
Check it actually shipped
curl -s https://yourdomain.com/up -o /dev/null -w '%{http_code}\n'
A green workflow means the commands exited zero. It does not mean the new container is serving. The health check in the deploy covers that, but a request from outside confirms DNS and the proxy too.