You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
An app that resolves to several servers is now rolled a batch at a time
instead of deployed to all of them at once.
shipnode does not manage the load balancer and never talks to it. It owns
one thing: a readiness endpoint per replica that answers 200 normally and
503 while that replica is being deployed. Every managed LB decides rotation
from a health check, so flipping that endpoint is enough to pull a replica
out and put it back — no provider APIs, no LB for shipnode to keep alive,
and the same code works behind Hetzner, DigitalOcean, an ALB or someone
else's nginx.
The signal is a sentinel file rather than a rewritten Caddy config. Caddy
matches on its existence, so flipping takes effect with no reload and
survives a Caddy restart or a reboot — a drained replica stays drained until
something undrains it.
fleet.drainWait is the number that matters and the one shipnode cannot infer.
It has no way to know your LB's check interval or unhealthy threshold, so it
cannot detect when traffic has actually stopped; it flips the sentinel, waits
the declared seconds, and only then touches the app. Set it too low and the
roll drops requests that were already in flight. The dry run prints it as an
explicit step for that reason.
FleetOrchestrator sits above DeployOrchestrator rather than replacing it. A
replica's deploy is the ordinary single-server deploy — same releases, same
blue-green, same health check — so the composition is blue-green within a
replica, rolling across replicas. The invariant the new layer exists to hold
is that no more than `batch` replicas are ever out of rotation, and that a
replica is out of rotation before anything touches it. deployApp now takes a
fleet-wide releaseId, because a per-replica timestamp makes a converged fleet
indistinguishable from a half-rolled one.
A failure stops the roll and resolves rather than throwing: a partly-rolled
fleet is a real state to report, not an exception to unwind. Updated replicas
keep serving the new release, untouched ones the old, drained-but-undeployed
batch-mates go back into rotation, and the replica that actually failed stays
drained so no traffic reaches it and it can be inspected as-is.
Replicas stop claiming the app's public domain, which is the sharp edge of
replicating a Caddy config: five replicas all racing Let's Encrypt for one
name. TLS terminates at the load balancer and each replica serves plain HTTP
bound to its private address. Blue-green still flips that site's upstream, so
the two compose.
Also: --on <server> scopes deploy/status/logs/restart/stop/env/unlock to one
replica, `drain`/`undrain` give manual rotation control for inspecting a
replica without deploying it, and watch mode requires --on for a fleet app
since it holds one connection and patches one live release.
Running it against a real three-server config found four things unit tests
did not, each now covered:
- The dry run printed a fleet app once per replica, because getAppsForServer
deliberately stopped being a partition.
- Its Caddy preview showed the public-domain site, which is precisely the
file a replica does not write.
- The deploy flow omitted the drain steps, hiding the one setting the user
has to get right.
- `--app api --on db-1` dialled a server api does not run on and failed with
an SSH handshake timeout instead of saying so. --on is now checked against
what was actually asked for, before any connection is opened.
preDeploy still runs once per replica and now warns loudly when it would run
N times. Moving migrations to a run-once hook is Phase 2.
0 commit comments