RTL/docker
saubyk 4e2b7ca978 Rebuild the regtest dev fixture: bitcoind + 3 LND nodes + RTL
The setup in docker/ has been unable to start since Feb 2021. The boltz
service added in f817ae39 references BOLTZ_* variables that are not in .env
(so its ports render as ":" and compose rejects the file with "invalid
proto"), a ./boltz build context that has never existed in the repo, and
boltz_data/boltz_shared volumes that are never declared. Because compose
validates the whole project up front, this broke every service: "docker
compose up -d bitcoind", the first command in the README, failed too.

Rather than repair a five-year-old file pinned to bitcoind 0.19.0 and lnd
0.12.0-beta, this replaces it.

What changed:

- bitcoind 30.0 and 3x lnd 0.20.0-beta, using Polar's images. They are
  multi-arch, so nothing is built locally and this works on arm64. The old
  setup built bitcoind and lnd from local Dockerfiles.
- Three nodes, not one: alice -> bob -> carol. bob forwards, so RTL's
  routing and forwarding screens have data. Two nodes leave them empty.
- scripts/seed.sh funds the nodes, opens channels, and makes payments with
  fixed amounts. A fresh run reproduces identical state, so screenshots
  taken before and after a change differ only by the change. It is
  deliberately not idempotent and refuses to run against a seeded network,
  since re-running would double-fund it.
- rtl/RTL-Config.regtest.json configures all three nodes. RTL rewrites its
  config on startup, so an init container stages a copy into a volume: a
  read-only bind mount makes RTL exit with EROFS, and a writable one would
  let RTL modify a tracked file. It is not named RTL-Config.json because
  .gitignore matches that bare name at any depth.
- bin/ln-cli now takes a node name and passes --lnddir=/home/lnd/.lnd,
  because 'docker compose exec' lands as root while lnd's datadir is under
  /home/lnd. Both helpers use compose v2.
- README rewritten to match.

Boltz, Core Lightning and Eclair are left out of this pass. Polar publishes
multi-arch clightning and eclair images, so adding them later needs compose
services, config entries and seed adapters, but no image building.

Verified from a clean 'down -v': all nodes sync, channels go active, 5/5
payments route through bob, bob records 5 forwards, and RTL serves the UI
with all three nodes configured. Two independent from-scratch runs produced
identical balances.

The old bitcoind/ and lnd/ build contexts are now unreferenced but left in
place for a follow-up.
2026-07-15 23:26:30 -07:00
..
bin Rebuild the regtest dev fixture: bitcoind + 3 LND nodes + RTL 2026-07-15 23:26:30 -07:00
bitcoind Docker regtest setup (#108) 2019-04-14 19:00:20 -04:00
lnd Docker regtest setup (#108) 2019-04-14 19:00:20 -04:00
rtl Rebuild the regtest dev fixture: bitcoind + 3 LND nodes + RTL 2026-07-15 23:26:30 -07:00
scripts Rebuild the regtest dev fixture: bitcoind + 3 LND nodes + RTL 2026-07-15 23:26:30 -07:00
.env Rebuild the regtest dev fixture: bitcoind + 3 LND nodes + RTL 2026-07-15 23:26:30 -07:00
docker-compose.yml Rebuild the regtest dev fixture: bitcoind + 3 LND nodes + RTL 2026-07-15 23:26:30 -07:00
README.md Rebuild the regtest dev fixture: bitcoind + 3 LND nodes + RTL 2026-07-15 23:26:30 -07:00

RTL regtest dev fixture

NOT suitable for production. Development only. Every credential here is throwaway.

A self-contained regtest network for developing and testing RTL: bitcoind, three LND nodes, and RTL wired to all three.

alice --[ 5,000,000 sat ]--> bob --[ 3,000,000 sat ]--> carol

bob sits in the middle so it accrues forwarding history, which is what gives RTL's routing screens something to show. Two nodes would leave them empty.

Node images come from Polar, which publishes multi-arch (amd64 + arm64) builds. Nothing is built locally, so this works on Apple Silicon.

Requirements

Docker with Compose v2 (docker compose, not docker-compose).

Quick start

From this directory:

docker compose up -d          # bitcoind, alice, bob, carol, rtl
./scripts/seed.sh             # fund, connect, open channels, make payments

Then open http://localhost:3000 — password password. All three nodes appear in the node switcher.

Tear down, discarding all state:

docker compose down -v

What the seed creates

On-chain 10,000,000 sats per node, confirmed
Channels alice→bob 5,000,000 sats · bob→carol 3,000,000 sats (1,000,000 pushed each)
Routed payments 5 × alice→carol via bob (10k, 25k, 50k, 75k, 100k sats)
Direct payments 2 × alice→bob (5k, 15k sats)
Open invoices 2 unpaid on carol (20k, 40k sats)
Personas alice + bob OPERATOR, carol MERCHANT

Determinism

Every amount and payment in scripts/seed.sh is fixed. A fresh run always produces identical state, so screenshots taken before and after a change differ only by the change. Do not introduce randomness.

The seed is deterministic but deliberately not idempotent — running it twice would fund every node again and open a second set of channels. It refuses to run against an already-seeded network. To start over:

docker compose down -v && docker compose up -d && ./scripts/seed.sh

Helpers

bin/b-cli getblockcount                    # bitcoin-cli
bin/b-cli -rpcwallet=rtldev getbalance
bin/ln-cli alice getinfo                   # lncli, node name required
bin/ln-cli bob listchannels
bin/ln-cli bob fwdinghistory               # forwarding history

Logs:

docker compose logs -f rtl
docker compose logs alice

Notes and gotchas

RTL's config. rtl/RTL-Config.regtest.json is the tracked template. RTL rewrites its config on startup, so an init container copies it into a volume rather than bind-mounting it — a read-only mount makes RTL exit with EROFS, and a writable one would let RTL modify a version-controlled file. The name is not RTL-Config.json because .gitignore matches that bare filename at any depth.

lncli needs --lnddir=/home/lnd/.lnd. docker compose exec lands as root, whose HOME is /root, but lnd's datadir is /home/lnd/.lnd. bin/ln-cli handles this.

Changing bitcoind credentials. docker-compose.yml carries an -rpcauth hash for the BITCOIN_RPC_USER / BITCOIN_RPC_PASSWORD in .env. Changing them there is not enough; regenerate the hash:

python3 - <<'EOF'
import hmac, hashlib
user, password, salt = "rtldev", "rtldev", "8a1f2c3d4e5b6a7c8d9e0f1a2b3c4d5e"
print(f"{user}:{salt}${hmac.new(salt.encode(), password.encode(), hashlib.sha256).hexdigest()}")
EOF

In docker-compose.yml the $ must be written $$ to escape Compose interpolation.

Payments right after channel open will fail. The channel graph has to reach alice before she can route to carol. The seed waits for this; anything you script yourself should too.

Not included

Core Lightning and Eclair nodes, and the Boltz swap service. Polar publishes multi-arch clightning and eclair images, so adding them means compose services, RTL config entries, and seeding adapters — no image building.