Zero-downtime deploys: start the new api, wait until healthy, then stop the old one #91
Labels
No labels
priority/P0
priority/P1
priority/P2
priority/P3
size/L
size/M
size/S
size/XL
size/XS
state/blocked
state/done
state/in-progress
state/in-review
state/needs-refinement
state/ready
type/bug
type/chore
type/feature
type/refactor
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
amtronics/platform-actions#91
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
Every deploy through the shared deploy-vps path stops the api container before starting the new one. That gives about 10 s of 502s per deploy, which was seen on rihla in the morning of 8 Oct 2026. While families are registering, that window costs real requests. Ahmad approved this as a follow-up on 9 Oct 2026 (overnight ticket w4rpP0Jy). Until it lands, rihla avoids deploys in its registration windows and keeps its migrations additive (suhba/rihla#417).
Scope
scale api=2with Traefik healthcheck labels, and dropping the fixedcontainer_namethis needs. Rollback when the new one never becomes healthy. A doc for consuming repos.Acceptance criteria
Estimated half a day to a day with testing. Rihla's adoption: suhba/rihla#417.
Picked up.
Proving app:
suhba/suhba-web. It runs on the same VPS as rihla, behind the same Traefik v3.3 docker provider, so it tests the mechanism rihla will actually use. portfolio and namey sit behind NPM with an nginx that proxies the api by name, which works differently.Approach: a rolling swap in plain compose, with no new host tooling such as docker-rollout. Opt-in input
rolling-services; empty keeps today's path byte-for-byte. For each listed service:up --scale <svc>=2N --no-recreate --no-depsdocker stopthe old ones, so the image's stop signal and the composestop_grace_periodapplyIf a new container goes unhealthy or runs out of time, it is removed, the old ones keep serving, and the deploy fails.
Added two acceptance criteria to the body: a graceful stop, and no change for consumers that don't opt in.
Progress, proving on suhba-web (suhba/suhba-web#102, deploy run from the branch). The load loop sends 4 parallel
GET https://suhba.nl/about every 50 ms from outside.suhba-web-1, run 4664): success, 2,224 × 200 and 32 × 404 in a single 2 s window.RetryAttempts: 0).dieevent, so for about 2 s it still dials an address nobody answers. The dial hangs until Traefik's 30 s default dial timeout, and the retry middleware never gets an error to retry on.serversTransportwith a 2 s dial timeout that rolled services reference. A dead address then fails fast and the retry sends the request to the new replica. Testing it on the VPS next. The durable home is the traefik role in infrastructure-vps; I will open an issue there.Results on suhba-web. Every run deployed v0.22.0 from the branch suhba/suhba-web#102 using deploy-vps from #92. The load loop sends 4 parallel
GET https://suhba.nl/every ~50 ms from outside the VPS; any non-200 or connection error counts as a failure.In the final setup, a request that hits the stopping replica costs 1–3 s instead of failing (4 such requests in the access log across both runs, all 200).
Rollback (run 4675): the healthcheck was pointed at a dead port, under load. The new replica went unhealthy and was removed, the deploy reported failure, the old replica kept serving, and 3,336 of 3,336 requests returned 200.
What changed after the first comment:
rolling-settle-seconds(default 10): the old replica keeps serving until Traefik has picked up the new one. Without it, Traefik's only server could be the stopped one (run 4676's cause).rolling-deploy@file(dial timeout 1 s) sits on the VPS by hand for now. Making it ansible-managed is amtronics/infrastructure-vps#544.docker stop, so its stop signal and itsstop_grace_periodapply (nginx images: SIGQUIT). A behaviour test kills thestop -t 0mutant.Rihla was not touched; its adoption stays suhba/rihla#417. Rihla's api needs
stop_grace_periodabove its longest welcome-email send.Merged and released as v0.23.0. Its release deploy (run 4687) went through
deploy-vps@v3(v3.9.0) under the same load loop: 8,468 of 8,468 requests returned 200 over 467 s, with a new image and the rolling swap.webnow runs assuhba-web-web-11.