Using Kamal with GitHub Container Registry (ghcr.io)
Kamal was one of the defining features of Rails 8 with the promise of âNo PaaS requiredâ, but of all the new Rails stuff I experimented with, I put this one off the longest. Deployment always seemed to be a DevOps dark art and something with high potential cost if not done properly.
I returned to Kamal recently determined to sit down with the docs and at least try it once. After a few obstacles along the way and some âgotchasâ, which Iâll explain below partly as a way to document this for myself, I found Kamal to be everything promised and also an excellent dev experience (when you finally know what youâre doing). I plan to use this for all future Rails apps as opposed to a PaaS as I would have resorted to previously.
Setup
Server and domain
- Youâll need a server to deploy to with SSH access. There are many VPS providers out there, but I used Hetzner because they seemed cheapest and easiest to set up.
- Kamal gem and deploy.yml (this should already be setup for new Rails 8 apps)
- A domain name pointed to your server, this should just be a DNS A record with the IP of your new server from above
deploy.yml Configuration
This is where I made the majority of my mistakes. First a quick overview of what each part does:
imageshould be{username}/{project_name}, this will be like the repo of your docker imageservers/webis the IP of the server to deploy to, same one you used for the DNS stepproxy/hostis the domain name of the server to deploy that you set up earlierregistry/usernameis your dockerhub username, dockerhub is just like a github for docker images (more on this later)proxy/ssl: trueenables automatic Letâs Encrypt SSL certificates, you want this to be true
.kamal/secrets Configuration
- Mostly setup out the box but more can be added
- Names have to match the names referenced in deploy.yml
- Can use external commands like
cator1password - Some examples:
KAMAL_REGISTRY_PASSWORD=$(bin/rails runner "puts Rails.application.credentials.dig(:github, :token)") # From rails credentials KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD # From environment RAILS_MASTER_KEY=$(cat config/master.key) # From file
Choosing GitHub Container Registry over Docker Hub
A note about docker image registries:
Dockerhub is the default and it does work well out of the box, however you are only allowed one private repo on the free plan which obviously is not sustainable. You can sign up for a paid plan, but to me that defied the reason for using Kamal at all. Why pay another service if the plan is to ditch Heroku, Render etc by deploying yourself anyway?
GitHub has a container registry service too, which makes sense if youâre already in that ecosystem, but this wasnât as straightforward as Dockerhub and I had a few headaches along the way to get it setup as the documentation seems lacking for non-dockerhub setup. It all works fine without issues if you follow these steps:
â ď¸ CRITICAL: Use a Classic Personal Access Token
This is the gotcha that caught me out:
- Get a PAT from Settings > Developer settings
- You MUST generate a classic token with
write:packagesandread:packagespermissions - The new fine-grained tokens will not work - youâll get authentication errors
Hereâs the config that actually works:
Run kamal setup and you should be good to go!
Git Repository â Docker Image Relationship
This is crucial to understand the Kamal workflow, and something that caught me out multiple times when my changes werenât being deployed.
How It Works
- Git is the source of truth: Kamal uses your current Git commit SHA as the Docker image tag
- Image naming: Your image becomes
{repo_owner}/{repo_name}:<git-commit-sha> - Remote requirement: Your code must be pushed to a Git remote before deploying
- Build process: Kamal builds a Docker image from your current commit
- Registry storage: The image is pushed to your configured registry (ghcr.io in our case)
- Server deployment: Kamal pulls this specific image version to your servers
Common Symptoms When You Forget to Push
- Your changes arenât visible after deployment
- The deployment succeeds but nothing has changed
- You see an old commit SHA in the deployment logs
The Correct Workflow
Zero-Downtime Deployments
Kamal deployments are zero-downtime by design.
This was one of those âIâll believe it when I see itâ features, but watching it work in real-time is quite satisfying.
The Zero-Downtime Process
- New container starts: Kamal builds and starts a new container alongside the old one
- Health check wait: Waits for the new container to respond
200 OKtoGET /up - Traffic switch: kamal-proxy redirects traffic from old â new container
- Old container cleanup: Stops and removes the previous container
- No dropped connections: Existing requests complete on the old container
Health Check Requirements
Your Rails app must respond to GET /up for this to work. Rails 8 includes this by default:
Basic Kamal Commands
Deployment Commands
Monitoring & Debugging
Server Management
Build & Registry
Troubleshooting
During my setup, I hit a few snags that might help others:
net-pop Ruby Version Error
If you encounter errors related to net-pop during kamal setup, itâs likely a Ruby version mismatch:
When Things Go Wrong
My debugging checklist when deployments fail:
Conclusion
What started as an intimidating âDevOps dark artâ has become my go-to deployment method. The journey from deployment anxiety to confidently running git push && kamal deploy has been worth it.
Yes, there were gotchas (looking at you, GitHub token types), and yes, I spent time debugging issues that seemed obvious in hindsight. But now I have:
- Zero-downtime deployments that actually work
- Complete control over my infrastructure
- No monthly PaaS bills
- A deployment process thatâs as simple as any PaaS
If youâre on the fence about trying Kamal, especially with GitHub Container Registry, Iâd encourage you to give it a shot. The initial setup investment pays off quickly, and youâll gain a much better understanding of how your application actually runs in production.
Cost is another major benefit, Iâm spending around 3 EURO per month for my current setup to run several small rails apps with sqlite databases whereas a PaaS would be charging 10-20x that.
For my next projects? Itâs Kamal all the way.