wheels deploy ships a Dockerized Wheels app to production Linux servers via SSH. Ported from Basecamp Kamal's developer CLI — same config/deploy.yml schema, same on-server conventions (container names, labels, network, lock path), invokes the same kamal-proxy Go binary for zero-downtime rollover. No Ruby runtime required.
wheels deploy init # scaffold config/deploy.yml + .kamal/secrets
wheels deploy setup # one-time server bootstrap + first deploy
wheels deploy # rolling deploy
wheels deploy --dry-run # print commands without executing
wheels deploy rollback v1 # roll back to a previous version
wheels deploy config # print resolved config as YAML
wheels deploy version # show Kamal version this port mirrors
wheels deploy app <verb> # boot/start/stop/details/containers/images/logs/live/maintenance/remove
wheels deploy proxy <verb> # boot/reboot/start/stop/restart/details/logs/remove
wheels deploy accessory <verb> # boot/reboot/start/stop/restart/details/logs/remove (sidecars: db/redis/search)
wheels deploy build <verb> # deliver/push/pull/create/remove/details/dev
wheels deploy registry <verb> # setup/login/logout/remove
wheels deploy bootstrap # install Docker on every host (flat alias — preferred)
wheels deploy exec "<cmd>" # run a command on every host (flat alias — preferred)
wheels deploy server <verb> # exec/bootstrap (legacy nested form — see #2677)
wheels deploy prune <verb> # all/images/containers [--keep=N]
wheels deploy lock <verb> # acquire/release/status (manual — normal deploys auto-lock)
wheels deploy fetch-secrets ... # resolve KEY=VALUE lines from an adapter (flat alias — preferred)
wheels deploy extract-secrets # pull one key from a KEY=VALUE block (flat alias — preferred)
wheels deploy print-secrets # print resolved .kamal/secrets (flat alias — preferred)
wheels deploy secrets <verb> # fetch/extract/print (legacy nested form — see #2697)
wheels deploy audit # tail /tmp/kamal-audit.log on each server
wheels deploy details # aggregate app + proxy + accessory status
wheels deploy remove --confirm # teardown all app/proxy/accessory containers
wheels deploy docs [section] # in-terminal config reference
- Container names:
<service>-<role>-<version> - Labels:
service=,role=,destination=,version= - Docker network:
kamal - Lock file:
/tmp/kamal_deploy_lock_<service> - Proxy config:
/home/<user>/.config/kamal-proxy/ - Hook env prefix:
KAMAL_*(neverWHEELS_*— user hooks migrate unchanged)
A server managed by Ruby Kamal can be taken over by wheels deploy without cleanup.
cli/lucli/services/deploy/
├── cli/*.cfc DeployMainCli + Deploy<App|Proxy|Accessory|Build|Registry|Server|Prune|Lock|Secrets>Cli
├── commands/*.cfc Base + Docker/App/Proxy/Builder/Registry/Auditor/Lock/Hook/Accessory/PruneCommands
├── config/*.cfc Config + Role/Env/Builder/Proxy/Registry/Ssh/Accessory/Validator/ConfigLoader
├── lib/*.cfc JarLoader/Mustache/Yaml/SshClient/SshPool/FakeSshPool/Output/SecretResolver
└── secrets/*.cfc BaseAdapter + OnePassword/Bitwarden/AwsSecrets/LastPass/Doppler adapters
cli/lucli/lib/deploy/*.jar jmustache, snakeyaml, sshj + BouncyCastle transitives (URLClassLoader-isolated)
cli/lucli/templates/deploy/ Mustache templates for `wheels deploy init` output
Commands-are-strings invariant: every *Commands.cfc method returns a shell-command string; only *Cli.cfc and the orchestrator execute them. That's why --dry-run is trivial and unit tests run without network.
- Kamal-compatible schema, ONE divergence. ERB in
deploy.ymlis NOT supported (rendering it would require embedding a Ruby runtime). Kamal's native${VAR}env-var interpolation is preserved unchanged — uppercase-snake tokens resolve viaenvOverride → .kamal/secrets → System.getenv → ""(seeConfigLoader.$interpolate). Mustache ({{...}}) is used only bywheels deploy initto scaffold a freshdeploy.yml/secrets; it is NOT applied todeploy.ymlat runtime. Everything else inconfig/deploy.ymlis byte-identical to Kamal 2.4.0. - Hook env prefix is
KAMAL_, notWHEELS_. Deliberate — Ruby Kamal users' existing.kamal/hooks/scripts work unchanged. app live/app maintenanceuse a marker file (/tmp/kamal-maintenance-<svc>) rather than kamal-proxy native maintenance mode. Phase 2 simplification; Phase 3 follow-up will align with Kamal's proxy-native semantics.wheels deploy removeis destructive and requires--confirm. Barewheels deploy removethrows without touching anything.- Lucee reserved scope names in subagent-authored deploy code.
client,session,application— usessh/sc,sess,appinstead. Bit us multiple times during the port. - No
--dry-runflag in Ruby Kamal 2.4.0. Thetools/deploy-config-diff.shharness compares config-layer output only. Byte-identical command-string parity is aspirational; seetools/deploy-dry-run-diff.shfor the plan. wheels deploy server <verb>collides with LuCLI's top-levelservercommand. LuCLI (the picocli runtime under the wheels brand) registersserverfor Lucee dev-server lifecycle, so picocli grabs theservertoken before it can reach the deploy dispatcher. The wheels module exposes flat aliaseswheels deploy bootstrapandwheels deploy execthat sidestep the collision — these are the canonical CLI form. The nestedserver <verb>branch is retained inModule.cfc::deploy()for MCP/programmatic callers. See #2677.wheels deploy secrets <verb>collides with LuCLI's top-levelsecretscommand. Same shape as #2677 — LuCLI registerssecretsfor its own credential store (init/set/list/rm/get/provider). The wheels module exposes flat aliaseswheels deploy fetch-secrets,wheels deploy extract-secrets, andwheels deploy print-secretsthat sidestep the collision — these are the canonical CLI form. The nestedsecrets <verb>branch is retained for MCP/programmatic callers. See #2697.
cli/lucli/tests/specs/deploy/ extends wheels.wheelstest.system.BaseSpec. Run with:
bash tools/test-cli-local.sh
Fixtures at cli/lucli/tests/_fixtures/deploy/configs/ (minimal.yml, full.yml, with-accessories.yml, invalid/*.yml). FakeSshPool.cfc records every command for offline assertions; no sshd needed for unit tests. SshClientSpec + SshPoolSpec exercise real SSH via the fixture at cli/lucli/tests/_fixtures/deploy/sshd/ (brought up by tools/deploy-sshd-up.sh).
- User guides:
web/sites/guides/src/content/docs/v4-0-0/deployment/(first-deploy, production-config, accessories, secrets, hooks, migrating-from-kamal, security-hardening, docker-deployment) - In-source CLI docs:
cli/lucli/services/deploy/cli/docs/(per-verb) - Design spec: docs/superpowers/specs/2026-04-20-wheels-deploy-kamal-port-design.md
- Implementation plan: docs/superpowers/plans/2026-04-20-wheels-deploy-kamal-port.md
- Retrospective: docs/superpowers/plans/2026-04-21-phase1-retrospective.md