From a5f9d13ca4aa765c0d8ff8e3129500a9b6649173 Mon Sep 17 00:00:00 2001 From: bitdeals Date: Thu, 6 Aug 2026 11:56:56 +0000 Subject: [PATCH] feat: docker image for ElectrumX, configured by environment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces lukechilds/electrumx, unmaintained for years. ElectrumX 2.0.0 from the upstream tag, rocksdb, built in a venv so only the runtime library follows into the final image. Every variable is ElectrumX's own — the entrypoint only fills defaults and rejects the one mistake that is expensive to diagnose: NET spelled the way bitcoind spells it ("test" for "testnet"), which otherwise fails deep inside a coin-class lookup. DB_ENGINE defaults to rocksdb because 2.0 made the variable required, and peer discovery is off because this image is for private indexers. Verified on testnet2 against the sibling bitcoind image: coin BitcoinRegtest, db height matching daemon height at 101, Electrum protocol answering on 50001. That run also found what the README now states — 2.x refuses to serve unless the daemon runs with both txindex=1 and txospenderindex=1. --- README.md | 131 +++++++++++++++++++++++++++++++++++++++ README.ru-RU.md | 132 ++++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 26 ++++++++ docker/Dockerfile | 74 ++++++++++++++++++++++ docker/healthy_check.sh | 12 ++++ docker/run.sh | 75 +++++++++++++++++++++++ 6 files changed, 450 insertions(+) create mode 100644 README.md create mode 100644 README.ru-RU.md create mode 100644 docker-compose.yml create mode 100644 docker/Dockerfile create mode 100755 docker/healthy_check.sh create mode 100755 docker/run.sh diff --git a/README.md b/README.md new file mode 100644 index 0000000..305e158 --- /dev/null +++ b/README.md @@ -0,0 +1,131 @@ +# Intro + +> Русская версия: [README.ru-RU.md](README.ru-RU.md) + +[ElectrumX](https://github.com/spesmilo/electrumx) is a server for the Electrum protocol. It indexes the chain from a full node and answers the queries a light client cannot answer for itself: the history and the UTXOs of an arbitrary address, and transaction broadcast. + +ElectrumX running in a docker container, configured by environment variables. + +This repository covers the docker deployment only. ElectrumX itself is installed from a pinned upstream git tag. + +# Usage + +ElectrumX needs a `bitcoind` it can reach over RPC, on the same network, not +pruned. `DAEMON_URL` is the only required variable; `NET` must name the same +chain the daemon runs. + +The container serves the Electrum protocol on **50001** (plaintext TCP) and, if +`SERVICES` asks for it, on **50002** (TLS). There is no authentication of any +kind, so keep both inside the docker network or on loopback. The **RPC** service +on `localhost:8000` is administrative and never leaves the container — the +health check is what uses it. + +## docker-compose + +```yaml +services: + electrumx: + build: + context: https://git.bitdeals.org/private/electrumx.git + dockerfile: ./docker/Dockerfile + image: registry.bitdeals.org/electrumx + environment: + - COIN=Bitcoin + - NET=regtest + - DAEMON_URL=http://CHANGE_ME:CHANGE_ME@bitcoind:18443 + - CACHE_MB=400 + ports: + - 127.0.0.1:50001:50001 # no auth — loopback only + volumes: + - electrumxdata:/data + +volumes: + electrumxdata: +``` + +## docker cli + +```sh +docker run -d \ + -e COIN=Bitcoin \ + -e NET=regtest \ + -e DAEMON_URL=http://CHANGE_ME:CHANGE_ME@bitcoind:18443 \ + -e CACHE_MB=400 \ + -p 127.0.0.1:50001:50001 \ + -v electrumxdata:/data \ + registry.bitdeals.org/electrumx +``` + +Anything after the image name is passed on to `electrumx_server`. + +## build and publish + +```sh +docker build . --file docker/Dockerfile --tag registry.bitdeals.org/electrumx +docker push registry.bitdeals.org/electrumx +``` + +A different ElectrumX release is a build argument — use a tag from +[the upstream repository](https://github.com/spesmilo/electrumx/tags): + +```sh +docker build . --file docker/Dockerfile \ + --build-arg ELECTRUMX_VERSION=2.0.0 \ + --tag registry.bitdeals.org/electrumx:2.0.0 +``` + +# Parameters + +Container images are configured using parameters passed at runtime. Every +variable is ElectrumX's own, so +[the upstream environment reference](https://electrumx-spesmilo.readthedocs.io/en/latest/environment.html) +applies unchanged; the table lists the ones this image gives a default to. + +|Parameter|Function| +|:--------|:-------| +|-p 127.0.0.1:50001|Electrum protocol over plaintext TCP. No authentication — see "Notes"| +|-p 127.0.0.1:50002|Electrum protocol over TLS. Served only when `SERVICES` includes `ssl://`| +|-v /data|Data directory: the index, and the self-signed certificate if one was generated. Losing it means indexing the chain again| +|-e DAEMON_URL|**Required.** The daemon's RPC, e.g. `http://user:password@bitcoind:8332`. Several may be given, comma-separated| +|-e COIN|Coin class. Default: `Bitcoin`| +|-e NET|Network: `mainnet`, `testnet`, `testnet4`, `signet`, `regtest` or `mutinynet`. Must match the daemon's chain. Default: `mainnet`| +|-e DB_ENGINE|Index storage. Default: `rocksdb`, the only engine whose libraries this image carries| +|-e DB_DIRECTORY|Where the index lives. Default: `/data`| +|-e SERVICES|What to serve, comma-separated. Default: `tcp://:50001,rpc://localhost:8000`. Keep an `rpc://` entry — the health check needs it| +|-e SSL_CERTFILE, -e SSL_KEYFILE|TLS certificate and key. Default: generated as a self-signed pair in the data directory when `SERVICES` includes `ssl://`| +|-e PEER_DISCOVERY|Whether to learn other public servers. Default: `off` — this image is meant for a private indexer| +|-e PEER_ANNOUNCE|Whether to announce this server to the peer network. Default: `false`| +|-e CACHE_MB|Indexing cache. Default: `1200` (ElectrumX's own). Lower it for a small chain, raise it to speed up an initial mainnet index| + +# Notes + +- **There is no authentication.** Anyone who reaches 50001 or 50002 can query + any address and broadcast transactions. Publish to loopback, or not at all + when the clients are containers on the same network. +- **An index from another image is not readable.** This one runs rocksdb; + `lukechilds/electrumx`, which it replaces in BitDeals, wrote leveldb. Point + the container at an empty volume and let it index — for regtest and testnet + this is quick, for mainnet it is a matter of days. +- **The daemon needs `txindex=1` and `txospenderindex=1`.** ElectrumX 2.x checks + `getindexinfo` before it serves anything and exits with a `RuntimeError` + naming the missing index — one at a time, so fixing `txindex` earns you the + same message about `txospenderindex`. With the sibling + [bitcoind image](https://git.bitdeals.org/private/bitcoind) that is + `BITCOIND_TXINDEX=1` and `BITCOIND_TXOSPENDERINDEX=1`; on an existing datadir + turning them on means a reindex of the daemon. +- **The daemon must not be pruned.** Indexing reads every block; a pruned node + fails partway through with an error about a missing block rather than about + the configuration. +- **`NET` is ElectrumX's spelling, not bitcoind's.** The daemon calls the + networks `main`, `test` and `regtest`; ElectrumX calls them `mainnet`, + `testnet` and `regtest`. The entrypoint rejects an unknown value, because the + pair is usually configured from one `.env` and `NET=test` would otherwise + fail deep inside a coin lookup. +- **The container turns healthy only once it is serving**, which is after it has + caught up with the daemon. An initial mainnet index takes days and the + container will be unhealthy for all of it — watch the logs instead; on regtest + it is a matter of seconds. +- **A self-signed certificate is generated only if `SERVICES` asks for TLS and + no files are supplied.** Clients then have to trust it explicitly. That is the + price of ElectrumX refusing to start when `ssl://` is requested without a + certificate, which is a surprising way to learn you added a port. diff --git a/README.ru-RU.md b/README.ru-RU.md new file mode 100644 index 0000000..ba35ba2 --- /dev/null +++ b/README.ru-RU.md @@ -0,0 +1,132 @@ +# Общие сведения + +> English version: [README.md](README.md) + +[ElectrumX](https://github.com/spesmilo/electrumx) — сервер протокола Electrum. Он индексирует цепочку по данным полного узла и отвечает на запросы, которые лёгкий клиент не может обработать сам: история и UTXO произвольного адреса, а также публикация транзакций. + +ElectrumX, работающий в docker-контейнере, настраивается переменными окружения. + +Репозиторий описывает только развёртывание в docker. Сам ElectrumX устанавливается из зафиксированного тега upstream-репозитория. + +# Использование + +ElectrumX нужен `bitcoind`, доступный по RPC, в той же сети и без обрезки +цепочки. `DAEMON_URL` — единственная обязательная переменная; `NET` должен +называть ту же сеть, в которой работает демон. + +Контейнер отдаёт протокол Electrum на порту **50001** (открытый TCP) и, если +этого требует `SERVICES`, на порту **50002** (TLS). Никакой аутентификации нет, +поэтому держите оба порта во внутренней сети docker или на loopback. +Служебный **RPC** на `localhost:8000` наружу не выходит — им пользуется проверка +состояния контейнера. + +## docker-compose + +```yaml +services: + electrumx: + build: + context: https://git.bitdeals.org/private/electrumx.git + dockerfile: ./docker/Dockerfile + image: registry.bitdeals.org/electrumx + environment: + - COIN=Bitcoin + - NET=regtest + - DAEMON_URL=http://CHANGE_ME:CHANGE_ME@bitcoind:18443 + - CACHE_MB=400 + ports: + - 127.0.0.1:50001:50001 # без аутентификации — только loopback + volumes: + - electrumxdata:/data + +volumes: + electrumxdata: +``` + +## docker cli + +```sh +docker run -d \ + -e COIN=Bitcoin \ + -e NET=regtest \ + -e DAEMON_URL=http://CHANGE_ME:CHANGE_ME@bitcoind:18443 \ + -e CACHE_MB=400 \ + -p 127.0.0.1:50001:50001 \ + -v electrumxdata:/data \ + registry.bitdeals.org/electrumx +``` + +Всё, что указано после имени образа, передаётся `electrumx_server`. + +## сборка и публикация + +```sh +docker build . --file docker/Dockerfile --tag registry.bitdeals.org/electrumx +docker push registry.bitdeals.org/electrumx +``` + +Другая версия ElectrumX задаётся аргументом сборки — берите тег из +[upstream-репозитория](https://github.com/spesmilo/electrumx/tags): + +```sh +docker build . --file docker/Dockerfile \ + --build-arg ELECTRUMX_VERSION=2.0.0 \ + --tag registry.bitdeals.org/electrumx:2.0.0 +``` + +# Параметры + +Образы контейнера настраиваются параметрами, передаваемыми при запуске. Все +переменные — собственные переменные ElectrumX, поэтому +[справочник upstream](https://electrumx-spesmilo.readthedocs.io/en/latest/environment.html) +применим без изменений; в таблице перечислены те, которым этот образ задаёт +значения по умолчанию. + +|Параметр|Назначение| +|:--------|:-------| +|-p 127.0.0.1:50001|Протокол Electrum поверх открытого TCP. Аутентификации нет — см. «Замечания»| +|-p 127.0.0.1:50002|Протокол Electrum поверх TLS. Отдаётся, только если `SERVICES` содержит `ssl://`| +|-v /data|Каталог данных: индекс и самоподписанный сертификат, если он был создан. Его потеря означает повторную индексацию цепочки| +|-e DAEMON_URL|**Обязательный.** RPC демона, например `http://user:password@bitcoind:8332`. Можно указать несколько через запятую| +|-e COIN|Класс монеты. По умолчанию: `Bitcoin`| +|-e NET|Сеть: `mainnet`, `testnet`, `testnet4`, `signet`, `regtest` или `mutinynet`. Должна совпадать с сетью демона. По умолчанию: `mainnet`| +|-e DB_ENGINE|Хранилище индекса. По умолчанию: `rocksdb` — единственный движок, библиотеки которого есть в образе| +|-e DB_DIRECTORY|Где лежит индекс. По умолчанию: `/data`| +|-e SERVICES|Что обслуживать, через запятую. По умолчанию: `tcp://:50001,rpc://localhost:8000`. Оставьте запись `rpc://` — она нужна проверке состояния| +|-e SSL_CERTFILE, -e SSL_KEYFILE|Сертификат и ключ TLS. По умолчанию: самоподписанная пара создаётся в каталоге данных, если `SERVICES` содержит `ssl://`| +|-e PEER_DISCOVERY|Искать ли другие публичные серверы. По умолчанию: `off` — образ рассчитан на приватный индексатор| +|-e PEER_ANNOUNCE|Объявлять ли себя в сети серверов. По умолчанию: `false`| +|-e CACHE_MB|Кэш индексации. По умолчанию: `1200` (значение ElectrumX). Снижайте для небольшой цепочки, повышайте для ускорения первой индексации mainnet| + +# Замечания + +- **Аутентификации нет никакой.** Любой, кто дотянулся до 50001 или 50002, может + запросить произвольный адрес и опубликовать транзакцию. Публикуйте на + loopback, а если клиенты — контейнеры той же сети, не публикуйте вовсе. +- **Индекс от другого образа не читается.** Здесь rocksdb, а + `lukechilds/electrumx`, который этот образ заменяет в BitDeals, писал leveldb. + Подключите пустой том и дайте проиндексировать заново: для regtest и testnet + это быстро, для mainnet — несколько суток. +- **Демону нужны `txindex=1` и `txospenderindex=1`.** ElectrumX 2.x перед + началом работы запрашивает `getindexinfo` и завершается с `RuntimeError`, + называя недостающий индекс — по одному за раз, так что после включения + `txindex` вы получите то же сообщение про `txospenderindex`. В соседнем + [образе bitcoind](https://git.bitdeals.org/private/bitcoind) это + `BITCOIND_TXINDEX=1` и `BITCOIND_TXOSPENDERINDEX=1`; на существующем каталоге + данных их включение означает переиндексацию демона. +- **Демон не должен быть обрезанным.** Индексация читает каждый блок; узел с + `-prune` приводит к отказу на середине с сообщением об отсутствующем блоке, а + не о неверной настройке. +- **`NET` пишется по-электрумовски, а не по-биткойновски.** Демон называет сети + `main`, `test` и `regtest`, ElectrumX — `mainnet`, `testnet` и `regtest`. + Точка входа отвергает неизвестное значение, потому что пара обычно + настраивается из одного `.env`, и `NET=test` иначе упал бы глубоко внутри + поиска класса монеты. +- **Контейнер становится healthy только когда начинает обслуживать клиентов** — + то есть после того, как догнал демона. Первая индексация mainnet занимает + несколько суток, и всё это время контейнер будет unhealthy; смотрите логи. На + regtest это секунды. +- **Самоподписанный сертификат создаётся, только если `SERVICES` требует TLS, а + файлы не заданы.** Клиентам придётся доверять ему явно. Это плата за то, что + ElectrumX отказывается стартовать при запросе `ssl://` без сертификата — не + лучший способ узнать, что вы добавили порт. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..b4f01ba --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,26 @@ +services: + electrumx: + build: + # Repository root, not ./docker: the Dockerfile copies ./docker/run.sh + # and ./docker/healthy_check.sh, and those paths are resolved against the + # build context. + context: . + dockerfile: ./docker/Dockerfile + image: registry.bitdeals.org/electrumx + environment: + - COIN=Bitcoin + # ElectrumX spelling, not bitcoind's: the daemon calls this chain + # "regtest" too, but "main"/"test" there are "mainnet"/"testnet" here. + - NET=regtest + # The daemon must run with txindex=1 and txospenderindex=1. + - DAEMON_URL=http://CHANGE_ME:CHANGE_ME@bitcoind:18443 + - CACHE_MB=400 + ports: + # No authentication of any kind — loopback only, or nothing at all when + # the clients are containers on the same network. + - 127.0.0.1:50001:50001 + volumes: + - electrumxdata:/data + +volumes: + electrumxdata: diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..cb716eb --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,74 @@ +# A container for the ElectrumX server +# +# ElectrumX indexes a bitcoind and answers the Electrum protocol: address +# history, UTXOs, transaction broadcast. BitDeals uses it for payment detection +# (ДС) and for UTXO lookup and broadcast (ГС). +# +# Installed from the upstream git tag, pinned below. The venv is built in the +# first stage with the rocksdb headers, and only the runtime library follows it +# into the final image. + +FROM python:3.14-trixie AS builder + +ARG ELECTRUMX_VERSION=2.0.0 + +WORKDIR /usr/src/app + +RUN apt-get update \ + && apt-get install -yq --no-install-suggests --no-install-recommends \ + build-essential git librocksdb-dev pkg-config \ + && rm -rf /var/lib/apt/lists/* + +RUN python -m venv venv \ + && venv/bin/pip install --no-cache-dir \ + "e_x[rocksdb] @ git+https://github.com/spesmilo/electrumx.git@${ELECTRUMX_VERSION}" + + +FROM python:3.14-slim-trixie + +# Electrum protocol, plaintext TCP. SSL is served on 50002 when SERVICES asks +# for it; the RPC (8000) stays on localhost and is what the health check uses. +EXPOSE 50001/tcp +EXPOSE 50002/tcp + +ENV USER_UID=2000 +ENV USER_GID=2000 +ENV HOME=/home/electrumx +# /data, not the home directory: this is the path the image this one replaces +# (lukechilds/electrumx) used, so a compose file keeps its volume line. The +# index itself has to be rebuilt anyway — see README, "Notes". +ENV DB_DIRECTORY=/data + +# librocksdb9.10 is the runtime half of librocksdb-dev above; openssl is only +# for the self-signed certificate the entrypoint generates on demand. +RUN apt-get update \ + && apt-get install -yq --no-install-suggests --no-install-recommends \ + librocksdb9.10 openssl gosu \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=builder /usr/src/app/venv /usr/src/app/venv +COPY ./docker/run.sh /usr/local/bin/ +COPY ./docker/healthy_check.sh /usr/local/bin/ + +# Exec-form ENTRYPOINT/HEALTHCHECK targets: a clone on a filesystem that does +# not carry the executable bit would otherwise build an unstartable image. +RUN chmod +x /usr/local/bin/run.sh /usr/local/bin/healthy_check.sh + +# electrumx_server and electrumx_rpc live in the venv; putting it on PATH keeps +# both this file and the scripts free of the full path. +ENV PATH=/usr/src/app/venv/bin:$PATH + +# groupadd, not addgroup: the slim images dropped the adduser package. +RUN groupadd --gid $USER_GID electrumx \ + && useradd --uid $USER_UID --gid $USER_GID --skel /dev/null --create-home --home-dir $HOME electrumx + +VOLUME ${DB_DIRECTORY} +WORKDIR ${HOME} + +ENTRYPOINT ["/usr/local/bin/run.sh"] + +# The server starts serving only after it has caught up with the daemon, and on +# mainnet the first index takes days. The start period covers a restart on an +# existing index, not a first run — for that, watch the logs. +HEALTHCHECK --interval=30s --timeout=10s --start-period=300s --retries=3 \ + CMD ["/usr/local/bin/healthy_check.sh"] diff --git a/docker/healthy_check.sh b/docker/healthy_check.sh new file mode 100755 index 0000000..ded43ab --- /dev/null +++ b/docker/healthy_check.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +# Healthy = the server answers its own RPC. That happens once it has caught up +# with the daemon and started serving, which is what callers care about: an +# ElectrumX still building its index accepts no Electrum sessions at all. +# +# The RPC lives on localhost:8000 by default (SERVICES). Overriding SERVICES +# without an rpc:// entry leaves nothing to ask — see README, "Notes". + +set -eu + +exec gosu electrumx electrumx_rpc getinfo > /dev/null diff --git a/docker/run.sh b/docker/run.sh new file mode 100755 index 0000000..344bbab --- /dev/null +++ b/docker/run.sh @@ -0,0 +1,75 @@ +#!/bin/sh + +set -eu + +# ElectrumX reads its own configuration from the environment, so this script +# only fills in defaults and checks the two mistakes that are expensive to +# diagnose later. Every variable below is ElectrumX's own name, not ours — +# anything documented upstream works here unchanged. + +export COIN="${COIN:-Bitcoin}" +export NET="${NET:-mainnet}" +export DB_DIRECTORY="${DB_DIRECTORY:-/data}" +# Required by ElectrumX 2.0 (it refuses to start without it). rocksdb is what +# the image ships the libraries for. +export DB_ENGINE="${DB_ENGINE:-rocksdb}" +# Plaintext Electrum protocol plus the local RPC the health check talks to. +# Add ssl://:50002 to serve TLS (see the certificate section below). +export SERVICES="${SERVICES:-tcp://:50001,rpc://localhost:8000}" +# Off by default: this image is meant for private indexers behind a known +# daemon. A node that announces itself to the public server network is a +# deliberate act — set PEER_DISCOVERY=on and PEER_ANNOUNCE=true for it. +export PEER_DISCOVERY="${PEER_DISCOVERY:-off}" +export PEER_ANNOUNCE="${PEER_ANNOUNCE:-false}" + +if [ -z "${DAEMON_URL:-}" ] +then + echo "DAEMON_URL is required, e.g. http://user:password@bitcoind:8332" >&2 + exit 1 +fi + +# NET names the coin class ElectrumX looks up, and a wrong one is not a +# harmless mismatch: the class carries the genesis hash, so a testnet index +# pointed at a regtest daemon fails at the first block with a message about +# hashes rather than about configuration. The check exists mostly for one +# specific typo — bitcoind calls these networks `main` and `test`, ElectrumX +# calls them `mainnet` and `testnet`, and the pair is usually configured from +# the same .env. +case "$NET" in + mainnet | testnet | testnet4 | signet | regtest | mutinynet) ;; + *) + echo "NET must be one of mainnet, testnet, testnet4, signet, regtest, mutinynet (got '$NET')" >&2 + exit 1 + ;; +esac + +# A named volume starts out owned by root. Non-recursive: only the empty case +# needs fixing, and a mainnet index is hundreds of gigabytes. +mkdir -p "$DB_DIRECTORY" +if [ "$(stat -c %u "$DB_DIRECTORY")" != "$USER_UID" ] +then + chown "$USER_UID:$USER_GID" "$DB_DIRECTORY" +fi + +# TLS on demand. ElectrumX requires both files as soon as SERVICES mentions +# ssl://, and refuses to start when they are missing — generating a self-signed +# pair is the difference between "works out of the box" and a startup error +# nobody expects from adding a port. Clients must trust it explicitly; a real +# certificate is supplied by pointing SSL_CERTFILE/SSL_KEYFILE elsewhere. +case "$SERVICES" in + *ssl://*) + export SSL_CERTFILE="${SSL_CERTFILE:-${DB_DIRECTORY}/electrumx.crt}" + export SSL_KEYFILE="${SSL_KEYFILE:-${DB_DIRECTORY}/electrumx.key}" + if [ ! -f "$SSL_CERTFILE" ] || [ ! -f "$SSL_KEYFILE" ] + then + echo "generating a self-signed certificate for $SSL_CERTFILE" + openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \ + -subj "/CN=electrumx" \ + -keyout "$SSL_KEYFILE" -out "$SSL_CERTFILE" 2>/dev/null + chown "$USER_UID:$USER_GID" "$SSL_CERTFILE" "$SSL_KEYFILE" + chmod 600 "$SSL_KEYFILE" + fi + ;; +esac + +exec gosu electrumx electrumx_server "$@"