INCIDENT: production has delivered no alerts since 2026-08-11 — crash-looping on a stale image against a forward-migrated DB #186
Labels
No labels
area:ai
area:ci-cd
area:notifications
area:observability
area:public-pages
backlog
bug
duplicate
enhancement
help wanted
invalid
question
security
type:decision
type:feature
type:infra
type:maintenance
type:security
v1.0.1
v1.1.0
v1.2.0
v1.3.0
v2.0.0
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
rbrooks/WeatherBot#186
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?
Severity: high. Production severe-weather alerting has been silently down for ~9.5 days. Found incidentally on 2026-08-20 while querying production for an unrelated
radar_animateddistribution (#184).Impact
2026-08-11 03:27:05 UTC. Nothing since.sent_alertsholds 1,719 rows and has not grown in 9.5 days.postgres,signal-cli) areUp 9 days (healthy), so a container-level health view looks partly fine.Root cause
The database is at Alembic revision
0033(shipped in v1.8.1). The container is runningweatherbot-app— a locally-built image reportingWEATHERBOT_VERSION=v1.7.0-29-g12d47dd, which predates0033and does not contain it. Startup therefore dies every time:app/main.py's lifespan runsalembic upgrade headwithcheck=True, so a migration failure is fatal to startup by design — correct behaviour, but it means a code/DB mismatch is an unrecoverable crash loop rather than a degraded start.How the mismatch happened
The running container's compose labels point at the old pre-Ansible checkout:
while the Ansible-managed deployment lives at
/opt/weatherbot-svc. Both use the compose project nameweatherbot, so they own the same containers. Something randocker compose upfrom/home/ryan/WeatherBoton ~2026-08-11 05:54 (containerCreatedtimestamp), which rebuilt the app from that stale checkout and replaced the Ansible-deployed v1.8.1 container — while leaving the database forward-migrated at0033.This is exactly the failure mode
.forgejo/workflows/cd.ymldocuments as the reason the merge-time dev deploy was retired: "it rebuilt the stack from a git checkout under the same compose project name the Ansible role owns." The same landmine existed on production and was never disarmed.weatherbot.service(the Ansible-managed systemd unit) is present but infailedstate.Proposed remediation
docker-host. Thegroup_varspin is1.8.1, whose schema is exactly0033, so it matches the database with no migration needed. This is the committed intended state./home/ryan/WeatherBotmust stop being able to clobber the Ansible stack. Either remove it, or rename its compose project so it can never again ownweatherbot's containers. Restoring service without this leaves the same trap armed.Not the cause
The
weatherbot-backup.timerisactiveand its serviceinactive dead(normal between runs). Postgres and signal-cli are healthy. This is purely the app container.Incidental finding (the query that surfaced this)
notification_channelson production: 4 rows, allradar_animated = true. So #184's migration0037backfill is a no-op on production as well as dev — no channel has opted out of radar, so none can be wrongly switched on. The migration remains correct; there is simply no row that exercises it.Resolved — service restored 2026-08-20 ~21:47 UTC
Production is running
git.rhoving.com/rbrooks/weatherbot:1.8.1(the registry image the pin specifies, not a local build), healthy, with the full Ansible-managed stack recreated (postgres:16.14-alpine, signal-cli).Confirmed working end-to-end, not merely container-healthy: all APScheduler jobs execute successfully (
poll_alerts_job,poll_spc_job,process_delivery_outbox_job, …), andsent_alertsrecorded a delivery at2026-08-20 21:51:25 UTC— the first since 2026-08-11 03:27. Total outage: ~9.7 days. Database remains at0033, matching 1.8.1, so no migration was needed.Correcting the root cause above
My original attribution — "something ran
docker compose upfrom/home/ryan/WeatherBot" — was stated with more confidence than the evidence supported. The journal now shows a more specific first domino:weatherbot.servicetimed out duringdocker compose upbecause Postgres took 66s to become healthy, and systemd SIGINT'd compose with the stack half-started — the app container never came up under the unit.This is precisely the failure the systemd-unit change in iac-repo#277 was written to fix, whose own comment reads: "is slow to become healthy,
compose upstops waiting, and the unit dies with the stack half-started." Production had not received that change.What happened between
05:40:59(unit failed) and05:54:48(the crash-loopingweatherbot-appcontainer's creation timestamp) is not established by evidence I have. The 14-minute gap and the container's build-from-source origin at/home/ryan/WeatherBotare consistent with a manual recovery attempt from the old pre-Ansible checkout, but I did not find a log proving that, and I should not have asserted it as fact. What is certain is that the resulting container was built from stale v1.7.0-era source and could not satisfy the database's0033, producing 13,294 restarts.So the chain was: iac-repo#277 missing on prod → compose-up timeout on slow Postgres → half-started stack → recovery from a stale checkout → permanent version/schema mismatch.
Still outstanding
weatherbot.serviceremains infailedstate (stale, from Aug 11) while the containers run healthy outside it. The unit file is now correct —WorkingDirectory=/opt/weatherbot-svc,TimeoutStartSec=300, and #277'sRestart=on-failure/RestartSec=30are all present — so this is leftover state, not a live fault. It wantssystemctl reset-failed weatherbot.servicefollowed bysystemctl start weatherbot.serviceso the unit adopts the running stack. Until then, anything monitoring unit state reports a failure that no longer exists./home/ryan/WeatherBot/docker-compose.ymlstill exists ondocker-host, andweatherbot-app:latestis still in the local image store. Both should go, or the checkout's compose project renamed, so it can never again own theweatherbotcontainers.Follow-up check on production, 2026-10-13 (read-only)
Status of the three items left outstanding on 2026-08-20:
weatherbot.servicestuck infailedactive (exited),is-failed=active. Unit carriesWorkingDirectory=/opt/weatherbot-svc,TimeoutStartSec=300,Restart=on-failure,RestartSec=30./home/ryan/WeatherBotstill exists, but it has nodocker-compose.yml(onlydocker-compose.yml.retiredand a.bak), so a baredocker compose upthere no longer does anything. The locally builtweatherbot-app:latestimage is still in the image store.Running state:
weatherbot:1.8.1from/opt/weatherbot-svc, all three containers healthy, database at0033, 5 locations, 4 channels.The unit now recovers on its own
docker-hostwas down from 2026-10-11 00:55 to 06:08 UTC (clean stop, then a new boot). The stack came back through the systemd unit with no intervention: Postgres healthy in 5 seconds, app started at 06:09:16. That is the path that failed on 2026-08-11.A three-week silence that is not an outage
sent_alertshas nothing between 2026-09-21 01:36 and 2026-10-13 05:34, which looked like a repeat. It is not:product_freshness_samplesshows the NWS alert poll succeeding 1,440 times a day, every day from 09-30 through 10-12, apart from 10-11 (1,125, matching the 5h13m host outage). Earlier days are past that table's 14-day retention.lightning_clustershas rows on 09-22, 09-27 and 10-09, so the app was writing during the part the freshness table no longer covers.So the gap is quiet weather. But telling the two apart took four queries against production, which is item 3 in practice: the same picture, with the app dead, would have looked identical from
sent_alerts.Not checked
Backup files.
weatherbot-backup.timeris active and the last run (10-13 03:34 UTC) exited 0, but/opt/weatherbot-svc/backupsis not readable by the login user, so the dumps themselves were not listed.Left to do
weatherbot-app:latestfromdocker-host(and optionally the leftover2.0.0-rc3,1.8.0,1.7.1images; rc3 is the candidate that must not be deployed)./home/ryan/WeatherBot.Detection gap (item 3) is now tracked as Rhoving/iac-repo#636. Per @rbrooks on 2026-10-13 it does not gate v2.0.0.
What remains on this issue is the production cleanup: the
weatherbot-app:latestimage and the/home/ryan/WeatherBotcheckout.Production cleanup done, 2026-10-13 16:10 UTC — closing
Requested by @rbrooks. Run through Ansible from the control node; the running stack was not touched.
Old checkout removed.
/home/ryan/WeatherBotis gone. Checked first:12d47dd, a merge commit already onmain. No unpushed commits, no stashes; the only tracked change was the already-deleteddocker-compose.yml./etc/systemd/systemand no crontab entry referenced the path..env,.env.bak.pre-v160-modernize, and two retired compose files. These were archived before deletion to/home/ryan/weatherbot-old-checkout-env-20261013.tar.gzondocker-host(mode 600, 3.2 KB). It holds the pre-Ansible secrets, so delete it once you are sure nothing in it is missing from the vault.Unused images removed (none had a container; removed without force):
weatherbot-app:latest,weatherbot:2.0.0-rc3,weatherbot:1.8.0,weatherbot:1.7.1. The three registry images can be pulled again if ever needed. Onlyweatherbot:1.8.1remains.After: all three containers
Up 2 days (healthy)on 1.8.1,weatherbot.serviceactive.Where each original item ended up
failed: resolved, and it recovered the stack unaided after the 2026-10-11 reboot.