Skip to content

Deploy the scanned digest, and roll back when it never goes healthy - #141

Merged
louisbels merged 1 commit into
mainfrom
ci/deploy-digest-rollback
Jul 26, 2026
Merged

Deploy the scanned digest, and roll back when it never goes healthy#141
louisbels merged 1 commit into
mainfrom
ci/deploy-digest-rollback

Conversation

@louisbels

Copy link
Copy Markdown
Member

Three problems in the deploy step. None of them are visible until the day something goes wrong.

It deployed a moving tag

docker compose pull fetched whatever :main pointed at — not the image CI had just built and scanned with Trivy. A push landing between the scan and the rollout ships something nobody checked, and once the tag has moved there is no record of what was running.

The digest now flows from the docker job to deploy and is exported as SECRET_IMAGE, so the rollout is pinned to the exact artifact that passed the gates.

A failed deploy left the failure running

The existing health loop waited 60s and exited 1. That turned the workflow red — and left the broken container serving traffic. Worse, docker image prune -f had already run unfiltered, deleting the previous image, so there was nothing to roll back to even by hand.

Now: record what is running, wait up to 200s (60s is short for a cold start with a WAL replay), and on failure print the last 50 log lines and restore the previous image. The prune keeps a week of history.

Nothing described any of it

docs/self-hosting.md gains a Deploying from CI section — why the digest matters, and why a green docker compose up says nothing about whether the app actually works — plus a reference docs/examples/docker-compose.prod.yml showing SECRET_IMAGE, the health check the rollout depends on, memory limits and log rotation.

All generic. Nothing tied to a particular instance, so anyone self-hosting can lift it.

How this was verified

The script cannot run without deploying, so its logic was replayed locally against stubbed docker commands:

Scenario Result
Becomes healthy pull → up → prune, exit 0
Never healthy, previous image known error, logs, rollback, exit 1
Never healthy, no previous image error, logs, warning, exit 1
Stuck in starting rollback, exit 1

Validated with actionlint. Also removed a duplicate health loop I had introduced alongside the existing one — worth a look in review, since two loops with different timeouts would have been a confusing thing to inherit.

One thing to do after merging

Your production compose needs to accept the variable, otherwise the digest is ignored and the deploy behaves exactly as before (no breakage, just no pinning):

image: ${SECRET_IMAGE:-ghcr.io/largerio/secret:main}

I can apply that on the server once this is merged.

Three problems in the deploy step, all of which only show up on the day
something goes wrong.

**It deployed a moving tag.** `docker compose pull` fetched whatever `:main`
pointed at, not the image CI had just built and scanned with Trivy. A push
landing between the scan and the rollout ships something nobody checked. The
digest is now passed through from the docker job and exported as SECRET_IMAGE,
so the rollout is pinned to the exact artifact that passed the gates.

**A failed deploy left the failure running.** The health loop waited 60s and
exited 1, which turned the workflow red but left the broken container serving
traffic — and `docker image prune -f`, unfiltered, had already deleted the
previous image, so there was nothing to go back to. The loop now records what
was running before, waits up to 200s (60s is short for a cold start with a
WAL replay), prints the last 50 log lines on failure, and restores the previous
image. The prune keeps a week of history.

**Nothing described any of this.** docs/self-hosting.md gains a "Deploying from
CI" section explaining why the digest matters and why a green `docker compose
up` says nothing about whether the app works, plus a reference
docker-compose.prod.yml showing SECRET_IMAGE, the health check the rollout
depends on, memory limits and log rotation — all generic, nothing tied to a
particular instance.

The script cannot be exercised without deploying, so its logic was replayed
locally against stubbed docker commands: healthy (pull, up, prune, exit 0),
unhealthy with a previous image (rollback, exit 1), unhealthy without one
(warn, exit 1), and stuck in "starting" (rollback, exit 1). Also removed a
duplicate health loop this had introduced alongside the existing one.
@louisbels
louisbels merged commit fdc28bf into main Jul 26, 2026
12 checks passed
@louisbels
louisbels deleted the ci/deploy-digest-rollback branch July 26, 2026 16:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant