forgejo-runner pods don't self-heal after k3s restart (dind containerd boltdb timeout -> CrashLoopBackOff) #313
Labels
No labels
burndown-2026-06
coherence-core
consult
headless
interactive
P0
P1
P2
P3
P4
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
coilyco-flight-deck/infrastructure#313
Loading…
Add table
Add a link
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?
Summary
The two
forgejo-runner-{0,1}StatefulSet pods cannot self-heal after a k3s restart. On 2026-06-12 ~03:11 PDT k3s restarted on kai-server, bouncing every pod in the cluster. Everything recovered on its own except the two dind-backed Actions runners, which sat inCrashLoopBackOfffor ~55 minutes until a manualkubectl delete podkicked them. CI was effectively down that whole window.Root-cause chain
runnercontainer dies withcannot ping the docker daemon ... Cannot connect to the Docker daemon at tcp://localhost:2375.dindsidecar never comes up. dind's containerd hangs onwaiting for response from boltdb openand thenfailed to start containerd: timeout waiting for containerd to start(the default ~10s containerd startup timeout).forgejo-runner-tap-writer-0(no dind sidecar) recovered fine. Only the dind runners wedged, and CrashLoopBackOff's exponential backoff kept them down well past the point the host had gone idle again.Host was healthy throughout the stuck window: disk 57% (197G free), 18Gi RAM available, load ~2.0, zero processes in I/O-wait. The contention had cleared - the pods were just stuck in backoff.
Remediation applied
Both came back
2/2 Runningwithin ~2 minutes (3 harmless early restarts each as the runner container races ahead of dind on first boot, same pattern tap-writer shows). This confirmed the crash-loop was transient, not a persistent fault.Proposed fix (the reliability gap)
A node-restart should not require a human to manually delete pods to restore CI. Options, roughly in order of preference:
runnercontainer waits for dind to be reachable instead of dying on first ping and burning into backoff.docker-libEmptyDir and StatefulSet shape are still the right design vs. a host-docker or rootless-runner approach.Also unexplained
Why k3s itself restarted at 03:11 PDT is unknown - reading
sudo journalctl -u k3srequires a password not in the agent's passwordless sudo allowlist. If k3s restarts turn out to be anything but rare, that is its own investigation and raises the priority of the self-heal fix above.Acceptance
systemctl restart k3s), bothforgejo-runner-{0,1}reach2/2 Runningwith no manual intervention.🔒 Reserved by
ward agent --harness codex— containerengineer-codex-infrastructure-313on hostkais-macbook-pro-2.localis carrying this issue (reserved 2026-07-08T17:10:10Z). Concurrentward agentruns are blocked until it finishes or the reservation goes stale (2h0m0s TTL);--forceoverrides.Do not comment on or edit this issue to steer the run while it is reserved. The engineer seeded the body once at launch and never re-reads it, so a comment or edit reaches only human readers, never the running engineer. A correction goes to a new issue, dispatched fresh — that is the only channel that reaches a run in flight. Where the forge supports it, ward locks this conversation to make that a road-block rather than a convention (ward#494).
run seed context — what this run is carrying (ward#609)
coilyco-flight-deck/infrastructure#313· branchissue-313· harnesscodex· workflowdirect-mainengineer-codex-infrastructure-313· wardv0.451.0· dispatched2026-07-08T17:10:10ZIssue body as seeded:
… (truncated to 2000 chars; full body is on this issue)
Static container doctrine and seed boilerplate are identical every run and omitted here (they ride ward v0.451.0).
— Codex, via
ward agentWARD-OUTCOME: done - forgejo-runner now waits for dind before starting the daemon, so transient post-restart containerd stalls no longer cascade into CrashLoopBackOff.
This went more cleanly once I stopped treating the containerd timeout as the thing to tune and instead fixed the runner's failure mode. The only friction was the repo's local
uvcache path, which wanted a writable Python install dir for pre-commit. Confidence is high, but if dind ever starts taking materially longer to come up again, the next place to look is the image-side startup path.