feat(docker): streamline regtest dev environment

- Fix EXTENSION_SERVICE_URL default to localhost (was hardcoded LAN IP)
- Fix strfry healthcheck (BusyBox nc lacks -z, use /proc/net/tcp)
- Add LP healthcheck + depends_on strfry
- Add regtest-bootstrap.sh: funds LND, opens channels, creates LP app
- Add regtest.sh: single management script (up/down/reset/status/logs/lncli/bitcoin)
- Minimal start uses only 3 containers (bitcoind, lnd-1, lnd-4) instead of 17

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Patrick Mulligan 2026-03-10 01:31:51 -04:00
commit 0395aab3d1
3 changed files with 440 additions and 9 deletions

View file

@ -4,16 +4,22 @@
# environment at ~/dev/local/docker/regtest
#
# Usage:
# 1. Start the regtest environment:
# cd ~/dev/local/docker/regtest && ./start-regtest
# 1. Start regtest (minimal — only what LP needs):
# cd ~/dev/local/docker/regtest && docker compose up -d bitcoind lnd-1 lnd-4
#
# 2. Start lamassu services:
# cd lamassu-next/docker && docker compose -f docker-compose.regtest.yml up -d
# cd docker && docker compose -f docker-compose.regtest.yml up -d
#
# 3. Configure apps/machine/.env:
# 3. Bootstrap (funds LND, opens channels, creates LP app):
# cd docker && ./regtest-bootstrap.sh
#
# 4. Configure apps/machine/.env:
# VITE_RELAY_URL=ws://localhost:7777
# VITE_EXTENSION_API_URL=http://localhost:1777
#
# Phone wallet testing (LNURL callbacks need LAN IP):
# HOST_IP=192.168.1.100 docker compose -f docker-compose.regtest.yml up -d
#
# Services:
# - strfry: Private Nostr relay (port 7777)
# - lightning-pub: Nostr-native Lightning account system (port 1776)
@ -37,10 +43,11 @@ services:
soft: 524288
hard: 524288
healthcheck:
test: ['CMD', 'nc', '-z', 'localhost', '7777']
interval: 10s
timeout: 5s
retries: 5
# Check port 7777 (0x1E61) is listening via procfs — BusyBox nc lacks -z flag
test: ['CMD-SHELL', 'grep -q ":1E61 " /proc/net/tcp']
interval: 5s
timeout: 3s
retries: 10
restart: unless-stopped
networks:
- regtest
@ -76,7 +83,16 @@ services:
- ADMIN_TOKEN=lamassu-dev-admin-token
# Extension HTTP API URL (for LNURL callbacks from external wallets)
# Use HOST_IP env var for your machine's LAN IP (required for phone wallets)
- EXTENSION_SERVICE_URL=http://${HOST_IP:-192.168.1.190}:1777
- EXTENSION_SERVICE_URL=http://${HOST_IP:-localhost}:1777
depends_on:
strfry:
condition: service_healthy
healthcheck:
test: ['CMD-SHELL', 'curl -so /dev/null -w "%{http_code}" http://localhost:1776 | grep -q .']
interval: 10s
timeout: 5s
retries: 30
start_period: 30s
restart: unless-stopped
networks:
- regtest

273
docker/regtest-bootstrap.sh Executable file
View file

