TL;DR:
- Kamal 2 deploys any Docker container to any Linux server with zero-downtime rolling deployments using Kamal Proxy — no Kubernetes, no managed platform required
- Configuration is a single
config/deploy.ymlfile; the tool handles SSH, image push, container lifecycle, and rollbacks - Well-suited to teams leaving Heroku, Fly.io, or Render who want predictable infrastructure costs on their own VPS or bare metal
Managed platforms like Heroku, Fly.io, and Render made deployment easy but expensive at scale. A Rails app that costs $30/month on a DigitalOcean VPS can cost $500/month on a managed platform with the same resource profile. Kubernetes solves the problem at the other end of the complexity spectrum — robust but overkill for most applications.
Kamal, created by 37signals (the team behind Basecamp and HEY), fills the middle ground: SSH-based deployment of Docker containers to any Linux host, with zero-downtime deployments, built-in proxy, and no permanent daemon or control plane required on your servers.
What Changed in Kamal 2
Kamal 2, released in late 2024, replaced the Traefik dependency with Kamal Proxy — a purpose-built reverse proxy designed specifically for Kamal’s deployment model. The key improvements over Kamal 1:
- No Traefik configuration complexity — Kamal Proxy is configured entirely through Kamal’s deploy.yml
- Faster zero-downtime handoffs — Kamal Proxy handles health checks and traffic switching natively
- Better multi-server support — multiple roles (web, workers, cron) with per-role configuration
- Accessory improvements — databases and cache services are properly isolated from app deployments
A Minimal Setup
Install Kamal on your development machine (it doesn’t install anything permanent on servers):
gem install kamal
kamal init
This generates config/deploy.yml. A minimal configuration:
service: myapp
image: your-registry/myapp
servers:
web:
- 203.0.113.10
- 203.0.113.11
workers:
hosts:
- 203.0.113.12
cmd: bundle exec sidekiq
proxy:
ssl: true
host: myapp.example.com
healthcheck:
path: /up
registry:
server: ghcr.io
username: your-github-username
password:
- KAMAL_REGISTRY_PASSWORD
env:
secret:
- RAILS_MASTER_KEY
- DATABASE_URL
accessories:
db:
image: postgres:16
host: 203.0.113.10
port: 5432
env:
secret:
- POSTGRES_PASSWORD
volumes:
- /var/lib/postgresql/data:/var/lib/postgresql/data
cache:
image: redis:7
host: 203.0.113.10
port: 6379
First deployment:
kamal setup # Installs Docker on servers, boots accessories, deploys app
Subsequent deployments:
kamal deploy # Builds image, pushes to registry, rolls out with zero downtime
How Zero-Downtime Works
Kamal Proxy runs on each server and manages the traffic handoff. During a deployment:
- Kamal builds and pushes the new image
- The new container starts alongside the running one
- Kamal Proxy waits for the new container’s health check to pass
- Traffic switches to the new container
- The old container stops after its drain period
The health check endpoint (/up by default in Rails) is what gates the cutover. If the new container doesn’t pass health checks within the configured timeout, the deployment fails and the old container keeps serving traffic. No manual intervention needed.
Rollback to the previously deployed image is one command:
kamal rollback
Secrets Management
Kamal never stores secrets in deploy.yml. Secrets are referenced by name and pulled from your shell environment or a .env file at deploy time:
# .env (never committed to git)
KAMAL_REGISTRY_PASSWORD=ghp_xxxx
DATABASE_URL=postgres://user:pass@host/db
RAILS_MASTER_KEY=abc123
On the server, secrets are injected into containers as environment variables — they don’t exist in the filesystem or image. Kamal also supports 1Password integration for teams managing secrets centrally.
Multi-Server and Multi-Role Deployments
Larger applications can separate concerns across server roles:
servers:
web:
- 203.0.113.10
- 203.0.113.11
job:
hosts:
- 203.0.113.12
cmd: bundle exec solid_queue
cron:
hosts:
- 203.0.113.13
cmd: bundle exec whenever --update-crontab
Each role deploys the same image but runs a different command. You can deploy individual roles independently:
kamal deploy --roles web # Deploy only web servers
kamal deploy --roles job # Deploy only job workers
What Kamal Doesn’t Do
Being clear about limitations helps match Kamal to the right situations:
- No auto-scaling — you define your servers explicitly; horizontal scaling means adding IPs and redeploying
- No service discovery — internal service-to-service communication needs explicit configuration
- No built-in load balancer — Kamal Proxy handles HTTP/HTTPS ingress, but external load balancers are your responsibility for multi-region setups
- Ruby required on the deploy machine — Kamal is a Ruby gem; the servers themselves only need Docker
For teams running 2–50 servers in a single region with a well-understood traffic profile, these aren’t limitations in practice. For teams that need auto-scaling to zero, multi-region active-active deployments, or service mesh features, Kubernetes or a managed platform will still be the better choice.
Where Kamal Makes Economic Sense
The cost argument for Kamal is most compelling when:
- You’re on Heroku Professional or higher (typically $250+/month for a medium app)
- You’re on Fly.io or Render with predictable traffic and find yourself paying for idle capacity
- You have engineering time to manage servers but it’s absorbed into other ops work anyway
A Hetzner CX31 (4 vCPU, 8GB RAM) costs €11/month. A DigitalOcean Droplet with equivalent specs is $48/month. Either runs a small-to-medium Rails or Django app with room to spare. The break-even against managed platform pricing is typically within 2–3 months.
Kamal is now the default deployment tool for new Rails 8 applications — kamal init is included in the rails new generator. That mainstream adoption means the ecosystem around it (CI/CD examples, community support, documentation) has matured quickly. For teams already in the Rails world, it’s the path of least resistance to get off the managed platform treadmill.