# 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