Start / Kamal and KamalCloud
Kamal and KamalCloud
Which Kamal version KamalCloud runs, the two files it writes, and how to run the same deploy without it.
KamalCloud does not replace Kamal or wrap it in a format of its own. Every deploy is the official
kamal CLI, run against your repository’s own config/deploy.yml, plus two files KamalCloud writes
beside it in Kamal’s own format. Anything KamalCloud deploys, you could deploy by hand from the same
checkout.
Kamal builds the image, pushes it, boots containers and swaps traffic. KamalCloud handles the machines, the credentials and the record of who deployed what.
Which Kamal
Deploys run the official image, pinned to ghcr.io/basecamp/kamal:v2.12.0. Nothing is
installed with Ruby or gems. A runner pulls the image when it is prepared, and
every command is a throwaway container:
docker run --rm --network host \
-v /var/run/docker.sock:/var/run/docker.sock \
-v <runner ssh directory>:/root/.ssh \
-v <environment checkout>:/workdir \
ghcr.io/basecamp/kamal:v2.12.0 deploy -d production
- The Docker socket is the runner’s, so images are built on the runner.
- Host networking is needed because Kamal’s local registry listens on the runner’s loopback.
- The SSH directory holds the account’s deploy key, which is how Kamal reaches your servers.
The pin covers the whole account, so every environment deploys with the same Kamal. Preparing a runner again pulls whatever the current pin is.
The two files KamalCloud writes
Each environment is a Kamal destination. Before every run, KamalCloud shallow-clones the repository onto the runner and writes:
| File | What it holds |
|---|---|
config/deploy.<environment>.yml |
What only the environment knows: machines, hostnames, credentials |
.kamal/secrets.<environment> |
The environment’s secrets, as NAME=value lines |
Both are added to the checkout’s .git/info/exclude, so neither can be committed. Nothing is
written into your repository upstream.
What goes in the destination file
Kamal deep-merges a destination file over deploy.yml, so KamalCloud writes only the keys it
actually knows:
| Key | Where it comes from |
|---|---|
service |
The app’s name in KamalCloud |
servers |
One entry per role, listing the IPs of the servers placed in it, and the role’s cmd where one is set |
proxy |
The environment’s domains, plus ssl (see below) |
registry |
localhost:5555, Kamal’s local registry, unless your repository names its own (see below) |
builder.arch |
The architecture of the servers it deploys to |
env |
Plain variables under clear, secret names under secret |
ssh |
The deploy key with keys_only: true, the runner’s SSH config, plus the user and port when they are not root and 22 |
volumes |
Only when the service was renamed, so that <service>_storage follows the new name |
accessories |
The environment’s accessories |
A key KamalCloud has nothing to say about is left out, so your repository’s value stands.
Merge rules worth knowing
Kamal merges hashes and replaces arrays. Two things follow from that:
env.secretis an array. KamalCloud writes your repository’s secret names back into it, alongside its own, so none are lost.- Publishing a port turns the primary role into a hash. KamalCloud carries the repository’s hosts across so they are not dropped.
Kamal also rejects a proxy block that has both host and hosts. KamalCloud uses whichever key
your repository already uses.
What stays yours
Everything else in config/deploy.yml: the image name, the builder, healthchecks, asset_path,
role commands, hooks, and your Dockerfile. KamalCloud reads the file for its shape (roles, hosts,
the secrets it expects) but never runs it:
- ERB is not evaluated. Output tags are read as a placeholder and logic tags are dropped. Kamal still renders them at deploy time, but KamalCloud does not run repository code in order to understand the file.
- Some machines are outside KamalCloud’s reach.
builder.remote,registry.serverandssh.proxyname machines the destination file cannot redirect. When your file sets them, the environment page lists them.
KamalCloud reads the file from the fresh checkout on every deploy, so the file it checks is the one actually being deployed.
Registry
If deploy.yml names no registry, KamalCloud uses Kamal’s local registry on the runner. Kamal
tunnels it to each host over SSH, so there is no account to create.
If your repository names a registry, it stays in use as long as the environment has the secret
its password reads. Without that secret, KamalCloud falls back to the local registry instead of
failing the push.
TLS
kamal-proxy can issue its own Let’s Encrypt certificate, but only on a single host, because it
does not share certificates between servers. KamalCloud therefore sets:
- One host in the proxy role:
ssl: true. The domain has to resolve to that server first. - More than one: add the secrets
TLS_CERTIFICATE_PEMandTLS_PRIVATE_KEY_PEMand KamalCloud pointssslat them. Without them the environment is blocked, not deployed without TLS.
Environments without a destination
An app first deployed with a plain kamal deploy, with no -d, has containers and proxy
routes whose names contain no destination. Adding one would strand them. When KamalCloud
imports such an app, the environment keeps deploying
without a destination: KamalCloud merges your deploy.yml with its own keys into one file and
passes it with -c, rather than using -d.
What each button runs
| In KamalCloud | Kamal command |
|---|---|
| Deploy | kamal deploy |
| Set up and deploy (first time) | kamal setup |
| Prepare | kamal server bootstrap |
| Roll back (on the Deployments list) | kamal rollback <version> |
| Reboot the app | kamal app boot --version <current version> |
| Maintenance / Go live | kamal app maintenance [--message] / kamal app live |
| Release the deploy lock | kamal lock release |
| Stop (a running deployment) | docker rm -f on Kamal’s container, then kamal lock release |
| Tasks | kamal app exec --primary --reuse |
| Accessory actions | kamal accessory boot/reboot/start/stop/restart, and kamal accessory remove --confirmed |
| Removing an environment | kamal remove --confirmed, then docker volume rm for its named volumes via kamal server exec |
Every command adds -d <environment> (or -c for an environment without a destination).
There is no Redeploy button. redeploy is a deployment kind KamalCloud knows, but nothing on the
page starts one.
When an environment publishes a raw port, a deploy becomes
kamal build deliver && kamal app stop && kamal deploy --skip-push. Kamal boots the new
container before stopping the old one, and two containers cannot hold the same published port.
A deploy also sets the checkout’s git user to the person who deployed, so Kamal’s own record on
the server names them rather than root.
Warnings from your deploy.yml
The environment page raises Kamal Handbook advice that a file ignores:
- No
asset_path. During a rollover, a request can reach the old container and 404 on the new one’s fingerprinted assets. builder.archdoes not match the server. KamalCloud writes the server’s architecture into the destination file, which takes precedence over yours.
Template apps
A template has no repository. KamalCloud renders a deploy.yml and a
Dockerfile into the working directory instead, then writes the same two files on top. The
version is passed as VERSION, because Kamal would otherwise read it from git.
Deploying without KamalCloud
Your servers run ordinary Kamal containers, and nothing on them depends on KamalCloud. To deploy one by hand:
- Copy
config/deploy.<environment>.ymlfrom the environment’s checkout on the runner into your repository, or write your own. - Recreate
.kamal/secrets.<environment>from the environment’s secrets. Keep it out of git. - Change
ssh.keysto a key of yours that the servers accept. The destination file points at the path of KamalCloud’s deploy key inside the Kamal container. - Run
kamal deploy -d <environment>.
The service name, roles, volumes and proxy hostnames stay exactly as KamalCloud left them, so Kamal picks up the same containers.