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.
15 KiB
Общие сведения
English version: README.md
PyBitmessage — клиент P2P-протокола обмена сообщениями Bitmessage, служащий для отправки шифрованных сообщений как одному адресату, так и множеству подписчиков.
Клиент PyBitmessage, работающий демоном в docker-контейнере с включённым XML-RPC API.
Репозиторий описывает только развёртывание в docker.
Использование
Контейнер создаёт детерминированные адреса Bitmessage на основе переменной BITMESSAGE_SEED_PHRASE.
Ниже — примеры, с которых удобно начать создание контейнера.
У контейнера два порта, и они не взаимозаменяемы. 8442 — XML-RPC API: он полностью управляет демоном и не имеет TLS, поэтому задайте свои учётные данные и оставьте его на loopback. 8444 — P2P-порт Bitmessage: опубликуйте его, чтобы к узлу могли подключаться другие, или не публикуйте, и тогда узел работает только на исходящих. Внутри контейнера демон слушает оба в любом случае.
docker-compose
services:
pybitmessage:
build:
context: https://git.bitdeals.org/private/bitmessage.git
dockerfile: ./docker/Dockerfile
image: registry.bitdeals.org/bitmessage
environment:
- BITMESSAGE_API_USER=CHANGE_ME
- BITMESSAGE_API_PASSWORD=CHANGE_ME
- BITMESSAGE_SEED_PHRASE=bitmessage_seed_phrase
- BITMESSAGE_SEED_ADDRESSES=1
- BITMESSAGE_TTL=172800
- BITMESSAGE_STOPRESENDINGAFTERXDAYS=60
- BITMESSAGE_MAXTOTALCONNECTIONS=40
ports:
- 127.0.0.1:8442:8442 # API — только loopback
- 8444:8444 # P2P — уберите строку, чтобы остаться на исходящих
volumes:
- bitmessage:/home/bitmessage
volumes:
bitmessage:
docker cli
docker run -d \
-e BITMESSAGE_API_USER=CHANGE_ME \
-e BITMESSAGE_API_PASSWORD=CHANGE_ME \
-e BITMESSAGE_SEED_PHRASE=bitmessage_seed_phrase \
-e BITMESSAGE_SEED_ADDRESSES=1 \
-e BITMESSAGE_TTL=172800 \
-e BITMESSAGE_STOPRESENDINGAFTERXDAYS=60 \
-e BITMESSAGE_MAXTOTALCONNECTIONS=40 \
-p 127.0.0.1:8442:8442 \
-p 8444:8444 \
-v bitmessage:/home/bitmessage \
registry.bitdeals.org/bitmessage
Параметры
Образы контейнера настраиваются параметрами, передаваемыми при запуске.
| Параметр | Назначение |
|---|---|
| -p 127.0.0.1:8442 | Порт API. Внутри контейнера демон всегда слушает 0.0.0.0, поэтому доступность определяет то, что опубликовано |
| -p 8444 | P2P-порт Bitmessage. Необязательный: без него узел всё равно подключается к пирам сам, просто к нему подключиться нельзя. Публиковать только как 8444:8444 — см. «Замечания» |
| -v /home/bitmessage | Каталог данных: keys.dat (личность, настройки) и messages.dat. Без него после каждого обновления это новый узел |
| -e BITMESSAGE_API_USER | Пользователь XML-RPC API. По умолчанию: bitmessage_api_user — измените |
| -e BITMESSAGE_API_PASSWORD | Пароль XML-RPC API. По умолчанию: bitmessage_api_password — измените, см. «Замечания» |
| -e BITMESSAGE_SEED_PHRASE | Парольная фраза для создания детерминированных адресов. По умолчанию: генерируется заново при каждом старте, то есть адреса каждый раз другие. Используется только при BITMESSAGE_SEED_ADDRESSES больше 0 |
| -e BITMESSAGE_SEED_ADDRESSES | Количество создаваемых детерминированных адресов. По умолчанию: 0 |
| -e BITMESSAGE_TTL | Срок жизни вновь отправляемых сообщений, в секундах. По умолчанию: 172800 |
| -e BITMESSAGE_STOPRESENDINGAFTERXDAYS | Прекратить повторную отправку недоставленного сообщения через X дней. По умолчанию: 30 |
| -e BITMESSAGE_APIVARIANT | Предоставляемый API: xml или json-RPC. По умолчанию: legacy |
| -e BITMESSAGE_MAXTOTALCONNECTIONS | Предел одновременных соединений, входящих и исходящих вместе (maxoutboundconnections равен 8, то есть это значение минус 8 — запас на входящие). По умолчанию: 200, штатное значение PyBitmessage — снижайте, если P2P-порт опубликован |
| -e BITMESSAGE_TRUSTED_PEER | host:port единственного пира, к которому узел подключается наружу; больше ни к кому. По умолчанию: пусто — узел выбирает пиров сам. См. «Замечания» |
| -e BITMESSAGE_SEND_OUTGOING | Подключается ли узел наружу вообще: True или False. По умолчанию: True. При False получается узел, который только принимает входящие, — центр приватного контура |
| -e BITMESSAGE_KNOWN_NODES | Список host:port через запятую; записывается в knownnodes.dat при каждом старте вместо того, что там было. По умолчанию: пусто — файл не трогается. Заодно выключает бутстрап по DNS, см. «Замечания» |
| -e BITMESSAGE_WATCHDOG | Перезапускать ли демона, потерявшего всех пиров: True или False. По умолчанию: True. См. «Замечания» |
| -e BITMESSAGE_WATCHDOG_PERIOD | Секунд между проверками числа пиров. По умолчанию: 60 |
| -e BITMESSAGE_WATCHDOG_AFTER | Сколько проверок подряд без пиров нужно, чтобы вмешаться. По умолчанию: 5 — то есть пять минут при периоде по умолчанию |
| -e BITMESSAGE_WATCHDOG_COOLDOWN | Минимальный промежуток между двумя перезапусками, в секундах. По умолчанию: 900. 0 снимает ограничение |
Замечания
-
%в пароле API использовать нельзя: он ломает собственный чтец конфигурации PyBitmessage, и любой вызов API отвечает500, притом чтоkeys.datвыглядит правильным. Остальные символы допустимы, контейнер их экранирует. -
Клиенты обязаны кодировать учётные данные — они попадают в
http://user:password@host:port/, где@,#,/и:меняют разбор URL. Скрипты этого образа кодируют, ваш клиент должен тоже. -
Контейнер становится здоровым, когда у демона появилось сетевое соединение, — на новом узле это несколько минут. Начальный период ожидания заодно покрывает стартовый
VACUUMфайлаmessages.dat. -
Демона без пиров перезапускаем, молчащего — нет. Демон, потерявший всех пиров, сам обратно дорогу не находит: он может простоять на нуле соединений сколько угодно, — а только что запущенный ищет пиров напористо и находит. Поэтому контейнер присматривает за собственным демоном: раз в
BITMESSAGE_WATCHDOG_PERIODспрашивает у API число соединений и послеBITMESSAGE_WATCHDOG_AFTERнулей подряд останавливает демона через его же API и поднимает заново. Контейнер при этом не пересоздаётся и никто снаружи не участвует, так что соседний узел сохраняет адреса и видит лишь моргнувший API.Демона, у которого API не отвечает вовсе, сторож не трогает намеренно. Это узел, застрявший на стартовом
VACUUM, которого он не может закончить; перезапуск его не лечит, а лишь обрывает следующийVACUUMна середине. Проверка здоровья показываетunhealthyв обоих случаях, но вмешиваться стоит только в первый, а во втором нужен человек.Задайте контейнеру
stop_grace_period: 90s(compose) или--stop-timeout 90(docker run). Корректное закрытиеmessages.datне укладывается в десять секунд, отпущенные Docker по умолчанию, — замерено около тринадцати на небольшой базе, и растёт вместе с файлом, — так что без этого демона убьёт SIGKILL на середине записи, ровно то, ради предотвращения чего супервизор и заведён. Сам образ это задать не может, только вызывающая сторона.BITMESSAGE_WATCHDOG=Falseвыключает правило о пирах. Супервизор остаётся в любом случае: он же корректно останавливает демона поdocker stopи поднимает того, кто умер сам, вместо контейнера, стоящего вокруг трупа. -
P2P-порт публикуется только как
8444:8444. Пирам демон сообщает порт из собственной конфигурации (portвkeys.dat), а не тот, в который вы его отобразили, поэтому8555:8444объявляет сети порт, где никого нет. Для другого порта на хосте нуженextportвkeys.dat, а его этот контейнер не подставляет. -
Свой адрес нигде не указывается. В version-сообщении демон шлёт захардкоженный
127.0.0.1, который каждый пир отбрасывает и берёт вместо него IP, увиденный на сокете, а порт — из того же сообщения. Поэтому узел с опубликованным 8444 сеть находит сама, как только он подключится наружу: ни IP, ни DNS-имя хоста задавать негде. Единственное исключение — скрытый сервис Tor, которому нужен явныйonionhostname. -
Приватному контуру нужны назначенные пиры и звезда, в которую их назначать. PyBitmessage отвергает кандидата, чья сетевая группа (для IPv4 — /16) уже представлена среди его исходящих соединений. Все контейнеры одного compose-проекта живут в одной /16, поэтому каждый узел удерживает ровно одно исходящее соединение — со случайно выбранным пиром, и контур чаще всего распадается на компоненты. Задайте одному узлу
BITMESSAGE_SEND_OUTGOING=False, и он станет хабом, который только принимает (проверка смотрит лишь на исходящие, входящие она не ограничивает), остальные направьте на него черезBITMESSAGE_TRUSTED_PEER=<ip-хаба>:8444— объекты пойдут спица → хаб → спицы. Адреса задавайте IP, а не именами сервисов: проверка разбирает хост как IP, да и обменaddrмежду узлами оперирует IP. -
BITMESSAGE_KNOWN_NODES— то, что делает приватный контур приватным. Узел, у которого вknownnodes.datесть пир не из встроенного списка PyBitmessage, перестаёт спрашивать адреса уbootstrap8080.bitmessage.org; без этого даже узел с назначенным пиром при каждом старте резолвит публичный бутстрап-хост. Файл переписывается на каждом старте, поэтому во что узел верит при загрузке, определяет переменная, а не история контейнера. -
Публикация 8444 — осознанный компромисс по безопасности. Демон работает на Python 2 и уже разбирает недоверенные данные от своих исходящих пиров, так что открытый порт эту поверхность не создаёт — он меняет то, кто может подключиться, когда и сколько их. Разбор до рукопожатия становится доступен кому угодно, а реальный риск — исчерпание ресурсов, а не выполнение кода. Держите
BITMESSAGE_MAXTOTALCONNECTIONSнизким и ограничьте контейнер по памяти и CPU.