Build docker image and push to registry.bitdeals.org / main-build-job (push) Successful in 7m12s
A PyBitmessage daemon that has dropped to zero network connections does not find its way back. It can sit there for days — testnet1 did, and every BitDeals deal completed on that stand since 2026-08-30 left its escrow unspent, because the guarantor never received a CH3 the node could no longer publish. A daemon that has just started, by contrast, dials hard and reconnects within seconds. The cure was already known; what was missing was anything to notice and apply it. Docker will not: it reacts to a process exiting, never to a healthcheck, and health-driven restarts exist only under Swarm. Doing it from outside means handing a container the docker socket, which is root on the host and a poor trade for a relay with a published port. So the container supervises itself. `run.sh` no longer ends at `exec gosu bitmessage pybitmessage -d`. That made the daemon PID 1, and it is a bad PID 1: daemonize() double-forks and parks the grandfather in `while True: time.sleep(1)`, the final child SIGTERMs it to say "ready", and PID 1 drops that signal for want of a handler. The grandfather slept for ever; `docker stop` therefore reached the real daemon only as the SIGKILL ten seconds later, cutting a startup VACUUM in half — the way a node gets trapped retrying one it can never finish; and a daemon that died on its own left the container Up around a corpse, because what PID 1 was doing had nothing to do with whether the daemon lived. Away from PID 1 that grandfather does die on the ready signal — measured here: the start call returns at once with status 143 and leaves exactly one pybitmessage process behind. That makes starting the daemon an ordinary blocking call, and the supervisor ordinary shell: a trap that stops the daemon through its own API, a restart when it is gone, and the peer rule. What the supervisor does not do is act on a daemon whose API is not answering at all. That is the trapped-VACUUM node; a restart does not cure it and cuts the next VACUUM short as well. watchdog.py reports that case as its own exit code so the loop can leave it to a person. The healthcheck is untouched: it reports, and does not act. Four settings, on by default: BITMESSAGE_WATCHDOG, and _PERIOD, _AFTER, _COOLDOWN. All validated at start, where a typo is visible, rather than hours later as a supervisor that spins or one that never acts. Callers must raise the stop grace period — 90s in compose, or --stop-timeout 90. A clean shutdown took 17.5 s on a small database and grows with it, so under Docker's default ten the daemon is killed mid-write anyway and the supervisor buys nothing. An image cannot set this for itself.