Skip to content

feat(proxy): rename container identity to dash and migrate hosts across - #130

Merged
mhenrixon merged 2 commits into
mainfrom
issue-124-gem-container-identity
Aug 26, 2026
Merged

feat(proxy): rename container identity to dash and migrate hosts across#130
mhenrixon merged 2 commits into
mainfrom
issue-124-gem-container-identity

Conversation

@mhenrixon

@mhenrixon mhenrixon commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Summary

Part 2 of stage 3c (#124), the gem side. The proxy image moved first in zoolutions/dash-proxy#122, released as v1.1.0.0, which this pins.

Renamed From To
proxy container kamal-proxy dash-proxy
port holder kamal-proxy-net dash-proxy-net
docker network kamal dash
config volumes kamal-proxy-config, kamal-loadbalancer-config dash-proxy-config, dash-loadbalancer-config
image title label kamal-proxy dash-proxy
in-image paths /home/kamal-proxy/… /home/dash-proxy/…

⚠️ This causes a short outage on every proxy host

Replacing the container means the old one must release ports 80/443 before the new one can bind them, and no port-holder handoff spans two container names. Accepted deliberately — settled in #124.

The three migrations

Wired through a new Dash::Cli::Proxy::LegacyRename, run once per proxy host before the normal boot. All idempotent, all guarded on the destination not existing.

  1. Bridge the network. Docker cannot rename one, so dash is created alongside kamal and everything still attached joins the new one. App containers get replaced by the next deploy anyway; accessories do not, which is the whole point — without this a renamed proxy cannot reach db or redis.
  2. Copy the config volume, before anything starts. It holds the routing table and the ACME account and certificate cache; losing it means re-issuing every certificate and spending Let's Encrypt rate limits to get back to where we were.
  3. Replace the legacy container, with the configured drain timeout.

Nothing removes the legacy network or volume. An operator who wants them gone runs docker network rm kamal by hand; stage 3d deletes this whole path.

A data-loss bug worth reading

I first wrote the volume-copy guard the way the issue phrases it:

exists || legacy_exists && create && copy

Shell && and || share precedence and associate left, so that parses as ((exists || legacy_exists) && create) && copy. Once the new volume existed, exists short-circuited the || to true and the copy ran anyway — re-copying the legacy volume over live state on every deploy.

Restructured to lead with a negated guard so the whole chain is one left-associative AND:

! exists && legacy_exists && create && copy || true

Verified against a real shell rather than reasoned about twice:

Case Result
new volume exists skipped ✓
new missing, legacy exists copies ✓
neither (fresh host) skipped, exit 0 ✓

Test plan

  • Unit suite — 1553 runs, 2 failures, both the known Apple-Silicon builder cases
  • bin/test incl. integration — 1572 runs, 2 failures, 0 errors; integration added 19 runs and zero failures, so the migrations ran against real Docker-in-Docker hosts without breaking a deploy
  • bundle exec rubocop --parallel — 223 files, no offenses
  • cd docs && bundle exec rspec — 64 examples, 0 failures
  • Flag manifest regenerated at v1.1.0.0; coverage guard green
  • Verified the published image before pinning to it: /usr/local/bin/dash-proxy, uid 1001, multi-arch, and org.opencontainers.image.title=dash-proxy — which the prune filters key on

Deviations & judgment calls

A second bug, caught by tests. Making the prune filters dual-match meant changing label, which is also used for --label on docker run — so containers were briefly created with --label dash-proxy instead of the full key=value. Split into label (full pair, for creation) and image_title/legacy_image_title (bare, for pruning).

Prune filters match both titles. Docker ANDs multiple --filter label= values, so this is two chained commands, not one filter with two values. Without the legacy pass, dash proxy remove on a host that has not yet migrated silently leaves the old container and image behind.

The issue's file list was incomplete. It missed Dash::Configuration::Proxy::CONTAINER_NAME / LOADBALANCER_CONTAINER_NAME, commands/app/execution.rb, and commands/proxy/cert_transfer.rb. All found by grepping for the identifiers rather than following the list.

bin/sync-proxy-flags was broken by part 1 — it shelled out to kamal-proxy --help to read Cobra's flag list, so it failed the moment the binary moved. Fixed here. The flag surface itself is unchanged, this release being a rename.

Legacy names live as LEGACY_* constants on Dash::Configuration::Proxy rather than inline strings, so stage 3d has one place to delete and grep LEGACY_ shows the entire compatibility surface.

otel_shipper.rb still reports service.name: "kamal". Left alone deliberately — that is telemetry identity, and renaming it breaks existing operator dashboards, the same reasoning #124 uses to defer the proxy's Prometheus namespace. Worth its own decision.

Docs claimed things this falsifies. from_kamal.rb said the container keeps its name and "dash manages the one kamal booted"; overview.rb said an upgrade leaves it untouched. Both corrected, including the outage operators must plan for.

⚠️ Not done — the gate that actually matters

The manual upgrade rehearsal from #124 has not been run: deploy a host on the pre-rename gem, upgrade, then confirm the ACME certificate was not re-issued (compare notBefore), the accessory is attached to both networks, dash-proxy list returns the same services, and a second deploy replaces nothing.

Integration proves the commands work against real Docker. It does not prove the certificate store survives, because the dind fixtures have no real ACME certificate. Given this is 4.0.0 and a botched volume copy means re-issuing certificates against Let's Encrypt rate limits, I would not merge this without that rehearsal.

Closes #124


Summary by cubic

Renames the proxy container from kamal-proxy to dash-proxy (including network, config volumes, and in-image paths) and adds a one‑time migration that runs on each proxy host before boot. Replacing the container costs a short outage per host because ports 80/443 must be released, which is accepted per #124.

Migration

  • Creates the dash network, copies the config volume (routing table + ACME cache) into dash-proxy-config, and swaps the legacy container.
  • Accessories stay attached to both networks so the proxy can still reach db and redis.
  • The volume‑copy guard now uses a negated leading condition, fixing a shell precedence bug that would have re‑copied legacy data over live state on every deploy.
  • Migration is idempotent and guarded on the destination not existing.

Other changes

  • Prune filters now match the legacy image title too, so dash proxy remove doesn’t leave old containers behind.
  • bin/sync-proxy-flags was broken by the binary rename and now calls dash-proxy; the flag surface is unchanged.
  • Legacy network and volume are left in place for manual cleanup; stage 3d will delete this path.
  • The manual upgrade rehearsal to confirm certificates survive the volume copy is still pending.

Written for commit c3844eb. Summary will update on new commits.

Review in cubic

Stage 3c part 2 of the server-artifact rename (#124), gem side. The proxy
image moved first in zoolutions/dash-proxy#122, released as v1.1.0.0.

Renamed: the proxy container and its port holder, the docker network every
app, accessory and proxy joins, the config volumes, the image title the
prune filters key on, and the in-image paths the gem mounts and execs.

Three migrations run once per proxy host, before the normal boot, wired
through a new Dash::Cli::Proxy::LegacyRename:

  - The network is bridged rather than renamed, because docker cannot
    rename one. App containers would be replaced by the next deploy
    anyway; accessories are not, and without the bridge a renamed proxy
    cannot reach db or redis.

  - The config volume is copied before anything starts. It holds the
    routing table and the ACME account and certificate cache, so losing it
    means re-issuing every certificate and spending Let's Encrypt rate
    limits to get back to where we were.

  - The legacy container is stopped and removed so the renamed one can
    claim ports 80/443. No port-holder handoff spans two container names,
    so this stage costs a brief outage per host. Deliberate, and settled
    in the issue.

Each is idempotent and guarded on its destination not existing, so a
second deploy is a no-op. The volume copy's guard is negated and leads its
chain: shell && and || share precedence and associate left, so the obvious
`exists || legacy && create && copy` parses as `((exists || legacy) &&
create) && copy` and would re-copy the legacy volume over live state on
every deploy. Verified against a shell rather than reasoned about.

Prune filters now match the pre-rename image title as well as the current
one, or `dash proxy remove` would silently leave the old container and
image on a host that has not yet migrated. Docker ANDs multiple
`--filter label=` values, so that is two chained commands.

Nothing removes the legacy network or volume: an operator who wants them
gone removes them by hand, and stage 3d deletes this whole path.

MINIMUM_VERSION is NOT bumped here - that lands with the docs, once
v1.1.0.0 is pullable.

Refs #124
MINIMUM_VERSION moves to v1.1.0.0, the first image with the renamed
binary, user, data directory and image title. Verified before pinning:
the published tag carries /usr/local/bin/dash-proxy, runs as uid 1001,
is multi-arch, and its org.opencontainers.image.title is dash-proxy —
which the prune filters key on.

bin/sync-proxy-flags invoked `kamal-proxy` to read Cobra's help, so it
broke the moment the binary moved. Fixed and the manifest regenerated;
the flag surface is unchanged, this release being a rename.

The docs claimed three things this stage falsifies: that the container
keeps the kamal-proxy name, that dash manages the one kamal booted, and
that an upgrade leaves it untouched. Replaced with what actually happens,
including the part operators need to plan for — replacing the container
costs a short outage on that host, because the old one has to release
ports 80/443 before the new one can bind them.

Refs #124
@mhenrixon mhenrixon self-assigned this Aug 26, 2026
@mhenrixon mhenrixon added the enhancement New feature or request label Aug 26, 2026
@mhenrixon
mhenrixon merged commit 000affd into main Aug 26, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Stage 3c: container identity — proxy container, docker network, volumes, image label, in-image paths

1 participant