Skip to content

Make the production deploy check the site, and restart once if it is down - #905

Open
edwh wants to merge 2 commits into
developfrom
deploy-self-heal
Open

Make the production deploy check the site, and restart once if it is down#905
edwh wants to merge 2 commits into
developfrom
deploy-self-heal

Conversation

@edwh

@edwh edwh commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

What happened today

production was pushed at 12:02:30. deploy-fly-prod built the image and started a rolling update. The machine came up at 12:08:07 without passing its health check, and flyctl gave up:

Updating existing machines in 'restarters' with rolling strategy
Error: failed to update machine 7813112c002178: Unrecoverable error:
timeout reached waiting for health checks to pass for machine 7813112c002178
Exited with code exit status 1

The job exited 1 and stopped, leaving the machine started but out of the proxy pool. With min_machines_running = 1 that is a total outage — every request got no known healthy instances found for route tcp/443. It stayed down 43 minutes, until the machine was restarted by hand.

Restarting that same image brought it straight back. Nothing was wrong with the build. The deploy had everything it needed to recover and instead walked away.

Change

Stop treating flyctl's exit code as the verdict, and ask the question that actually matters: is the site serving?

  • Probe https://restarters.net/robots.txt from outside Fly — the same static path the Fly health check uses, so the answer doesn't depend on the database being reachable.
  • If it isn't serving, restart each machine once, re-probing after each so we stop as soon as it recovers.
  • Never loop. A restart that doesn't help means something a restart cannot fix, and that needs a person — so the job fails, dumps flyctl status and the last 50 log lines.
  • A deploy that only passed because of the restart says so loudly. This path existing is not a reason to stop asking why a machine failed to come up healthy.

Notes

  • Verified: the YAML parses and both step scripts pass bash -n.
  • jq isn't used anywhere else in this config, so its presence is no longer assumed — it's installed if missing.
  • This is a mitigation, not a cure. The single machine is the reason an unhealthy instance means a full outage; a second machine would make a failed rolling update a non-event. Worth considering separately.
  • Needs to reach the production branch to take effect, so it follows the usual develop → master → production route.

edwh and others added 2 commits September 2, 2026 14:02
…down

Today's outage: production was pushed at 12:02, deploy-fly-prod built the
image and ran a rolling update, the machine came up at 12:08 without
passing its health check, and flyctl gave up:

  Error: failed to update machine 7813112c002178: Unrecoverable error:
  timeout reached waiting for health checks to pass

The job exited 1 and stopped there, leaving the machine started but out
of the proxy pool.  With one machine that is a total outage, and it
stayed down for 43 minutes.  Restarting that same image brought it back
immediately, so nothing was wrong with the build - the deploy simply had
everything it needed to fix this and instead walked away.

So don't stop at flyctl's exit code.  Ask the question that actually
matters - is the site serving? - by fetching robots.txt from outside Fly,
the same static path the health check uses, so the answer doesn't depend
on the database.  If it isn't serving, restart each machine once,
re-checking after each so we stop as soon as it recovers.  Never loop: a
restart that doesn't help means something a restart cannot fix, and that
needs a person.

A deploy that only passes because of the restart says so loudly.  This
path existing is not a reason to stop asking why a machine failed to come
up healthy.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The post-deploy probe only fetched robots.txt.  That is static, and it is
also the path the Fly health check uses - so it reports success in
exactly the case it most needs to catch.  On 2026-09-03 restarters-dev
served robots.txt with a passing health check while every
database-backed page returned 500, because it pointed at a database app
that no longer existed.  This check would have called that deploy fine.

Two probes now, answering different questions.  robots.txt still asks
whether anything is serving, because that is what decides a restart is
worth trying.  /api/homepage_data - public, cheap, aggregates over the
database - asks whether the app actually works, and that is what decides
pass or fail, including after a restart.

Serving but not working is called out and deliberately does not restart:
a restart cannot fix a bad config or an unreachable database, and
cycling a machine there would only hide the fault.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sonarqubecloud

sonarqubecloud Bot commented Sep 3, 2026

Copy link
Copy Markdown

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