Kubernetes solves real problems, but most side projects and small production apps do not have those problems. What they actually need is a way to build a Docker image, push it to a registry, and run it on one or two servers without downtime and without babysitting a control plane. That is exactly the gap Kamal 2 fills.
Kamal is a deployment tool from Basecamp that drives your existing servers over SSH, using plain Docker underneath. Version 2 replaced the old Traefik-based routing layer with a purpose built reverse proxy called kamal-proxy, which is what finally makes true zero-downtime rollouts and instant rollback practical on a single VPS. This guide walks through a full deployment: proxy, accessories, secrets, and recovering from a bad release.
Why Kamal 2 Instead Of Kubernetes
Kubernetes earns its complexity at a certain scale: many services, many teams, autoscaling across a fleet of nodes. Below that scale, a control plane, etcd, and a networking layer add operational overhead with little payoff, and most solo developers or small teams end up spending more hours learning YAML dialects than shipping features. Kamal targets the space in between a single docker run command and a full cluster, borrowing the parts of container orchestration that actually matter for a small deployment — image builds, health checks, and coordinated restarts — while leaving out the parts that only pay off at large scale.
- No control plane to patch, upgrade, or lose sleep over at 2am
- Works on any box you can SSH into — a five dollar VPS, bare metal, or a cloud VM
- Deploys are just SSH commands wrapping standard docker CLI calls, so failures are easy to reason about
- Zero-downtime rollouts and rollback come built in through kamal-proxy, not bolted on
Kamal is framework agnostic. It was built for Rails apps but works with any application that can be packaged into a Docker image and exposes an HTTP health check endpoint — Node, Next.js, Django, or a static binary all qualify.
Setting Up The Deploy Configuration
Everything Kamal does is driven by one YAML file, config/deploy.yml, plus a secrets file. The config below targets a single server, builds an image, pushes it to a registry, and configures kamal-proxy to route traffic and run health checks against the app.
# config/deploy.yml
service: myapp
image: myregistry/myapp
servers:
web:
hosts:
- 203.0.113.10
labels:
traefik.http.routers.myapp.rule: Host(`myapp.example.com`)
proxy:
ssl: true
host: myapp.example.com
app_port: 3000
healthcheck:
path: /up
interval: 1
timeout: 5
registry:
server: ghcr.io
username: myuser
password:
- KAMAL_REGISTRY_PASSWORD
env:
clear:
NODE_ENV: production
secret:
- DATABASE_URL
- REDIS_URLThe proxy block is the important part. Kamal-proxy sends a GET request to the health check path once per second, and only switches traffic to the new container once it responds with a success status. Old traffic is drained from the previous container before it is stopped, which is what makes the swap invisible to users.
What Happens During A Deploy
Running the deploy command triggers a sequence that is worth understanding, because most deploy failures happen in one of these four steps and the error messages point straight at which one failed. Reading the Kamal output line by line during your first few deploys pays off later, since it teaches you exactly which stage — build, push, pull, or health check — to inspect when something eventually goes wrong in production.
- Kamal builds the Docker image locally or on a remote builder, tags it, and pushes it to your configured registry
- Each target server pulls the new image and starts a new container alongside the currently running one
- Kamal-proxy polls the new container's health check path until it returns a healthy response, then atomically switches routing to it
- The previous container is drained of in-flight requests and stopped, and old images are pruned after a retention window
| Command | What it does | When to run it |
|---|---|---|
| kamal setup | Provisions Docker, kamal-proxy, and directories on a fresh server | Once, before the first deploy to a new host |
| kamal deploy | Builds, pushes, and rolls out the app with zero downtime | Every release |
| kamal rollback | Reverts to a previously deployed image tag | Immediately after a bad release is noticed |
| kamal accessory boot | Starts or restarts a supporting service like a database | When provisioning or updating a dependency container |
Running Postgres And Redis As Accessories
Accessories are Kamal's term for the supporting services your app depends on but that are not part of the zero-downtime rollout — a database, a cache, a queue. They are declared in the same deploy.yml file, but managed with their own lifecycle through the kamal accessory command rather than kamal deploy. This separation exists because a database restart is fundamentally different from an app restart: you cannot simply run two Postgres containers side by side and switch traffic between them the way kamal-proxy switches between app containers, since the data itself lives on a single volume.
# config/deploy.yml
accessories:
postgres:
image: postgres:16
host: 203.0.113.10
port: "127.0.0.1:5432:5432"
env:
clear:
POSTGRES_DB: myapp
secret:
- POSTGRES_PASSWORD
directories:
- postgres-data:/var/lib/postgresql/data
redis:
image: redis:7
host: 203.0.113.10
port: "127.0.0.1:6379:6379"
directories:
- redis-data:/dataAccessories do not get zero-downtime updates. Restarting Postgres or Redis with kamal accessory reboot causes a brief connection drop while the container restarts, so schedule accessory changes during low-traffic windows and make sure your app retries dropped database connections gracefully.
Managing Secrets Safely
Kamal reads secrets from a .kamal/secrets file by default, which should never be committed to git. The file supports destination-specific overrides, so a value in .kamal/secrets.production only applies when deploying to that destination, while .kamal/secrets-common applies everywhere.
- Reference secret names in deploy.yml under env.secret, and Kamal injects the actual values from the local secrets file at deploy time — the values never touch your repository
- Registry credentials, API keys, and database URLs all follow the same pattern: declare the name in config, keep the value local or pulled from a password manager
- Rotate a secret by updating the local file and running kamal env push followed by a redeploy of the affected service
Rolling Back A Bad Deploy
Every deploy leaves the previous container's image on the server for a retention window, which is what makes rollback fast — no rebuild or registry pull needed. First find the image tag you want to revert to, then roll back to it directly. It is worth practicing this command on a staging server before you actually need it in production, so the exact syntax and the container listing output are already familiar when a real incident happens and every second of downtime matters.
# see exited containers from previous deploys
kamal app containers -q
# roll back to a specific image tag
kamal rollback abc1234
# roll a proxy restart across hosts one at a time
kamal proxy reboot --rollingBecause rollback reuses an already-pulled image, it is nearly as fast as the original deploy's traffic switch. There is no build step and no registry round trip, so a bad release from ten minutes ago can be undone in seconds.
Multi-Server And Rolling Restarts
For apps running on more than one host, Kamal deploys to all servers in parallel by default, but you can opt into a rolling strategy so that only a subset of servers are updated at a time, keeping the rest serving traffic. This matters most for proxy-level maintenance, like rotating SSL certificates or bumping the proxy image itself, where you want zero visible impact even while restarting infrastructure that sits in front of your app.
Start with a single-server setup even if you plan to scale out later. Kamal's configuration format makes adding a second or third host a one-line change to the hosts list — you do not need to design for multi-server from day one.
Kamal 2 will not replace Kubernetes for a genuinely large, multi-team platform, but for the vast majority of side projects, small SaaS products, and single-team backends, it removes an entire category of operational complexity while still delivering the zero-downtime deploys and clean rollback story that used to require a control plane to get right. If you already own a VPS and a Docker image, the fastest way to feel the difference is to run kamal setup once and watch your first zero-downtime deploy complete in under a minute.

