Repository navigation
feat(app): expose kamal-proxy rollout as kamal app rollout deploy/set/stop - #23
Merged
Merged
Conversation
…/stop
## Summary
kamal-proxy has implemented cookie-keyed canary rollouts for a while, but the
gem had zero surface for it (`grep rollout lib/` returned nothing), so the only
way to split traffic was to shell into the proxy container by hand.
This adds `kamal app rollout deploy|set|stop`, following the layer cake:
- `Kamal::Configuration::Proxy` builds the rollout arg lists. `rollout deploy`
accepts only `--target`/`--deploy-timeout`/`--drain-timeout`, so it needs its
own builder rather than reusing `deploy_options` - the service already exists
and keeps its host/TLS/buffering options.
- `Kamal::Commands::App::Proxy` emits the three `docker exec kamal-proxy
kamal-proxy rollout ...` commands.
- `Kamal::Cli::App#rollout` takes the action as a positional argument, matching
the `kamal proxy boot_config <set|get|reset>` precedent.
- `Kamal::Cli::App::RolloutBoot` boots the canary containers alongside the live
ones and registers them as the rollout target, stopping the new container
again if registration fails.
## Test Coverage
- commands: rollout deploy/set/stop argv, including that deploy drops the
non-rollout proxy flags, honours custom timeouts, and that set supports
percent, list, both, and percent zero
- cli: rollout deploy boots the canary and leaves the live version serving,
refuses a version that is already deployed, set/stop reach every proxy role,
and set without --percent/--list raises
## Verification
- [x] bundle exec rubocop --parallel passes
- [x] unit tests pass (7 pre-existing builder failures unchanged, verified
against a clean tree)
mhenrixon
force-pushed
the
issue-6-app-rollout
branch
from
July 25, 2026 07:12
16c1c9e to
e3fa764
Compare
This was referenced Jul 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
kamal-proxy has had cookie-keyed canary rollouts (
rollout deploy,rollout set --percent/--list,rollout stop) for a while, but the gem exposed none of it —grep rollout lib/returned nothing, so the only way to split traffic was todocker execinto the proxy container by hand.This adds
kamal app rollout deploy|set|stopthrough the full layer cake:lib/kamal/configuration/proxy.rb—rollout_deploy_command_args/rollout_set_command_args.kamal-proxy rollout deployaccepts only--target,--read-target,--deploy-timeout,--drain-timeout, so it gets its own builder instead of reusingdeploy_options: the service already exists and keeps its host/TLS/buffering/logging options.lib/kamal/commands/app/proxy.rb—rollout_deploy/rollout_set/rollout_stop, emitted through the existingproxy_exec.lib/kamal/cli/app.rb—kamal app rollout <deploy|set|stop>with--percentand--list.lib/kamal/cli/app/rollout_boot.rb(new) — boots the canary containers alongside the live ones and registers them as the proxy's rollout target, stopping the new container again if registration fails.Typical flow:
Closes #6
Test plan
kamal app rollout deployboots a second container per proxy role without touching the live one, and the live version keeps servingkamal app rollout set --percent 10splits traffic; thekamal-rolloutcookie keeps a session pinnedkamal app rollout set --list <value>routes only matching cookie valueskamal app rollout stopreturns all traffic to the live target and leaves the canary containers forkamal app stale_containers --stopkamal app rollout deployagainst an already-deployed version fails with a readable message instead of a docker name clashkamal app rollout setwith neither flag raisesDeviations & judgment calls
Discoveries
kamal-proxy rollout deployaccepts ONLY--target,--read-target,--deploy-timeout,--drain-timeout(../kamal-proxy/internal/cmd/rollout_deploy.go). It does NOT take the fulldeployoption set (--host,--tls,--error-pages, buffering, …). SoProxy#deploy_command_argscould not be reused; rollout needed its own, much smaller arg builder.rollout setrequires one of--percent/--list(cobraMarkFlagsOneRequired);rollout stoptakes no flags.Kamal::Cli::Base#commandresolves the invocation class againstKamal::Cli::Main.subcommand_classesand would raiseNoMethodErroron a nested Thor subcommand class, because a nested class is not in that list. That ruled outsubcommand "rollout", Kamal::Cli::App::Rollout.Judgment calls
rollouttakes a positional action (kamal app rollout deploy|set|stop) instead of a nested Thor subcommand — thekamal proxy boot_config <set|get|reset>precedent, and it sidesteps theCli::Base#commandlimitation above. The requested surface (kamal app rollout set --percent 10) is unchanged.rollout deployboots the canary containers itself rather than requiring a pre-booted container. Without that the command is unusable: nothing else in kamal boots a container without also making it live.rollout deployprepares assets but not error pages / SSL certificates. Both are service-level proxy state established by the real deploy, androllout deployhas no flags to pass them anyway. Assets are per-version and the canary container needs them.rollout stoponly stops the traffic split; it does not stop the canary containers. Removing them iskamal app stale_containers --stop(or the nextkamal deploy), which already exists. Makingrollout stopkill containers would surprise anyone re-enabling a split.docker runwould otherwise fail with an opaque "name already in use"; we raise aKamal::Cli::BootErrornaming the version instead.--listis a Thor:arrayso--list a band repeated--list=a --list=bboth work;optionizeemits one--list=per value, which cobra's StringSlice accepts.--percent 0is honoured (checked with.nil?, not truthiness) — a legitimate way to park a rollout at zero traffic without tearing it down.--read-targetsupport. This branch is rooted onmain, which has no loadbalancer / reader-writer concept.Known limitation to carry into
dashLike
Kamal::Cli::App::Boot, this registers the rollout target per host — each host tells its own proxy about its own container. That is correct for the one-proxy-per-host model onmain. With dash's loadbalancer (one proxy fronting several hosts, auto-activated for a primary role with >1 host) the per-hostrollout deploycalls would overwrite each other's rollout target. Rollout and the loadbalancer are therefore not wired together yet; use rollout on single-proxy services until a follow-up teachesKamal::Commands::Loadbalancerto emitrollout deploy --target/--read-target.Verification
bundle exec rubocop --parallelclean.bin/test) not run: it needs Docker-in-Docker plus a published proxy image, and no integration coverage was added for rollout — the issue asked for "integration or command-construction test", and command construction is covered.