Migrating from Kamal to DeployHQ
Kamal is the deploy tool DHH built when 37signals decided that "Kubernetes is too much, but Capistrano is not enough." It is opinionated, container-first, multi-server out of the box, actively developed, and very good at what it does. If your stack is one or two Dockerised apps deployed to a fleet of bare-metal hosts, Kamal is genuinely hard to beat.
This guide is for teams who have run into one of the natural ceilings of a CLI-and-YAML deploy tool:
- You want a deploy log that is not somebody's shell history. Kamal records nothing about who deployed what, when, from which machine. The only audit trail is your version control history of
deploy.yml, and that does not capture rollbacks orkamal app execinvocations. - You want builds to run somewhere reliable.
kamal deploybuilds the Docker image on whichever machine runs the command — usually a developer's laptop or a CI runner. DeployHQ builds on a managed build server with predictable resources, dependencies cached between deploys, and the same artefact shipped to every target. - You want role-based deploy permissions. In Kamal, anyone with SSH access to the targets and a copy of
deploy.ymlcan deploy anywhere. There is no concept of "junior engineer can deploy to staging but not production" outside of file system permissions on the YAML and SSH key distribution. - Your stack has parts that do not fit Kamal's model. Static sites, FTP-only legacy hosts, S3 syncs, Heroku targets, framework-shaped Rails or Laravel apps you would rather not containerize — Kamal is one tool, narrowly scoped. DeployHQ covers the breadth.
- You are tired of
deploy.ymlrace conditions. Two engineers runningkamal deployat roughly the same time is at best confusing and at worst destructive. A central dashboard with deploy queueing avoids the question entirely.
If none of those resonate, Kamal is excellent and you should stay on it.
For a side-by-side feature comparison, see DeployHQ vs Kamal. Kamal is sometimes positioned as the modern Capistrano — if you are weighing the older script-based path, see Migrating from Capistrano to DeployHQ. For other container-first options, DeployHQ vs Dokku and DeployHQ vs Coolify cover the self-hosted-PaaS alternative.
What changes shape, and what does not
Kamal is a deploy automation tool, not a PaaS. It does not manage databases, does not run a one-click app store, does not manage your DNS. So the model shift moving to DeployHQ is smaller than it would be from Dokku or Coolify — both tools live in the same conceptual neighbourhood.
| Kamal | DeployHQ |
|---|---|
config/deploy.yml |
DeployHQ project (web-configured) |
service: my-app |
Project name |
image: org/my-app |
Different model. Either build pipeline produces the artefact and ships it via SSH (file-based deploys), or Custom Actions build/push and docker pull on the target (container deploys). |
servers.web.hosts: [...] |
Servers attached to an environment, deployed in parallel |
servers.job.hosts: [...] (worker role) |
Separate environment, separate server group, or separate project — your call |
proxy.ssl + proxy.host (Kamal Proxy or Traefik) |
You manage this. Caddy (recommended for new setups), Traefik standalone, nginx + certbot, or terminate at a load balancer. DeployHQ does not run a proxy. |
registry.username / registry.password |
Docker registry credentials configured per server (Custom Actions path), or no registry needed (file-based path) |
env.clear and env.secret |
Per-project environment variables plus global env vars |
accessories (databases, Redis as named services) |
Not replicated. Managed DB / Redis services, or dedicated servers with their own deploys. |
ssh.user, ssh.proxy |
Server SSH user; bastion/jump host configuration if needed |
aliases (kamal app logs, kamal app exec) |
Direct SSH to the server — the same docker logs / docker exec works, just without the wrapper |
| Atomic container swap via Kamal Proxy | Atomic deploy with automatic symlink swap (file-based path), or container swap via Custom Actions |
kamal rollback |
One-click rollback |
The biggest practical change is where the build runs and what the artefact looks like. Kamal builds a Docker image; DeployHQ produces either a release directory of files (Path 1 below) or, with Custom Actions, a Docker image you push and pull (Path 2). Pick based on what your apps actually need.
Two migration paths
Path 1: Move to file-based atomic releases
If your apps are framework-shaped (Rails, Laravel, Django, Express) and the only reason they were containerised was to fit Kamal's model, Path 1 is the simpler outcome. You give up Docker; you gain atomic symlinked releases that are dramatically easier to operate, debug, and roll back.
This is the right path if your Kamal Dockerfile is mostly FROM ruby:3.3 plus bundle install plus assets:precompile plus CMD ["bundle", "exec", "puma"]. All of that translates directly into a DeployHQ build pipeline plus a systemd unit on the target.
The shape:
- DeployHQ build pipeline runs bundle install, asset compilation, etc. once on our build infrastructure.
- The release ships as files to the target via SSH; symlink swaps in one operation.
- Process management moves from Docker to systemd (or supervisord, pm2, etc.).
- Routing and SSL move from Kamal Proxy to Caddy (or your existing load balancer).
Path 2: Keep containers, deploy via Custom Actions
If your apps need containers — multi-stage Dockerfiles with native dependencies, image-only deploys to remote targets, deploys that orchestrate Kubernetes — Custom Actions (currently in beta) run any CLI tool inside Docker as a deploy step. docker push, docker pull, docker run, kubectl set image, gcloud run deploy, aws ecs update-service, terraform apply all work, with the same DeployHQ audit log, role-based permissions, and rollback story.
This is the right path if you want the DeployHQ governance layer (audit, RBAC, build infrastructure, AI error explanation) without de-containerizing.
Filling the gaps
If you choose Path 1, here is the practical replacement set:
Routing and SSL: Caddy is the closest one-step replacement for Kamal Proxy. Install once, define a site block per app, automatic Let's Encrypt out of the box. nginx + certbot if you prefer that shape; Traefik standalone if you want to preserve the configuration vocabulary.
Accessories (databases, Redis, etc.): Move to managed services if you can — RDS / Neon / Supabase for Postgres, Upstash / Redis Cloud for Redis. Self-host on a dedicated server if you must, with proper backups and monitoring. Either way, treat them as services with their own lifecycle independent of the apps that talk to them.
Worker / job processes: Kamal's servers.job role becomes either a second DeployHQ environment with its own server (for clean isolation) or a server group on the same environment with different SSH commands controlling which processes run where. Most teams prefer the separate-environment shape for safer deploys.
kamal app exec and kamal app logs: Direct SSH. ssh deploy@server 'systemctl status puma' and ssh deploy@server 'journalctl -u puma -f' cover the equivalents.
Heartbeats and uptime monitoring: Better Stack, UptimeRobot, or your monitoring tool of choice. See sending deploy events to Better Stack for the integration pattern.
Migration in three steps (Path 1)
1. Connect your repository
Point DeployHQ at the same Git URL your Kamal setup uses. Read-only deploy keys are generated automatically. DeployHQ checks out your code on our build infrastructure — no git binary needed on the target server.
2. Recreate your servers and roles as environments
Each entry in servers.<role>.hosts becomes a server attached to a DeployHQ environment:
servers.web.hosts: ["1.2.3.4", "1.2.3.5"]→ two servers attached to the production environment, deployed in parallel.servers.job.hosts: ["5.6.7.8"]→ either a separate environment, or a separate server group, or a separate project. Pick based on whether you want job and web deploys to be independent.ssh.userbecomes the server's SSH user.env.clearandenv.secretmap to per-project environment variables; shared values across projects move to global env vars.
3. Replicate the Dockerfile as a build pipeline
A typical Rails Kamal Dockerfile resolves to:
bundle install --deployment --without development test
bundle exec rails assets:precompile
Translate those into build pipeline commands. Add a systemd unit on the target to run bundle exec puma -C config/puma.rb (or your equivalent), and a separate SSH command to sudo systemctl reload puma after deploy.
For database migrations, add bundle exec rails db:migrate as an SSH command set to run after upload, before symlink change.
For most Kamal apps moving via Path 1, end-to-end migration is a half to a full day per app — closer to half a day if the Dockerfile was thin, closer to a full day if you also need to set up Caddy, systemd, and managed databases. Concierge migration is available on Pro and above — point us at your deploy.yml and we will do the migration with you on a screenshare.
What teams ask before they switch
Will I lose Kamal's zero-downtime deploys? No. DeployHQ's atomic symlink swap is functionally equivalent — old release stays running until the new one is in place. For multi-server setups, deploys happen in parallel with the same atomic semantics.
Can I keep using my existing servers? Yes. DeployHQ deploys to any SSH-reachable host. Bring your own VPS, dedicated server, or cloud instance — the same servers Kamal was deploying to.
What about my Kamal accessories? They become external services. Dump and restore the data into the new home (managed Postgres, dedicated Redis server, etc.). Do this once during the cut-over.
Do I have to give up kamal deploy from my laptop? You give up Kamal. You can absolutely still trigger deploys from your terminal — dhq deploy production does the equivalent. The difference is the deploy itself runs on DeployHQ's infrastructure, not your machine.
What if I want to keep Kamal for production and use DeployHQ for staging? That works. Kamal and DeployHQ deploy to different release-directory layouts on the same server (or different servers), so they do not conflict. Many teams run mixed setups for months during a cut-over.
My team is fully bought into the DHH / 37signals approach. That is a reasonable position and Kamal aligns with it well. DeployHQ does not. Stay on Kamal — we mean it. The teams that get value from migrating are the ones who have run into the specific limits at the top of this guide, not the ones for whom config-as-code-and-CLI is a deliberate philosophical choice.
What if I want to keep containers but move off Kamal? Path 2 (Custom Actions) is your route. Or evaluate other PaaS-style alternatives — see DeployHQ vs Dokku and DeployHQ vs Coolify.
Start your migration
Connect a DeployHQ project to your repo, recreate your deploy.yml server roles as environments and servers, replicate the Dockerfile build steps as a build pipeline (Path 1) or as Custom Actions (Path 2), and migrate one app at a time. Plan for half to a full day per app and a parallel-running period before you decommission Kamal.
If your stack is part Kamal-shaped and part not, there is no requirement to move everything. Use DeployHQ for what does not fit Kamal's model and keep Kamal for what does.
Kamal is a trademark of 37signals. DeployHQ is not affiliated with 37signals or the Kamal project. We just understand what the migration looks like when a CLI-and-YAML deploy tool stops being the right shape for the job.