Why choose Kamal over Kubernetes to deploy web applications on a VPS?
Kubernetes solves large-scale coordination problems — service discovery, horizontal autoscaling, multi-team namespaces — that simply do not exist on a single VPS or a small cluster. Its control plane alone consumes between 2 and 4 GB of RAM before a single application has launched. Kamal targets a different space: it takes your Dockerfile, builds the image locally or on the server, pushes it to a registry, runs it on your servers over SSH and switches traffic without downtime using kamal-proxy. All configuration lives in a single config/deploy.yml file versioned alongside your code. No control plane to maintain, no API certificates to renew, no YAML spread across ten resource types. You keep the operational simplicity of a VPS — direct SSH access, readable docker ps, logs in a file — while gaining a professional deployment workflow with instant rollback, health checks and automatic HTTPS.
What Kamal v2 gives you
- Zero-downtime deployments — kamal-proxy waits for health checks to pass on the new container before switching traffic, and drains in-flight requests on the old one
- Single command —
kamal deploychains build, registry push, SSH deployment and verification without manual steps - Automatic Let's Encrypt HTTPS — kamal-proxy handles TLS certificate renewal; no separate Certbot setup required
- Instant rollback —
kamal rollback [VERSION]repoints the proxy to an image already on the server within seconds - Structured secret management —
.kamal/secretssupports reading from 1Password, Bitwarden or environment variables without storing plain-text values in the repository - Native multi-server and multi-role support — web servers, Sidekiq workers, Postgres and Redis accessories described in one file, deployed in parallel
- Multiple apps on a single VPS — since Kamal 2, several applications share one kamal-proxy without configuration conflicts
- Any stack — Rails, Django, Node.js, Go, PHP: Kamal only requires a Dockerfile and a container registry
Prerequisites before deploying with Kamal
On the VPS side, Kamal requires Ubuntu 22.04 or 24.04 (or any Linux distribution with Docker 20.10+), SSH key access (no password), and at minimum 2 vCPU and 2 GB RAM for a standard web application with its database. Kamal can install Docker itself during the first kamal setup, but the SSH account must have sudo rights. Ports 80 and 443 open inbound and a DNS A record pointing to the VPS IP are required before enabling SSL. On your local machine, two options exist: install the Ruby gem (gem install kamal, requires Ruby 3.1+) or use the official Docker image. The current version is 2.12.0 (June 2026). You also need a container registry — Docker Hub, GitHub Container Registry or a private registry — and its credentials.
Kamal v1 vs Kamal v2: what changed
Kamal 2, released in September 2024 (bundled by default in Rails 8, released November 2024), replaces Traefik with kamal-proxy, an in-house reverse proxy developed by 37signals. The change is structural: Traefik is declarative (you submit a configuration and it converges), while Kamal is imperative. Version 1 had to poll Traefik's API repeatedly to determine whether a deployment had succeeded — a source of race conditions. With kamal-proxy, commands are direct and synchronous. In terms of configuration, the traefik: block disappears from deploy.yml and is replaced by proxy: with keys host, ssl and app_port. Secrets move from .env to .kamal/secrets, a shell-evaluated file that can call external CLI tools (op read, bw get). The kamal upgrade command detects a v1 configuration and offers an automated migration plan. Kamal 2 also introduces multiple applications on a single proxy, maintenance mode (kamal app pause) and experimental canary deployment support.
Installing and running your first deployment with Kamal v2
Install Kamal on your local machine
Run
gem install kamal(Ruby 3.1+ required) or, if you prefer to avoid Ruby, use the Docker alias:alias kamal='docker run -it --rm -v "${PWD}:/workdir" -v "${SSH_AUTH_SOCK}:/ssh-agent" -e SSH_AUTH_SOCK=/ssh-agent -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/basecamp/kamal:latest'. Verify withkamal version— current version is 2.12.0.Initialize configuration in your project
In the project root directory, run
kamal init. Kamal generatesconfig/deploy.ymland.kamal/secrets. Fill indeploy.yml: service name (service: myapp), image (image: youruser/myapp), server list (servers: web: - 203.0.113.10), and registry (registry: server: ghcr.ioor omitted for Docker Hub).Configure secrets and environment variables
Add
.kamal/secretsto your.gitignore. In this file, declare secrets as shell variables:KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD(read from local environment) orDB_PASSWORD=$(op read op://vault/myapp/db-password)(from 1Password). Inconfig/deploy.yml, reference them underenv.secret:for encrypted values andenv.clear:for non-sensitive variables likeRAILS_ENV=production.Configure the proxy and SSL
In
config/deploy.yml, add the proxy section:proxy: host: app.your-domain.com ssl: true app_port: 3000. Kamal-proxy will automatically obtain a Let's Encrypt certificate via ACME HTTP-01. Make sure the DNS A record already points to the VPS IP before this step — the ACME challenge fails otherwise.Provision the server (one-time step)
Run
kamal setup. Kamal connects via SSH, installs Docker if absent, authenticates the registry, starts kamal-proxy and deploys the first version of the application. This command is the only bootstrapping step: it configures the server from scratch. On an already-equipped server,kamal setupdetects existing Docker and skips reinstallation.Deploy subsequent updates
For each update, run
kamal deploy. Kamal builds the image (or retrieves it from cache if unchanged), pushes it to the registry, starts a new container alongside the old one, waits for the health check to pass (defaultGET /up), switches kamal-proxy to the new container, drains existing connections and stops the old one. The operation is visible in real time in your terminal.Check status and view logs
After deployment:
kamal app detailsshows the running version on each server,kamal app logs -fstreams logs in real time,kamal app exec 'bin/rails console'opens a console inside the production container. To check proxy status:kamal proxy details.Manage accessories (database, cache)
Declare Postgres under the
accessories:key indeploy.yml: image, port, volumes, variables.kamal accessory boot dbstarts the service on the right server. Accessories do not go through kamal-proxy — they are connected directly by the application via environment variables. This keeps the entire stack described in a single versioned file.
Multi-server configuration and parallel deployments
Kamal deploys to all servers in a role in parallel by default. For a web cluster, list their IPs under servers: web:. For rolling deployments, add boot: limit: 2 wait: 10 — Kamal will deploy to 2 servers at a time with 10 seconds between batches. Roles let you differentiate servers: under servers: declare web: (HTTP servers) and workers: with a specific command like bundle exec sidekiq. Each role can have its own servers, secrets and variables. A single kamal-proxy runs on the primary server (first in the web list) — other servers only run the application container. For multiple applications on a single VPS, each has its own service: and host: in its deploy.yml; kamal-proxy distinguishes them by hostname and routes requests without additional configuration.
Environment variables and secrets — best practices
The separation between clear variables and secrets is explicit in Kamal 2. In deploy.yml, non-sensitive variables go under env: clear: (they appear in logs and container inspection), and secrets under env: secret: — their value is read from .kamal/secrets at deploy time and injected into the container without ever appearing in plain text in a visible shell command. Each role (web, workers, accessories) must explicitly list the secrets it uses; a secret in .kamal/secrets is not automatically propagated to all containers. The kamal secrets print command displays resolved values to verify that reading from a password manager works correctly before deploying. For multiple environments (staging, production), .kamal/secrets.staging and .kamal/secrets.production coexist; .kamal/secrets-common holds shared values.
If a deployment introduces a regression, do not wait for the next build cycle: run kamal rollback [VERSION] to repoint kamal-proxy to a previous image already on the VPS within seconds. List available versions with kamal app images. Rollback rebuilds nothing — it repoints the proxy in seconds. Additionally, kamal app exec 'command' lets you run migrations or open a console in the active container without manually opening an SSH session.
Automatic HTTPS with kamal-proxy and Let's Encrypt
Kamal-proxy manages the complete TLS lifecycle: it listens on port 80, responds to Let's Encrypt ACME HTTP-01 challenges, obtains the certificate, stores it in a Docker volume on the server and renews it automatically before expiration. The only prerequisite is that the hostname declared in proxy: host: resolves to the VPS IP at the time of the first deployment — the ACME challenge is synchronous and blocks startup if DNS has not propagated. On multi-app servers each with their own domain, each application has its own host:; kamal-proxy routes by Server Name Indication (SNI) and manages a separate certificate per domain. For domains that do not require TLS (internal environments, staging without a public domain), simply omit ssl: true from the proxy: section — the proxy then responds in HTTP on port 80.
Troubleshooting — common errors when deploying with Kamal
SSH connection timeout: verify the server IP is reachable (ssh -i your_key [email protected]) and that the firewall allows port 22. The SSH agent must be running (eval $(ssh-agent) && ssh-add). Target failed to become healthy: the default health check hits GET /up — if your application does not respond on that path, declare proxy: healthcheck: path: /health in deploy.yml. Also verify app_port: matches the port your container actually listens on. Registry authentication error: run kamal registry login then retry; in CI, verify the KAMAL_REGISTRY_PASSWORD variable is exported before kamal deploy. Port conflict on the server: if an external process occupies port 80 or 443, kamal-proxy cannot start. Identify it with ss -tlnp | grep -E '80|443' and stop it. Kamal setup fails on Docker installation: the SSH account must have sudo rights. If automatic installation is blocked by system policy, install Docker manually and rerun kamal setup — it detects existing Docker and moves to the next step.
Integrating Kamal into a GitHub Actions CI/CD pipeline
A typical GitHub Actions workflow for Kamal 2 has two jobs: a test job (run on every push) and a conditional deployment job (triggered on push to main or release publication). In the deployment job, install Ruby and the Kamal gem, then run kamal deploy with secrets injected from GitHub Secrets. Key elements of .github/workflows/deploy.yml: the deploy job depends on the test job (needs: test), configures environment variables (KAMAL_REGISTRY_PASSWORD: ${{ secrets.KAMAL_REGISTRY_PASSWORD }}), installs Kamal with a pinned version (gem install kamal -v 2.12.0), then runs kamal deploy. The SSH agent is configured via webfactory/[email protected] with the private key stored in GitHub Secrets. Each deployment is traceable in the Actions tab, with full logs and the option to trigger manually via workflow_dispatch.
Kamal v1 vs Kamal v2 — key differences at a glance
Scroll the table
| Kamal v1 | Kamal v2 | |
|---|---|---|
| Proxy | Traefik (declarative, API polling) | kamal-proxy (imperative, direct commands) |
| Secrets | .env at project root | .kamal/secrets (shell-evaluated, password manager support) |
| SSL | Managed by Traefik via ACME | Managed by kamal-proxy (Let's Encrypt built-in) |
| Multiple apps | Not natively supported | Native: multiple apps per server, single proxy |
| Upgrade | — | `kamal upgrade` detects and migrates v1 config |
| Proxy config in deploy.yml | `traefik:` block | `proxy:` block with `host`, `ssl`, `app_port` |
Kamal on a ServOrbit VPS — from command to live domain
A ServOrbit Cloud VPS running Ubuntu 24.04 meets all Kamal prerequisites out of the box: SSH key access, 1 Gbit/s network and a dedicated IP. The typical workflow: create a DNS A record pointing app.your-domain.com to the VPS IP, then run kamal setup from your local machine — Kamal installs Docker, starts kamal-proxy, obtains the Let's Encrypt certificate and deploys your application in one pass. From that point, every kamal deploy from your terminal or CI pipeline applies the update with zero downtime. To host multiple projects on the same VPS, each with its own domain and certificate, add an application in a second deploy.yml pointing to the same server — kamal-proxy routes by hostname without reconfiguration. Billing remains that of the VPS, with no additional deployment overhead.