Skip to content
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.secret is 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.server and ssh.proxy name 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_PEM and TLS_PRIVATE_KEY_PEM and KamalCloud points ssl at 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.arch does 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:

  1. Copy config/deploy.<environment>.yml from the environment’s checkout on the runner into your repository, or write your own.
  2. Recreate .kamal/secrets.<environment> from the environment’s secrets. Keep it out of git.
  3. Change ssh.keys to a key of yours that the servers accept. The destination file points at the path of KamalCloud’s deploy key inside the Kamal container.
  4. 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.