RTL/docker/docker-compose.yml
saubyk 6e55059fe2 Add an Eclair node to the regtest docker fixture
Completes backend coverage of RTL's three implementations in the docker/
dev fixture: an eclair node (polarlightning/eclair 0.13.1) joins the three
LND nodes and the CLN node, wired to RTL over its HTTP API with basic auth,
and the seed opens an eclair->bob channel (3.5M sats, 1M pushed), sends two
direct payments and leaves one open invoice.

Non-obvious plumbing this needed:

- polarlightning/eclair instead of acinq/eclair: the official image is
  amd64-only (useless on Apple Silicon) and its newest versioned tag is
  years stale; Polar builds the same ACINQ source multi-arch.
- Eclair has no on-chain wallet of its own -- it drives a bitcoind wallet.
  A new eclair-wallet-init container creates a dedicated "eclair" wallet
  before the node starts; without it eclair attaches to "the default
  loaded wallet", i.e. the rtldev mining wallet.
- bitcoind now also publishes a zmqpubhashblock endpoint (28336): eclair's
  bitcoind.zmqblock consumes the hashblock topic, not the rawblock one LND
  uses. Wired to rawblock, eclair never sees new blocks and channels hang
  in WAIT_FOR_FUNDING_CONFIRMED.
- Eclair confirms channels at 8 blocks (channel.min-depth-blocks), not 6,
  and 'open' returns before the funding tx is broadcast -- the seed waits
  for the mempool and mines 8 blocks for this channel.

Adds a bin/e-cli helper (eclair-cli with the API password), updates the
README, and verified end-to-end: seed completes, the channel reaches
NORMAL, both payments settle, and RTL's /rtl/api/ecl endpoints return the
node, channel and invoice data.
2026-07-19 22:01:23 -07:00

313 lines
12 KiB
YAML

