Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 12 additions & 10 deletions web/sites/guides/src/content/docs/v4-0-0/deployment/accessories.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ accessories:

`wheels deploy accessory boot redis` on first deploy. Produces a container named `<service>-redis` on the named host, published on 6379. From the app side, connect to `redis://192.0.2.20:6379`.

## Postgres with volume, env, and an init file
## Postgres with volume and env

```yaml title="config/deploy.yml (illustrative — do not type)"
accessories:
Expand All @@ -61,21 +61,23 @@ accessories:
clear:
POSTGRES_USER: app
POSTGRES_DB: myapp_production
secret:
- POSTGRES_PASSWORD
volumes:
- /data/pg:/var/lib/postgresql/data
files:
- config/init.sql:/docker-entrypoint-initdb.d/init.sql
```

- `env.secret:` pulls `POSTGRES_PASSWORD` from `.kamal/secrets` — never commit it to `deploy.yml`.
- `env.clear:` values become `docker run -e` flags on the accessory container.
- `volumes:` persists `/var/lib/postgresql/data` to the host so the database survives `docker rm`.
- `files:` uploads local paths to the container filesystem at deploy time. Useful for initialization scripts, client certs, or any read-only payload the container needs.

<Aside type="caution" title="env.secret and files: are not delivered yet">
Two Kamal accessory features are parsed but not implemented in the current Phase 1 CLI:

