Make the CLN dev-fixture rune creation self-healing

Address review F5 on #1625: create-rune.sh was a one-shot poststart script — if
the RPC wasn't ready within its poll or createrune failed, it exited without ever
writing rtl.rune, and since the cln healthcheck gates on that file and rtl waits
on service_healthy, a failed pass deadlocked the whole stack until 'down -v'.

Drive rune creation from the healthcheck instead: the script is now a quick,
idempotent single attempt, and the healthcheck runs it on every interval, so a
transient RPC-startup race just retries and self-heals. Moved the script out of
lightning-poststart.d to /opt and updated the healthcheck, compose comment and
README accordingly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
saubyk 2026-07-17 11:54:55 -07:00 committed by Suheb
parent 99cb60d2a7
commit 9c3cd983b4
4 changed files with 46 additions and 49 deletions

View file

@ -115,12 +115,15 @@ before she can route to carol. The seed waits for this; anything you script your
should too.
**Core Lightning auth uses a rune.** RTL talks to `cln` over clnrest and authenticates
with a rune, not a macaroon. On first start `cln/poststart.d/create-rune.sh` runs inside
the node (once the RPC is up), creates a master rune, and writes it as
`LIGHTNING_RUNE="…"` to `rtl.rune` in the shared `cln_data` volume; RTL reads it via the
`runePath` in its config. The `cln` healthcheck only passes once that file exists, so RTL
waits for it. `--clnrest-host=0.0.0.0` is required for RTL (another container) to reach
clnrest; the default `127.0.0.1` would only be reachable from inside the node.
with a rune, not a macaroon. `cln/create-rune.sh` — run from the `cln` healthcheck —
creates a master rune once the RPC is up and writes it as `LIGHTNING_RUNE="…"` to
`rtl.rune` in the shared `cln_data` volume; RTL reads it via the `runePath` in its config.
The healthcheck reports unhealthy until that file exists, so RTL (which waits on
`service_healthy`) starts only once the rune is ready. Because it runs on every
healthcheck tick (idempotent), a transient RPC-startup race just retries and self-heals
rather than wedging the stack. `--clnrest-host=0.0.0.0` is required for RTL (another
container) to reach clnrest; the default `127.0.0.1` would only be reachable from inside
the node.
## Not included

26
docker/cln/create-rune.sh Executable file
View file

@ -0,0 +1,26 @@
#!/usr/bin/env bash
#
# Ensure the RTL rune exists. One quick, idempotent attempt:
# - already have it -> succeed
# - RPC up, createrune works -> write it, succeed
# - RPC not ready / failure -> fail, so the caller retries
#
# This is invoked from the cln healthcheck (not a one-shot poststart hook) so a
# transient RPC-startup race self-heals on the next healthcheck tick instead of
# permanently wedging the stack. Stores the rune as LIGHTNING_RUNE="<rune>", the
# format RTL reads via its runePath. POSIX sh compatible.
set -u
RUNE_FILE="${LIGHTNINGD_DATA}/rtl.rune"
[ -f "${RUNE_FILE}" ] && exit 0
lightning-cli --network="${LIGHTNINGD_NETWORK}" getinfo >/dev/null 2>&1 || exit 1
rune=$(lightning-cli --network="${LIGHTNINGD_NETWORK}" createrune 2>/dev/null \
| grep -o '"rune"[[:space:]]*:[[:space:]]*"[^"]*"' \
| sed -e 's/.*"rune"[[:space:]]*:[[:space:]]*"//' -e 's/"$//')
[ -n "${rune}" ] || exit 1
printf 'LIGHTNING_RUNE="%s"\n' "${rune}" > "${RUNE_FILE}"

View file

@ -1,35 +0,0 @@
#!/usr/bin/env bash
#
# Runs from Core Lightning's lightning-poststart.d. Creates a master rune (once)
# and stores it in the format RTL expects: a file containing
# LIGHTNING_RUNE="<rune>"
# which RTL reads via its runePath.
#
# The image entrypoint can invoke poststart scripts before the RPC socket is
# ready (it watches the datadir with a race that loses on a fresh node), so poll
# for `getinfo` before calling createrune. Idempotent: keeps the same rune across
# restarts so RTL's stored auth stays valid.
RUNE_FILE="${LIGHTNINGD_DATA}/rtl.rune"
[ -f "${RUNE_FILE}" ] && exit 0
# Wait (up to ~120s) for the RPC to accept commands.
for _ in $(seq 1 120); do
if lightning-cli --network="${LIGHTNINGD_NETWORK}" getinfo >/dev/null 2>&1; then
break
fi
sleep 1
done
rune=$(lightning-cli --network="${LIGHTNINGD_NETWORK}" createrune 2>/dev/null \
| grep -o '"rune"[[:space:]]*:[[:space:]]*"[^"]*"' \
| sed -e 's/.*"rune"[[:space:]]*:[[:space:]]*"//' -e 's/"$//')
if [ -z "${rune}" ]; then
echo "create-rune.sh: failed to create rune" >&2
exit 1
fi
printf 'LIGHTNING_RUNE="%s"\n' "${rune}" > "${RUNE_FILE}"
echo "create-rune.sh: wrote rune to ${RUNE_FILE}"

View file

@ -145,9 +145,9 @@ services:
# 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. The entrypoint runs scripts dropped in
# lightning-poststart.d once the RPC socket is up; create-rune.sh generates the
# rune there and writes it to /root/.lightning/rtl.rune (RTL mounts that read-only).
# 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.
@ -175,13 +175,16 @@ services:
- "${CLN_REST_PORT:-3010}:3010"
volumes:
- cln_data:/root/.lightning
- ./cln/poststart.d:/root/.lightning/lightning-poststart.d:ro
- ./cln/create-rune.sh:/opt/create-rune.sh:ro
healthcheck:
# Healthy only once the RPC is up AND the rune file has been written, so the
# rtl service (which reads the rune at startup) can wait on this.
test: ["CMD-SHELL", "lightning-cli --network=regtest getinfo >/dev/null 2>&1 && test -f /root/.lightning/rtl.rune"]
# 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: 5s
timeout: 10s
retries: 40
# RTL rewrites its config file on startup, so it cannot be given the tracked