# Regtest dev fixture for RTL: bitcoind + 3 LND nodes + RTL.
#
# NOT suitable for production. All credentials are throwaway.
#
# Topology is alice -> bob -> carol, so bob forwards payments and RTL's
# routing/forwarding screens have data in them. See README.md.
#
# Node images come from Polar (https://lightningpolar.com), which publishes
# multi-arch (amd64 + arm64) builds. Nothing is built locally.
volumes:
bitcoind_data:
alice_data:
bob_data:
carol_data:
cln_data:
eclair_data:
rtl_db:
rtl_config:
x-lnd: &lnd
image: polarlightning/lnd:0.20.0-beta
restart: unless-stopped
depends_on:
- bitcoind
services:
bitcoind:
container_name: ${COMPOSE_PROJECT_NAME}_bitcoind
image: polarlightning/bitcoind:30.0
restart: unless-stopped
command:
- bitcoind
- -server=1
- -regtest=1
# rpcauth hash for ${BITCOIN_RPC_USER}/${BITCOIN_RPC_PASSWORD}. '$$' escapes
# compose interpolation and reaches bitcoind as a single '$'.
- -rpcauth=rtldev:8a1f2c3d4e5b6a7c8d9e0f1a2b3c4d5e$$010df4b32c5e9a556cba1857eb5865990c983d8a56dadc0fdbf457cf90073c6c
- -zmqpubrawblock=tcp://0.0.0.0:${BITCOIN_ZMQ_BLOCK_PORT}
- -zmqpubrawtx=tcp://0.0.0.0:${BITCOIN_ZMQ_TX_PORT}
# eclair's zmqblock consumes the hashblock topic, not rawblock (LND uses
# rawblock/rawtx above); without this endpoint eclair never sees new
# blocks and channels stay in WAIT_FOR_FUNDING_CONFIRMED forever.
- -zmqpubhashblock=tcp://0.0.0.0:${BITCOIN_ZMQ_HASHBLOCK_PORT:-28336}
- -txindex=1
- -dnsseed=0
- -rpcbind=0.0.0.0
- -rpcallowip=0.0.0.0/0
- -rpcport=${BITCOIN_RPC_PORT}
- -listen=1
- -listenonion=0
- -fallbackfee=0.0002
ports:
- "${BITCOIN_RPC_PORT}:${BITCOIN_RPC_PORT}"
volumes:
- bitcoind_data:/home/bitcoin/.bitcoin
# --alias / --externalip / --tlsextradomain are per-node on purpose: the alias
# is what RTL displays, and the extradomain must match the hostname RTL dials
# (https://alice:8080) or TLS validation fails.
alice:
<<: *lnd
container_name: ${COMPOSE_PROJECT_NAME}_alice
command:
- lnd
- --noseedbackup
- --trickledelay=5000
- --alias=alice
- --externalip=alice
- --tlsextradomain=alice
- --tlsextradomain=${COMPOSE_PROJECT_NAME}_alice
- --tlsextradomain=host.docker.internal
- --listen=0.0.0.0:${LIGHTNING_P2P_PORT}
- --rpclisten=0.0.0.0:${LIGHTNING_RPC_PORT}
- --restlisten=0.0.0.0:${LIGHTNING_REST_PORT}
- --bitcoin.active
- --bitcoin.regtest
- --bitcoin.node=bitcoind
- --bitcoind.rpchost=${BITCOIN_HOST}:${BITCOIN_RPC_PORT}
- --bitcoind.rpcuser=${BITCOIN_RPC_USER}
- --bitcoind.rpcpass=${BITCOIN_RPC_PASSWORD}
- --bitcoind.zmqpubrawblock=tcp://${BITCOIN_HOST}:${BITCOIN_ZMQ_BLOCK_PORT}
- --bitcoind.zmqpubrawtx=tcp://${BITCOIN_HOST}:${BITCOIN_ZMQ_TX_PORT}
- --accept-keysend
- --accept-amp
ports:
- "${ALICE_REST_PORT}:${LIGHTNING_REST_PORT}"
volumes:
- alice_data:/home/lnd/.lnd
bob:
<<: *lnd
container_name: ${COMPOSE_PROJECT_NAME}_bob
command:
- lnd
- --noseedbackup
- --trickledelay=5000
- --alias=bob
- --externalip=bob
- --tlsextradomain=bob
- --tlsextradomain=${COMPOSE_PROJECT_NAME}_bob
- --tlsextradomain=host.docker.internal
- --listen=0.0.0.0:${LIGHTNING_P2P_PORT}
- --rpclisten=0.0.0.0:${LIGHTNING_RPC_PORT}
- --restlisten=0.0.0.0:${LIGHTNING_REST_PORT}
- --bitcoin.active
- --bitcoin.regtest
- --bitcoin.node=bitcoind
- --bitcoind.rpchost=${BITCOIN_HOST}:${BITCOIN_RPC_PORT}
- --bitcoind.rpcuser=${BITCOIN_RPC_USER}
- --bitcoind.rpcpass=${BITCOIN_RPC_PASSWORD}
- --bitcoind.zmqpubrawblock=tcp://${BITCOIN_HOST}:${BITCOIN_ZMQ_BLOCK_PORT}
- --bitcoind.zmqpubrawtx=tcp://${BITCOIN_HOST}:${BITCOIN_ZMQ_TX_PORT}
- --accept-keysend
- --accept-amp
ports:
- "${BOB_REST_PORT}:${LIGHTNING_REST_PORT}"
volumes:
- bob_data:/home/lnd/.lnd
carol:
<<: *lnd
container_name: ${COMPOSE_PROJECT_NAME}_carol
command:
- lnd
- --noseedbackup
- --trickledelay=5000
- --alias=carol
- --externalip=carol
- --tlsextradomain=carol
- --tlsextradomain=${COMPOSE_PROJECT_NAME}_carol
- --tlsextradomain=host.docker.internal
- --listen=0.0.0.0:${LIGHTNING_P2P_PORT}
- --rpclisten=0.0.0.0:${LIGHTNING_RPC_PORT}
- --restlisten=0.0.0.0:${LIGHTNING_REST_PORT}
- --bitcoin.active
- --bitcoin.regtest
- --bitcoin.node=bitcoind
- --bitcoind.rpchost=${BITCOIN_HOST}:${BITCOIN_RPC_PORT}
- --bitcoind.rpcuser=${BITCOIN_RPC_USER}
- --bitcoind.rpcpass=${BITCOIN_RPC_PASSWORD}
- --bitcoind.zmqpubrawblock=tcp://${BITCOIN_HOST}:${BITCOIN_ZMQ_BLOCK_PORT}
- --bitcoind.zmqpubrawtx=tcp://${BITCOIN_HOST}:${BITCOIN_ZMQ_TX_PORT}
- --accept-keysend
- --accept-amp
ports:
- "${CAROL_REST_PORT}:${LIGHTNING_REST_PORT}"
volumes:
- carol_data:/home/lnd/.lnd
# Core Lightning node. Unlike the LND nodes it talks to RTL over clnrest (the
# built-in REST plugin) using rune auth, so it needs --clnrest-* options and a
# rune written where RTL can read it. create-rune.sh (run from the healthcheck)
# creates the rune and writes it to /root/.lightning/rtl.rune (RTL mounts that
# read-only); the healthcheck is unhealthy until it exists, so rtl waits for it.
# --clnrest-host=0.0.0.0 is required so the rtl container can reach it; the default
# 127.0.0.1 would only be reachable from inside this container. Protocol stays https
# (clnrest default, self-signed) — RTL connects with rejectUnauthorized:false.
cln:
image: elementsproject/lightningd:v25.09
container_name: ${COMPOSE_PROJECT_NAME}_cln
restart: unless-stopped
depends_on:
- bitcoind
environment:
LIGHTNINGD_NETWORK: regtest
command:
- --alias=cln
- --bitcoin-rpcconnect=${BITCOIN_HOST}
- --bitcoin-rpcport=${BITCOIN_RPC_PORT}
- --bitcoin-rpcuser=${BITCOIN_RPC_USER}
- --bitcoin-rpcpassword=${BITCOIN_RPC_PASSWORD}
- --bitcoin-retry-timeout=3600
- --addr=0.0.0.0:9735
- --announce-addr=cln:9735
- --large-channels
- --clnrest-host=0.0.0.0
- --clnrest-port=3010
ports:
- "${CLN_REST_PORT:-3010}:3010"
volumes:
- cln_data:/root/.lightning
- ./cln/create-rune.sh:/opt/create-rune.sh:ro
healthcheck:
# create-rune.sh ensures the rune exists (idempotent, one quick attempt) and
# this reports healthy only once it does. Driving it from the healthcheck — which
# retries on its interval — means a transient RPC-startup race self-heals instead
# of a one-shot script permanently wedging the stack. rtl waits on this via
# depends_on: condition: service_healthy before reading the rune at startup.
test: ["CMD-SHELL", "sh /opt/create-rune.sh && test -f /root/.lightning/rtl.rune"]
interval: 5s
timeout: 10s
retries: 40
# Eclair has no on-chain wallet of its own -- it drives a bitcoind wallet over
# RPC. Without a dedicated wallet it would grab "the default loaded wallet",
# which here is the rtldev mining wallet, and eclair's on-chain balance would
# show the miner's coins. This init container creates (or reloads) a wallet
# named "eclair" before the eclair node starts; load_on_startup survives
# bitcoind restarts.
eclair-wallet-init:
container_name: ${COMPOSE_PROJECT_NAME}_eclair_wallet_init
image: polarlightning/bitcoind:30.0
depends_on:
- bitcoind
entrypoint: ["/bin/sh", "-c"]
command:
- |
bcli() { bitcoin-cli -regtest -rpcconnect=${BITCOIN_HOST} -rpcport=${BITCOIN_RPC_PORT} -rpcuser=${BITCOIN_RPC_USER} -rpcpassword=${BITCOIN_RPC_PASSWORD} "$$@"; }
i=0
until bcli getblockchaininfo >/dev/null 2>&1; do
i=$$((i+1)); [ "$$i" -ge 60 ] && echo "bitcoind never came up" && exit 1
sleep 1
done
bcli -named createwallet wallet_name=eclair load_on_startup=true >/dev/null 2>&1 \
|| bcli loadwallet eclair true >/dev/null 2>&1 \
|| true
bcli -rpcwallet=eclair getwalletinfo >/dev/null
echo "eclair wallet ready"
# Eclair node. Talks to RTL over its HTTP API with basic auth (the
# lnApiPassword in RTL's config). The polarlightning image is used because
# acinq/eclair on Docker Hub is amd64-only and its newest versioned tag is
# years stale; Polar builds the same ACINQ source multi-arch (amd64 + arm64).
# The image entrypoint translates each --key=value into -Declair.key=value.
# The entrypoint also overrides server.public-ips.0 with the container IP, but
# the arg must still be present for that substitution to happen.
eclair:
image: polarlightning/eclair:0.13.1
container_name: ${COMPOSE_PROJECT_NAME}_eclair
restart: unless-stopped
depends_on:
bitcoind:
condition: service_started
eclair-wallet-init:
condition: service_completed_successfully
command:
- polar-eclair
- --node-alias=eclair
- --server.public-ips.0=eclair
- --server.port=9735
- --api.enabled=true
- --api.binding-ip=0.0.0.0
- --api.port=8080
- --api.password=${ECLAIR_API_PASSWORD:-rtldev}
- --chain=regtest
- --bitcoind.host=${BITCOIN_HOST}
- --bitcoind.rpcport=${BITCOIN_RPC_PORT}
- --bitcoind.rpcuser=${BITCOIN_RPC_USER}
- --bitcoind.rpcpassword=${BITCOIN_RPC_PASSWORD}
# zmqblock must be bitcoind's *hashblock* endpoint (eclair subscribes to
# that topic); pointing it at the rawblock endpoint the LND nodes use
# leaves eclair blind to new blocks.
- --bitcoind.zmqblock=tcp://${BITCOIN_HOST}:${BITCOIN_ZMQ_HASHBLOCK_PORT:-28336}
- --bitcoind.zmqtx=tcp://${BITCOIN_HOST}:${BITCOIN_ZMQ_TX_PORT}
- --bitcoind.wallet=eclair
- --datadir=/home/eclair/.eclair
- --printToConsole=true
# Regtest feerates are far from mainnet estimates; without a wide
# tolerance eclair closes channels over feerate disagreements.
- --on-chain-fees.feerate-tolerance.ratio-low=0.00001
- --on-chain-fees.feerate-tolerance.ratio-high=10000.0
ports:
- "${ECLAIR_REST_PORT:-8281}:8080"
volumes:
- eclair_data:/home/eclair
# RTL rewrites its config file on startup, so it cannot be given the tracked
# template directly: a bind mount would either be read-only (RTL exits with
# EROFS) or would let RTL scribble into a version-controlled file. Instead the
# template is copied into a volume that 'down -v' discards, which keeps the
# source pristine and every run starting from identical config.
rtl-config-init:
container_name: ${COMPOSE_PROJECT_NAME}_rtl_config_init
image: busybox:1.36
command: >
sh -c "cp /template/RTL-Config.regtest.json /config/RTL-Config.json &&
chmod 644 /config/RTL-Config.json &&
echo 'config staged'"
volumes:
- ./rtl/RTL-Config.regtest.json:/template/RTL-Config.regtest.json:ro
- rtl_config:/config
rtl:
container_name: ${COMPOSE_PROJECT_NAME}_rtl
# Defaults to the published image; override with RTL_IMAGE (e.g. a locally built
# branch image) to test unreleased changes: RTL_IMAGE=rtl:pr1625 docker compose up.
image: ${RTL_IMAGE:-shahanafarooqui/rtl:v0.15.8}
restart: unless-stopped
depends_on:
rtl-config-init:
condition: service_completed_successfully
alice:
condition: service_started
bob:
condition: service_started
carol:
condition: service_started
cln:
condition: service_healthy
eclair:
condition: service_started
ports:
- "${RTL_PORT}:${RTL_PORT}"
environment:
RTL_CONFIG_PATH: /RTL/config
volumes:
- rtl_config:/RTL/config
- alice_data:/lnd/alice:ro
- bob_data:/lnd/bob:ro
- carol_data:/lnd/carol:ro
- cln_data:/cln:ro
- rtl_db:/RTL/database