- `env.secret:` — secret values from `.kamal/secrets` are not delivered to accessory containers ([#2956](https://github.com/wheels-dev/wheels/issues/2956)/[#2957](https://github.com/wheels-dev/wheels/issues/2957)). For something like `POSTGRES_PASSWORD` you currently have to provision the value another way (e.g. a host-managed env file or volume).
- `files:` — Kamal's "upload local paths into the container filesystem" mechanism is accepted by the config validator but no upload ever happens ([#3088](https://github.com/wheels-dev/wheels/issues/3088)). Initialization scripts and certs need to be placed on the host yourself and mounted via `volumes:`.
</Aside>

## Named containers and labels

Accessory containers are named `<service>-<accessory>` — the example above yields `myapp-db` and `myapp-redis`. They carry the same `service=` label as your app containers, so `wheels deploy details` lists them alongside everything else.
Accessory containers are named `<service>-<accessory>` — the example above yields `myapp-db` and `myapp-redis`. Their `service=` label uses that same combined value (`service=myapp-db`), **not** the app containers' bare `service=myapp` — so a `docker ps --filter label=service=myapp` won't catch them. `wheels deploy details` still lists them alongside everything else because it inspects each declared accessory container by name rather than relying on the shared label.

## Multi-host accessories

Expand Down Expand Up @@ -114,7 +116,7 @@ wheels deploy accessory boot all
wheels deploy accessory stop all
```

`wheels deploy setup` boots every declared accessory as part of first-run. `wheels deploy remove` tears them down.
`wheels deploy setup` does **not** boot accessories in the current Phase 1 CLI — it's an alias for `wheels deploy`, and full first-run orchestration is tracked in [#2957](https://github.com/wheels-dev/wheels/issues/2957). Run `wheels deploy accessory boot all` explicitly as part of first-run. `wheels deploy remove` does tear them down.

## Accessories and `wheels deploy`

Expand All @@ -133,6 +135,6 @@ When you do want to change an accessory — a Redis version bump, a Postgres con
<LinkCard
title="Secrets"
href="/v4-0-0/deployment/secrets/"
description="How .kamal/secrets feeds accessory env.secret lists."
description="How .kamal/secrets works today, and why env.secret delivery is still pending."
/>
</CardGrid>
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ This page takes an existing Wheels app and ships it to one or more Linux servers
- How to verify the rollout, tail logs, and roll back if something breaks

<Aside type="note" title="Assumptions">
You have a Wheels app that already builds into a working Docker image. You have at least one Linux server you can reach over SSH as a user with passwordless `sudo`, or as root. Docker can be installed later with `wheels deploy server bootstrap` — you don't need it preinstalled. You have a container registry (Docker Hub, GHCR, ECR, or self-hosted) you can push to.
You have a Wheels app that already builds into a working Docker image. You have at least one Linux server you can reach over SSH as a user with passwordless `sudo`, or as root. Docker can be installed later with `wheels deploy bootstrap` — you don't need it preinstalled. You have a container registry (Docker Hub, GHCR, ECR, or self-hosted) you can push to.
</Aside>

## Before you start
Expand All @@ -27,7 +27,7 @@ Three quick checks before running anything:

- **SSH works.** `ssh deploy@your-host "uname -a"` succeeds without a password prompt.
- **Registry login works.** `docker login <registry>` succeeds locally.
- **Your app has a `Dockerfile`.** It builds and runs locally (`docker build . && docker run <image>`). If not, write the `Dockerfile` first — `wheels deploy` doesn't generate one.
- **Your app has a working `Dockerfile`.** It builds and runs locally (`docker build . && docker run <image>`). If you don't have one yet, `wheels deploy init` generates a starter `Dockerfile` in step 1 (and refuses to overwrite an existing one unless you pass `--force`) — but verify the generated one actually builds your app before deploying.

## Walk-through

Expand All @@ -41,10 +41,12 @@ Three quick checks before running anything:
wheels deploy init
```

Creates two files:
Creates four files:

- `config/deploy.yml` — the deploy manifest. Under git.
- `.kamal/secrets` — secret values. **Never under git.** Add `.kamal/secrets` to `.gitignore` immediately.
- `Dockerfile` — a starter production Dockerfile. If one already exists, `init` refuses to overwrite it unless you pass `--force`.
- `.dockerignore` — keeps build context lean.

A `.kamal/hooks/` directory is also created for optional local hook scripts. It's empty and safe to leave that way.

Expand Down Expand Up @@ -88,7 +90,8 @@ Three quick checks before running anything:
# Registry — matches registry.password[0] in deploy.yml
KAMAL_REGISTRY_PASSWORD=$(op read op://Production/Registry/password)

# App-level secrets — referenced under env.secret in deploy.yml
# App-level secrets — will feed env.secret once container delivery
# lands (#2956/#2957); harmless to stage here in the meantime
DATABASE_URL=$(op read op://Production/App/database-url)
WHEELS_RELOAD_PASSWORD=$(op read op://Production/App/reload-password)
```
Expand All @@ -106,30 +109,43 @@ Three quick checks before running anything:
clear:
WHEELS_ENV: production
WHEELS_DATASOURCE_CLASS: com.mysql.cj.jdbc.Driver
secret:
- DATABASE_URL
- WHEELS_RELOAD_PASSWORD
```

`clear:` values are baked into `deploy.yml` and safe under git. `secret:` names pull from `.kamal/secrets`. At deploy time, `wheels deploy` translates both into `docker run -e` flags.
`clear:` values are baked into `deploy.yml` and safe under git. At deploy time, `wheels deploy` translates them into `docker run -e` flags.

<Aside type="caution" title="env.secret is not yet supported">
Kamal's `env.secret:` block — naming `.kamal/secrets` keys to be passed into the app container — is **not delivered to containers yet**. The released 4.0.3 CLI silently drops `env.secret` entries; CLIs built from `develop` reject the block outright with `Wheels.Deploy.EnvSecretUnsupported` so the gap can't bite silently. Secret delivery to app containers is tracked in [#2956](https://github.com/wheels-dev/wheels/issues/2956) and [#2957](https://github.com/wheels-dev/wheels/issues/2957). Until it lands, get secrets into the container another way (bake non-secrets into the image, or mount an env file you manage yourself). `.kamal/secrets` still works for its other consumer: `registry.password` resolution at `docker login` time.
</Aside>

5. **Bootstrap Docker on the host (first time only).**

If Docker is already installed on the target, skip this step. Otherwise:

```bash {test:cli cmd="wheels deploy server bootstrap"}
wheels deploy server bootstrap
```bash title="Install Docker on every host (requires reachable hosts)"
wheels deploy bootstrap
```

Runs `which docker || curl -fsSL https://get.docker.com | sh` on every host. Idempotent — safe to run on a host that already has Docker. Fails fast if SSH or `sudo` isn't configured correctly.

<Aside type="note" title="Use the flat verb">
Use `wheels deploy bootstrap`, not `wheels deploy server bootstrap`. The CLI runtime registers its own top-level `server` subcommand (Lucee instance management), so the nested form prints that unrelated help text and never reaches the deploy module ([#2677](https://github.com/wheels-dev/wheels/issues/2677)).
</Aside>

6. **Run setup once.**

```bash title="First-time deploy"
wheels deploy setup
```

`setup` is the first-run verb. It runs the full deploy flow *and* boots `kamal-proxy` and any accessories you've declared. On subsequent deploys you use `wheels deploy` (without `setup`) because the proxy and accessories are already running.
`setup` is the first-run verb. In the current Phase 1 CLI it is an alias for `wheels deploy` — it does **not** yet boot `kamal-proxy` or any accessories you've declared (full first-run orchestration is tracked in [#2957](https://github.com/wheels-dev/wheels/issues/2957)). Boot those explicitly before (or right after) your first deploy:

```bash title="First-run orchestration (run explicitly for now)"
wheels deploy proxy boot
wheels deploy accessory boot all
wheels deploy setup
```

On subsequent deploys you use `wheels deploy` — the proxy and accessories are already running.

Expect a few minutes on the first run — Docker pulls the base images for `kamal-proxy`, any accessories, and your app. Output is prefixed with `[host]` so you can see what each server is doing in parallel.

Expand All @@ -140,10 +156,10 @@ Three quick checks before running anything:
Check the container state:

```bash title="Check container state"
wheels deploy app details
wheels deploy app containers
```

Prints `docker ps` output filtered by your service label, for every host. You should see one running container per host per role.
Prints `docker ps` output filtered by your service label, for every host. You should see one running container per host per role. (`wheels deploy details` gives the wider view — app, proxy, and accessories. `wheels deploy app details` is a different verb: it requires `--release=<version>` and reports a single container's `docker inspect` status.)

8. **Make a change and redeploy.**

Expand All @@ -155,7 +171,7 @@ Three quick checks before running anything:

Same command, no `setup`. The rolling flow kicks in: new image builds, pushes, pulls to every host, then the proxy cuts traffic over host-by-host. Zero downtime — `kamal-proxy` drains in-flight requests to the old container before switching.

The version label is the short git sha by default. Override with `--version=<tag>` if you tag releases differently.
The version label is the short git sha by default. Override with `--release=<tag>` if you tag releases differently. (`--release` rather than `--version` because the CLI runtime's root parser claims `--version` for itself.)

</Steps>

Expand All @@ -178,12 +194,12 @@ If something looks wrong, `wheels deploy details` is always the first stop.
Every deploy tags its container with the version label (git sha by default). To roll back, you point `wheels deploy rollback` at a previous version:

```bash title="Roll back to a previous version"
wheels deploy rollback --version=abc1234
wheels deploy rollback abc1234
```

Finds containers tagged `abc1234` on every host, starts them, and asks the proxy to switch traffic back. The old containers usually still exist on-host — `wheels deploy` doesn't prune aggressively — so rollback is fast. If you've pruned, the rollback fails at the "no such container" step; re-deploy from that sha instead.
The version is a positional argument, same as Ruby Kamal's `kamal rollback VERSION`. Don't write `--version=abc1234` — the CLI runtime's root parser intercepts `--version` before the deploy module sees it and the command fails ([#2674](https://github.com/wheels-dev/wheels/issues/2674)).

Scoped rollouts and rollbacks are supported via `--hosts=` and `--role=` flags, useful when you want to test a change on one host before fanning out.
Finds containers tagged `abc1234` on every host, starts them, and asks the proxy to switch traffic back. The old containers usually still exist on-host — `wheels deploy` doesn't prune aggressively — so rollback is fast. If you've pruned, the rollback fails at the "no such container" step; re-deploy from that sha instead.

## Troubleshooting

Expand Down
16 changes: 8 additions & 8 deletions web/sites/guides/src/content/docs/v4-0-0/deployment/hooks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,19 @@ You've read [Your First Deploy](/v4-0-0/deployment/first-deploy/). Hooks are use
|------|-------|---------|
| `pre-deploy` | Before any host work starts. | Yes — non-zero exit aborts the deploy before anything on the servers changes. |
| `post-deploy` | After a successful deploy. | Yes — non-zero exit fails the deploy after-the-fact, useful for smoke tests. |
| `post-deploy-failure` | After a deploy that threw an error. | No — this is already a failure path; the exit code is logged but doesn't change what happens. |
| `post-deploy-failure` | After a deploy that threw an error. | Already a failure path — but make the hook itself exit 0. A non-zero exit currently throws and **replaces the original deploy error** in the output, masking the root cause ([#3087](https://github.com/wheels-dev/wheels/issues/3087)). |

All three live under `.kamal/hooks/` and must be executable (`chmod +x`). They run on the control machine — the same machine running `wheels deploy` — not on the target hosts.

## Environment variables every hook receives

`wheels deploy` fires every hook with a `KAMAL_*` env block that matches Ruby Kamal's contract exactly. Keeping the prefix `KAMAL_` (not `WHEELS_`) means any hook written for Ruby Kamal works unchanged — you can port hooks between the two tools without editing.
`wheels deploy` fires every hook with a `KAMAL_*` env block that follows Ruby Kamal's naming. Keeping the prefix `KAMAL_` (not `WHEELS_`) means hooks written for Ruby Kamal that read these variables work unchanged — you can port them between the two tools without editing. The block is a subset of Ruby Kamal's full contract: variables like `KAMAL_ROLE` and `KAMAL_SERVICE` are not set.

| Variable | When available | Value |
|----------|---------------|-------|
| `KAMAL_VERSION` | all events | Version being deployed (git short sha by default). |
| `KAMAL_HOSTS` | all events | Comma-separated list of hosts in the deploy. |
| `KAMAL_PERFORMER` | all events | Local user running `wheels deploy` (`$USER`). |
| `KAMAL_ROLE` | all events | Role being deployed, or empty when the deploy spans roles. |
| `KAMAL_PERFORMER` | all events | `git config user.name`, falling back to `$USER` when unset. |
| `KAMAL_DESTINATION` | all events | Value of `--destination`, or empty. |
| `KAMAL_RUNTIME` | `post-deploy`, `post-deploy-failure` | Seconds elapsed since the deploy started. |
| `KAMAL_ERROR` | `post-deploy-failure` only | Error message that stopped the deploy. |
Expand Down Expand Up @@ -83,7 +82,8 @@ The non-zero exit stops `wheels deploy` before it touches production.

```bash title=".kamal/hooks/post-deploy-failure (illustrative — do not type)"
#!/usr/bin/env bash
set -euo pipefail
# Deliberately no `set -e`: exit 0 even if the page fails, so the
# original deploy error stays visible (see #3087).

curl -sS -X POST https://events.pagerduty.com/v2/enqueue \
-H 'Content-Type: application/json' \
Expand All @@ -98,7 +98,7 @@ curl -sS -X POST https://events.pagerduty.com/v2/enqueue \
}
}
EOF
)"
)" || true
```

## Output
Expand All @@ -112,7 +112,7 @@ Hook stdout and stderr are merged and prefixed with `[hook:<name>]` in the deplo

A non-zero exit from `pre-deploy` aborts the deploy before any server work. A non-zero exit from `post-deploy` marks the deploy as failed after-the-fact — the containers are already rolled over, so this is useful for integration smoke tests that gate "did the deploy actually work" rather than "can I start the new container."

`post-deploy-failure` never changes the exit code; it runs best-effort on an already-failed path.
`post-deploy-failure` runs on an already-failed path, so the overall exit stays non-zero either way — but a non-zero exit from the hook itself currently throws and **replaces the original deploy error** in the output ([#3087](https://github.com/wheels-dev/wheels/issues/3087)). Until that's fixed, write `post-deploy-failure` hooks to always exit 0 (swallow notification failures with `|| true`) so the real error survives.

## Debugging hooks

Expand Down Expand Up @@ -141,7 +141,7 @@ KAMAL_PERFORMER=$USER \
<LinkCard
title="Secrets"
href="/v4-0-0/deployment/secrets/"
description="Hooks can shell out to wheels deploy secrets fetch."
description="Hooks can shell out to wheels deploy fetch-secrets."
/>
<LinkCard
title="Migrating from Kamal"
Expand Down
Loading
Loading