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.
88 lines
3.3 KiB
Docker
88 lines
3.3 KiB
Docker
# A container for PyBitmessage daemon
|
|
FROM ubuntu:bionic
|
|
|
|
SHELL ["/bin/bash", "-exo", "pipefail", "-c"]
|
|
|
|
# Install dependencies. update and install share a layer on purpose: split
|
|
# across two, a cached update feeds install package lists that may be months
|
|
# stale, and the install then fails or pulls something unintended.
|
|
RUN apt-get update \
|
|
&& apt-get install -yq --no-install-suggests --no-install-recommends \
|
|
build-essential libcap-dev libssl-dev \
|
|
python-all-dev python-msgpack python-pip python-setuptools \
|
|
git
|
|
|
|
## Do not use cache when building next layers of the image.
|
|
ARG NOCACHE=0
|
|
|
|
WORKDIR /root/PyBitmessage
|
|
RUN git clone https://github.com/Bitmessage/PyBitmessage .
|
|
|
|
# Install
|
|
RUN pip2 install jsonrpclib .
|
|
|
|
# Raise the SQL-thread startup timeout from the stock 60 s.
|
|
#
|
|
# PyBitmessage kills the daemon outright if the SQL thread is not ready within
|
|
# sql_timeout seconds (class_objectProcessor.py -> os._exit(1)). The startup
|
|
# VACUUM of a messages.dat that has grown to a few hundred MB does not fit in
|
|
# 60 s, and since the process dies mid-VACUUM lastvacuumtime is never updated,
|
|
# so every later start retries the same doomed VACUUM and the node never comes
|
|
# back. Measured: 26 s for a 264 MB database on an idle host, and the last
|
|
# start that did survive used 36 s of the 60.
|
|
#
|
|
# The greps are load-bearing: the clone above is unpinned, so if upstream ever
|
|
# moves or renames the constant, a silent no-op sed would ship an image that
|
|
# looks fixed and is not. Fail the build instead. The .pyc is refreshed because
|
|
# at runtime /usr/local is root-owned while the daemon runs as bitmessage, so a
|
|
# stale one can only be recompiled to memory on every start.
|
|
RUN f=/usr/local/lib/python2.7/dist-packages/pybitmessage/helper_sql.py \
|
|
&& grep -q '^sql_timeout = 60$' "$f" \
|
|
&& sed -i 's/^sql_timeout = 60$/sql_timeout = 600/' "$f" \
|
|
&& grep -q '^sql_timeout = 600$' "$f" \
|
|
&& rm -f "${f}c" \
|
|
&& python -c "import py_compile; py_compile.compile('$f')"
|
|
|
|
FROM ubuntu:bionic
|
|
|
|
# 8442 is the XML-RPC API (keep it on loopback), 8444 the Bitmessage P2P port.
|
|
# The daemon listens on both regardless; publishing 8444 is what makes the node
|
|
# reachable for inbound peers.
|
|
EXPOSE 8442/tcp
|
|
EXPOSE 8444/tcp
|
|
|
|
ENV USER_UID=2000
|
|
ENV USER_GID=2000
|
|
ENV HOME=/home/bitmessage
|
|
ENV BITMESSAGE_HOME=${HOME}
|
|
|
|
COPY --from=0 /usr/local/ /usr/local/
|
|
COPY ./docker/healthy_check.py /usr/local/bin/
|
|
COPY ./docker/seed_addr_gen.py /usr/local/bin/
|
|
COPY ./docker/watchdog.py /usr/local/bin/
|
|
COPY ./docker/run.sh /usr/local/bin/
|
|
|
|
# Install dependencies
|
|
RUN apt-get update \
|
|
&& apt-get install -yq --no-install-suggests --no-install-recommends python-setuptools moreutils gosu \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
# Create a user
|
|
RUN addgroup --gid $USER_GID bitmessage ;\
|
|
useradd --uid $USER_UID --gid $USER_GID --skel /dev/null --create-home --home-dir $HOME bitmessage
|
|
|
|
WORKDIR ${HOME}
|
|
|
|
# Generate default config
|
|
RUN su bitmessage -c "pybitmessage -t"
|
|
|
|
CMD ["sh", "/usr/local/bin/run.sh"]
|
|
|
|
## Check PyBitmessage active network connections.
|
|
## The start period covers the startup VACUUM of messages.dat, which takes tens
|
|
## of seconds once the database reaches a few hundred MB; without it the
|
|
## container reports unhealthy for that whole window on every restart.
|
|
HEALTHCHECK --retries=0 --interval=15s --start-period=180s \
|
|
CMD ["python", "/usr/local/bin/healthy_check.py"]
|
|
|