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.yml file; 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:

  1. Kamal builds and pushes the new image
  2. The new container starts alongside the running one
  3. Kamal Proxy waits for the new container’s health check to pass
  4. Traffic switches to the new container
  5. 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.