@ -0,0 +1,273 @@
#!/usr/bin/env bash
# regtest-bootstrap.sh — Bootstrap a working LP+LND regtest environment
#
# Prerequisites:
# 1. Regtest running (minimal — only needs bitcoind + lnd-1 + lnd-4):
# cd ~/dev/local/docker/regtest && docker compose up -d bitcoind lnd-1 lnd-4
# 2. Lamassu services running:
# cd docker && docker compose -f docker-compose.regtest.yml up -d
#
# What this script does:
# 1. Waits for bitcoind and LND nodes to be synced
# 2. Funds LND-1 (1 BTC from bitcoind)
# 3. Opens a 5M sat channel from LND-1 → LND-4 (LP's backend)
# 4. Mines blocks to confirm and announce the channel
# 5. Waits for LP to be healthy
# 6. Creates an LP app and funds it (10k sats via invoice from LND-1)
# 7. Prints summary with pubkeys, app token, and channel status
set -euo pipefail
# -- Colors --
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
info() { echo -e "${CYAN}[INFO]${NC} $*"; }
ok() { echo -e "${GREEN}[OK]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
err() { echo -e "${RED}[ERROR]${NC} $*"; }
# -- Auto-detect container name prefix --
# The regtest compose uses project name "lnbits" (via start-regtest) or "regtest" (direct).
# Detect which prefix the RUNNING bitcoind container has (filter=running avoids stale containers).
if docker ps -f name=lnbits-bitcoind-1 -f status=running --format '{{.Names}}' | grep -q .; then
PREFIX="lnbits"
elif docker ps -f name=regtest-bitcoind-1 -f status=running --format '{{.Names}}' | grep -q .; then
PREFIX="regtest"
else
err "No running bitcoind container found (tried lnbits-bitcoind-1 and regtest-bitcoind-1)"
err "Start regtest first: cd ~/dev/local/docker/regtest && docker compose up -d bitcoind lnd-1 lnd-4"
exit 1
fi
info "Using container prefix: ${PREFIX}"
# -- Container helpers --
bitcoin_cli() {
docker exec "${PREFIX}-bitcoind-1" bitcoin-cli -regtest "$@"
}
lncli_1() {
docker exec "${PREFIX}-lnd-1-1" lncli --network regtest --rpcserver=lnd-1:10009 "$@"
}
lncli_4() {
docker exec "${PREFIX}-lnd-4-1" lncli --network regtest --rpcserver=lnd-4:10009 "$@"
}
LP_URL="http://localhost:1776"
LP_EXT_URL="http://localhost:1777"
ADMIN_TOKEN="lamassu-dev-admin-token"
CHANNEL_SIZE=5000000 # 5M sats
FUND_AMOUNT=10000 # 10k sats for LP app
# -- Step 1: Wait for bitcoind --
info "Waiting for bitcoind..."
for i in $(seq 1 60); do
if bitcoin_cli getblockchaininfo > /dev/null 2>&1; then
ok "bitcoind ready (block $(bitcoin_cli getblockcount))"
break
fi
if [ "$i" -eq 60 ]; then
err "bitcoind not ready after 60s — is regtest running?"
exit 1
fi
sleep 1
done
# Ensure a wallet exists
bitcoin_cli createwallet default 2>/dev/null \
|| bitcoin_cli loadwallet default 2>/dev/null \
|| true
# Mine initial blocks if needed (coinbase needs 100 confirmations to be spendable)
BLOCKS=$(bitcoin_cli getblockcount)
if [ "$BLOCKS" -lt 101 ]; then
NEEDED=$((101 - BLOCKS))
info "Mining ${NEEDED} initial blocks (coinbase needs 100 confirmations)..."
bitcoin_cli -generate "$NEEDED" > /dev/null
ok "Mined to block $(bitcoin_cli getblockcount)"
fi
# -- Step 2: Wait for LND-1 and LND-4 to sync --
wait_lnd_sync() {
local name=$1
local cli_fn=$2
info "Waiting for ${name} to sync..."
for i in $(seq 1 120); do
if [ "$($cli_fn getinfo 2>/dev/null | jq -r '.synced_to_chain' 2>/dev/null)" = "true" ]; then
ok "${name} synced"
return 0
fi
if [ "$i" -eq 120 ]; then
err "${name} not synced after 120s"
exit 1
fi
sleep 1
done
}
wait_lnd_sync "LND-1" lncli_1
wait_lnd_sync "LND-4" lncli_4
# -- Step 3: Fund LND-1 --
info "Funding LND-1..."
LND1_ADDR=$(lncli_1 newaddress p2wkh | jq -r '.address')
bitcoin_cli -named sendtoaddress address="$LND1_ADDR" amount=1 fee_rate=100 > /dev/null
info "Mining 6 blocks to confirm funding..."
bitcoin_cli -generate 6 > /dev/null
# Wait for LND-1 to see the confirmed balance
for i in $(seq 1 30); do
BALANCE=$(lncli_1 walletbalance | jq -r '.confirmed_balance')
if [ "$BALANCE" != "0" ] && [ -n "$BALANCE" ]; then
ok "LND-1 funded: ${BALANCE} sats"
break
fi
sleep 1
done
# -- Step 4: Connect LND-1 → LND-4 and open channel --
LND4_PUBKEY=$(lncli_4 getinfo | jq -r '.identity_pubkey')
info "Connecting LND-1 → LND-4 (${LND4_PUBKEY:0:16}...)..."
lncli_1 connect "${LND4_PUBKEY}@${PREFIX}-lnd-4-1:9735" > /dev/null 2>&1 || true
# Check if channel already exists
EXISTING=$(lncli_1 listchannels --peer "$LND4_PUBKEY" 2>/dev/null | jq '.channels | length')
if [ "${EXISTING:-0}" -gt 0 ]; then
warn "Channel to LND-4 already exists, skipping open"
else
info "Opening ${CHANNEL_SIZE} sat channel LND-1 → LND-4..."
lncli_1 openchannel "$LND4_PUBKEY" "$CHANNEL_SIZE" > /dev/null
info "Mining 6 blocks to confirm channel..."
bitcoin_cli -generate 6 > /dev/null
# Wait for channel to leave pending
for i in $(seq 1 60); do
PENDING=$(lncli_1 pendingchannels | jq '.pending_open_channels | length')
if [ "$PENDING" = "0" ]; then
break
fi
sleep 1
done
fi
# -- Step 5: Verify channel is active and in graph --
info "Waiting for channel to become active..."
for i in $(seq 1 60); do
ACTIVE=$(lncli_1 listchannels --peer "$LND4_PUBKEY" 2>/dev/null | jq '[.channels[] | select(.active == true)] | length')
if [ "${ACTIVE:-0}" -gt 0 ]; then
ok "Channel active"
break
fi
if [ "$i" -eq 60 ]; then
warn "Channel not active after 60s — may need more blocks"
fi
sleep 1
done
# Mine a few more blocks to ensure graph announcement propagates
bitcoin_cli -generate 3 > /dev/null
info "Checking graph..."
for i in $(seq 1 30); do
GRAPH_NODES=$(lncli_1 describegraph 2>/dev/null | jq '.nodes | length')
GRAPH_EDGES=$(lncli_1 describegraph 2>/dev/null | jq '.edges | length')
if [ "${GRAPH_EDGES:-0}" -gt 0 ]; then
ok "Graph: ${GRAPH_NODES} nodes, ${GRAPH_EDGES} edges"
break
fi
if [ "$i" -eq 30 ]; then
warn "Graph has no edges yet — channel may not be announced"
fi
sleep 2
done
# -- Step 6: Wait for Lightning.Pub to be healthy --
info "Waiting for Lightning.Pub..."
for i in $(seq 1 120); do
if curl -so /dev/null "${LP_URL}" 2>/dev/null; then
ok "Lightning.Pub ready"
break
fi
if [ "$i" -eq 120 ]; then
err "Lightning.Pub not ready after 120s"
err "Check: docker logs lamassu-lightning-pub"
exit 1
fi
sleep 1
done
# Small delay to ensure LP has fully initialized its LND connection
sleep 5
# -- Step 7: Create LP app --
info "Creating LP app 'regtest-atm'..."
APP_RESPONSE=$(curl -sf "${LP_URL}/api/admin/app/add" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"name": "regtest-atm", "allow_user_creation": true}')
if [ -z "$APP_RESPONSE" ]; then
err "Failed to create LP app — check LP logs"
exit 1
fi
APP_ID=$(echo "$APP_RESPONSE" | jq -r '.app.id')
APP_NPUB=$(echo "$APP_RESPONSE" | jq -r '.app.npub')
APP_TOKEN=$(echo "$APP_RESPONSE" | jq -r '.auth_token')
if [ "$APP_ID" = "null" ] || [ -z "$APP_ID" ]; then
err "App creation returned unexpected response:"
echo "$APP_RESPONSE" | jq .
exit 1
fi
ok "App created: id=${APP_ID}"
# -- Step 8: Fund the app (create invoice via LP, pay from LND-1) --
info "Funding app with ${FUND_AMOUNT} sats..."
INVOICE_RESPONSE=$(curl -sf "${LP_URL}/api/app/add/invoice" \
-H "Authorization: Bearer ${APP_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"payer_identifier\": \"bootstrap\", \"http_callback_url\": \"\", \"invoice_req\": {\"amountSats\": ${FUND_AMOUNT}, \"memo\": \"regtest bootstrap\"}}")
INVOICE=$(echo "$INVOICE_RESPONSE" | jq -r '.invoice')
if [ "$INVOICE" = "null" ] || [ -z "$INVOICE" ]; then
warn "Failed to create invoice — app has 0 balance"
warn "Response: $INVOICE_RESPONSE"
else
# Pay from LND-1
PAY_RESULT=$(lncli_1 payinvoice --force "$INVOICE" 2>&1) || true
PAY_STATUS=$(echo "$PAY_RESULT" | jq -r '.status' 2>/dev/null || echo "")
if [ "$PAY_STATUS" = "SUCCEEDED" ] || echo "$PAY_RESULT" | grep -q "SUCCEEDED"; then
ok "App funded with ${FUND_AMOUNT} sats"
else
warn "Payment may have failed — check manually"
warn "Result: $(echo "$PAY_RESULT" | tail -3)"
fi
fi
# -- Summary --
# LP uses LND-4 as its backend, so LP pubkey = LND-4 pubkey
echo ""
echo -e "${GREEN}========================================${NC}"
echo -e "${GREEN} Regtest Bootstrap Complete${NC}"
echo -e "${GREEN}========================================${NC}"
echo ""
echo -e " ${CYAN}LND-1 pubkey:${NC} $(lncli_1 getinfo | jq -r '.identity_pubkey')"
echo -e " ${CYAN}LP (LND-4):${NC} ${LND4_PUBKEY}"
echo ""
echo -e " ${CYAN}LP app ID:${NC} ${APP_ID}"
echo -e " ${CYAN}LP app token:${NC} ${APP_TOKEN}"
echo ""
echo -e " ${CYAN}LP URL:${NC} ${LP_URL}"
echo -e " ${CYAN}Extension URL:${NC} ${LP_EXT_URL}"
echo -e " ${CYAN}Relay URL:${NC} ws://localhost:7777"
echo ""
CHAN_INFO=$(lncli_1 listchannels --peer "$LND4_PUBKEY" 2>/dev/null | jq -r '.channels[0] | "\(.local_balance) / \(.capacity) sats (active: \(.active))"' 2>/dev/null || echo "unknown")
echo -e " ${CYAN}Channel:${NC} LND-1 → LND-4: ${CHAN_INFO}"
echo ""

142
docker/regtest.sh Executable file
View file

@ -0,0 +1,142 @@
#!/usr/bin/env bash
# regtest.sh — Manage the LP+LND regtest development environment
#
# Usage:
# ./regtest.sh up Start everything + bootstrap
# ./regtest.sh down Stop everything
# ./regtest.sh reset Full teardown (volumes + data) and fresh start
# ./regtest.sh status Show container status
# ./regtest.sh logs [svc] Tail logs (lp, relay, miner, lnd1, lnd4, bitcoind)
# ./regtest.sh lncli N Run lncli on LND-N (e.g. ./regtest.sh lncli 1 getinfo)
# ./regtest.sh bitcoin Run bitcoin-cli (e.g. ./regtest.sh bitcoin getblockcount)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REGTEST_DIR="${REGTEST_DIR:-$HOME/dev/local/docker/regtest}"
COMPOSE_FILE="${SCRIPT_DIR}/docker-compose.regtest.yml"
RED='\033[0;31m'
GREEN='\033[0;32m'
CYAN='\033[0;36m'
NC='\033[0m'
# -- Detect container prefix (regtest- or lnbits-) --
detect_prefix() {
if docker ps -f name=regtest-bitcoind-1 -f status=running --format '{{.Names}}' | grep -q .; then
echo "regtest"
elif docker ps -f name=lnbits-bitcoind-1 -f status=running --format '{{.Names}}' | grep -q .; then
echo "lnbits"
else
echo ""
fi
}
cmd_up() {
echo -e "${CYAN}Starting regtest (bitcoind + lnd-1 + lnd-4)...${NC}"
cd "$REGTEST_DIR" && docker compose up -d bitcoind lnd-1 lnd-4
echo -e "${CYAN}Starting lamassu services...${NC}"
docker compose -f "$COMPOSE_FILE" up -d
echo -e "${CYAN}Running bootstrap...${NC}"
"$SCRIPT_DIR/regtest-bootstrap.sh"
}
cmd_down() {
echo -e "${CYAN}Stopping lamassu services...${NC}"
docker compose -f "$COMPOSE_FILE" down 2>/dev/null || true
echo -e "${CYAN}Stopping regtest...${NC}"
cd "$REGTEST_DIR" && docker compose down 2>/dev/null || true
echo -e "${GREEN}All stopped.${NC}"
}
cmd_reset() {
echo -e "${CYAN}Full reset: tearing down everything + wiping data...${NC}"
docker compose -f "$COMPOSE_FILE" down -v 2>/dev/null || true
cd "$REGTEST_DIR" && docker compose down -v 2>/dev/null || true
# Clean LND data (root-owned from containers)
docker run --rm -v "$REGTEST_DIR/data:/data" alpine sh -c 'rm -rf /data/lnd-1/* /data/lnd-4/*' 2>/dev/null || true
echo -e "${GREEN}Clean. Starting fresh...${NC}"
cmd_up
}
cmd_status() {
echo -e "${CYAN}Containers:${NC}"
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' | grep -E 'regtest|lamassu|NAMES' | sort
PREFIX=$(detect_prefix)
if [ -n "$PREFIX" ]; then
echo ""
BLOCK=$(docker exec "${PREFIX}-bitcoind-1" bitcoin-cli -regtest getblockcount 2>/dev/null || echo "?")
echo -e " ${CYAN}Block height:${NC} ${BLOCK}"
for N in 1 4; do
SYNCED=$(docker exec "${PREFIX}-lnd-${N}-1" lncli --network regtest --rpcserver="lnd-${N}:10009" getinfo 2>/dev/null | jq -r '.synced_to_chain' 2>/dev/null || echo "?")
echo -e " ${CYAN}LND-${N} synced:${NC} ${SYNCED}"
done
LP_STATUS=$(curl -so /dev/null -w "%{http_code}" http://localhost:1776 2>/dev/null || echo "down")
echo -e " ${CYAN}LP status:${NC} ${LP_STATUS}"
EXT_STATUS=$(curl -so /dev/null -w "%{http_code}" http://localhost:1777 2>/dev/null || echo "down")
echo -e " ${CYAN}Extension:${NC} ${EXT_STATUS}"
fi
}
cmd_logs() {
local svc="${1:-lp}"
case "$svc" in
lp|lightning-pub) docker logs -f --tail 50 lamassu-lightning-pub ;;
relay|strfry) docker logs -f --tail 50 lamassu-relay ;;
miner) docker logs -f --tail 50 lamassu-miner ;;
lnd1|lnd-1) PREFIX=$(detect_prefix); docker logs -f --tail 50 "${PREFIX}-lnd-1-1" ;;
lnd4|lnd-4) PREFIX=$(detect_prefix); docker logs -f --tail 50 "${PREFIX}-lnd-4-1" ;;
bitcoind) PREFIX=$(detect_prefix); docker logs -f --tail 50 "${PREFIX}-bitcoind-1" ;;
*) echo "Unknown service: $svc (try: lp, relay, miner, lnd1, lnd4, bitcoind)"; exit 1 ;;
esac
}
cmd_lncli() {
local N="$1"; shift
PREFIX=$(detect_prefix)
if [ -z "$PREFIX" ]; then
echo -e "${RED}No running regtest found${NC}"; exit 1
fi
docker exec "${PREFIX}-lnd-${N}-1" lncli --network regtest --rpcserver="lnd-${N}:10009" "$@"
}
cmd_bitcoin() {
PREFIX=$(detect_prefix)
if [ -z "$PREFIX" ]; then
echo -e "${RED}No running regtest found${NC}"; exit 1
fi
docker exec "${PREFIX}-bitcoind-1" bitcoin-cli -regtest "$@"
}
# -- Main --
case "${1:-help}" in
up) cmd_up ;;
down) cmd_down ;;
reset) cmd_reset ;;
status) cmd_status ;;
logs) shift; cmd_logs "${1:-lp}" ;;
lncli) shift; cmd_lncli "$@" ;;
bitcoin) shift; cmd_bitcoin "$@" ;;
*)
echo "Usage: $0 {up|down|reset|status|logs|lncli|bitcoin}"
echo ""
echo " up Start everything + bootstrap"
echo " down Stop everything"
echo " reset Full teardown + fresh start"
echo " status Show container & service status"
echo " logs [s] Tail logs (lp, relay, miner, lnd1, lnd4, bitcoind)"
echo " lncli N Run lncli on LND-N (e.g. $0 lncli 1 getinfo)"
echo " bitcoin Run bitcoin-cli (e.g. $0 bitcoin getblockcount)"
;;
esac