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:
image
should be{username}/{project_name}
, this will be like the repo of your docker imageservers/web
is the IP of the server to deploy to, same one you used for the DNS stepproxy/host
is the domain name of the server to deploy that you set up earlierregistry/username
is your dockerhub username, dockerhub is just like a github for docker images (more on this later)proxy/ssl: true
enables 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
cat
or1password
- 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:packages
andread:packages
permissions - 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 OK
toGET /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.