From Heroku to Kamal: Taking Control of My Rails Deployment
Software Engineer
Software Engineer
Ruby on Rails
Ruby on Rails
2026-10-26 21:46:17 +0700
·
5 mins read
I used to deploy my Rails application to Heroku. It was convenient: push the code, wait for the build, and let the platform handle the infrastructure. But eventually, I wanted more control over my server and a more cost-effective way to host my personal projects.
Before Kamal, I had already tried running my application with Docker Compose on a VPS. It worked, but I still had to think about SSL certificates, reverse proxy configuration, deployment scripts, and how to replace the running container without interrupting requests.
I didn't want to trade Heroku's convenience for a collection of scripts that I had to maintain myself. That's when I found Kamal. It is shipped with Rails 8 as the default deployment tool.
1. Why Kamal?
Kamal lets me deploy containerized applications directly to my own servers over SSH. It automates much of the deployment process without requiring me to adopt a fully managed hosting platform.
My setup uses an Azure VM running Ubuntu, Docker to package my Rails application, and GitHub Container Registry (GHCR) to store application images.
The deployment configuration is surprisingly small:
This is a simplified excerpt from my setup. The important part is that I declare the application, server, domain, and deployment settings instead of writing my own orchestration scripts.
On first deployment, you just need to prepare an empty VPS and run:
// language: bash
bin/kamal setup
This will install Docker (if it isn't already installed), build the image, push it to the container registry, pull it on the server, boot accessories like Redis, and start the app.
After making changes, deploying a new version becomes:
// language: bash
bin/kamal deploy
Kamal handles the deployment workflow, including pulling the image, starting the new container, checking its health, and coordinating traffic switching. See the official deployment documentation.
2. Useful Kamal Commands
Deploying the application is only one part of the workflow. I also need a way to inspect logs, open a Rails console, and recover from a bad deployment. Here are some commands I use regularly.
Deploy and rollback
// language: bash
# Deploy the latest version
bin/kamal deploy
# Roll back to a previous version
bin/kamal rollback
If a deployment introduces a problem, rollback helps restore a previous application version. However, rolling back application code doesn't automatically reverse database migrations, so schema changes must remain compatible with the previous version.
// language: bash
# Open an interactive Rails console
bin/kamal app exec --interactive --reuse "bin/rails console"
# Run a Rails task
bin/kamal app exec "bin/rails db:migrate"
# Run a one-off Rails runner command
bin/kamal app exec "bin/rails runner 'puts Rails.env'"
These commands are useful because I don't need to SSH into the server and manually locate the correct application container. Kamal executes the command in the application's container environment.
When a deployment fails, I usually start with the application logs and container status. If the application is running but requests aren't reaching it, checking the proxy is a useful next step.
With Docker Compose, I would have needed to configure a reverse proxy and arrange certificate issuance and renewal myself, typically using Nginx and Certbot or another solution. Kamal Proxy simplifies this considerably. By enabling SSL and specifying my domain, it can automatically obtain and renew Let's Encrypt certificates.
After configuring DNS and opening the required ports, I don't need to maintain a separate certificate-renewal setup. This is one of the reasons Kamal feels closer to a managed platform than a collection of Docker commands.
One detail worth understanding is that Kamal Proxy and Thruster serve different purposes.
Kamal Proxy sits in front of the application containers. It handles incoming traffic, HTTPS when configured, health-checked traffic switching, and zero-downtime deployments.
Thruster runs inside the application container in front of Puma. It provides HTTP/2 support, asset caching, compression, and efficient static-file serving.
With a basic Docker Compose deployment, I might stop the old container and start the new one. That can create a gap during which requests fail.
Kamal takes a different approach: start the new application container, wait for its health check to succeed, switch traffic, and then remove the old container when it is safe to do so.
I don't have to build this deployment mechanism myself.
Of course, zero-downtime deployment doesn't automatically make every application change safe. Database migrations must remain compatible with the old application version while both versions may be involved in the rollout.
6. Adding CI/CD with GitHub Actions
I also wanted to avoid building production images on my MacBook every time I deployed. My development machine uses Apple Silicon, while the Azure VM uses AMD64, so image architecture matters.
My intended CI/CD workflow is:
CI/CD with Kamal - Generated by ChatGPT
GitHub Actions can run the tests, build the production image on a Linux runner, publish it to GHCR, and run the Kamal deployment from that same environment. This avoids depending on my local machine for production builds. This gives me a repeatable release process: test first, build the artifact, publish it, and deploy it.
7. What I learned from the migration
The migration wasn't entirely painless. I had to deal with PostgreSQL data migration, Redis configuration, SSH authentication, container resource limits, and architecture-specific dependencies.
Kamal doesn't eliminate infrastructure responsibilities. I still need to manage server security, backups, monitoring, and database reliability.
But compared with maintaining Docker Compose deployment scripts, I get a much more integrated deployment workflow.
Heroku: Less infrastructure to manage, but less direct control over the underlying environment.
Docker Compose: Flexible and straightforward for running containers, but more deployment and proxy configuration is left to me.
Kamal: More control over my server, with built-in deployment orchestration, health checks, traffic switching, and convenient SSL configuration.
For a small Rails application, this is an appealing middle ground between a fully managed platform and manually managing every container.
Conclusion
What I like most about Kamal is that I own the infrastructure without having to reinvent the deployment process. Docker packages my application. GitHub Actions tests and builds it. GHCR stores the image. Kamal coordinates deployments, Kamal Proxy handles traffic and HTTPS, and Thruster helps serve the Rails application efficiently.
I still have operational responsibilities, but I spend less time maintaining deployment scripts and more time building my application.
If you're considering moving a Rails application from Heroku or Docker Compose to your own VPS, start with the official Kamal documentation. It's a practical way to get many of the conveniences of a managed deployment platform while keeping control of your infrastructure.