- TypeScript 97.9%
- Dockerfile 2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .github/workflows | ||
| assets | ||
| clboss@49ffd76ad0 | ||
| rust-teos@be344ecc52 | ||
| startos | ||
| .dockerignore | ||
| .gitignore | ||
| .gitmodules | ||
| .prettierignore | ||
| AGENTS.md | ||
| CLAUDE.md | ||
| Dockerfile | ||
| Dockerfile.ui | ||
| icon.svg | ||
| instructions.md | ||
| LICENSE | ||
| Makefile | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
| UPDATING.md | ||
Core Lightning on StartOS
Everything not listed in this document should behave the same as upstream Core Lightning. If a feature, setting, or behavior is not mentioned here, the upstream documentation is accurate and fully applicable — see the Documentation section of
instructions.mdfor links.
Core Lightning is a Lightning Network node implementation. This package builds it with the Sling plugin built into the image and runs a web UI alongside it. It does not include a watchtower: The Eye of Satoshi's tower cannot read BLAKE2b block headers, so the watchtower returns once it supports BLAKE2b.
This is the #blake flavor of the package. It builds Core Lightning v26.06.9 with support for the 164-byte BLAKE2b block header, which the stock build cannot parse: a stock node stops at the activation block. It also signs wallet transactions and new channels with the opt-in SIGHASH_UNIFIED digest, so a channel funded past activation from coins received past activation is signed in a way a node that has not upgraded rejects.
See BLAKE2b Hard Fork Support for what that changes, including the peers you can connect to and the coins you should fund channels from.
- Upstream repo: https://github.com/privkeyio/lightning (fork of https://github.com/ElementsProject/lightning)
- Wrapper repo: https://github.com/privkeyio/cln-startos (fork of https://github.com/Start9Labs/cln-startos)
Table of Contents
- Image and Container Runtime
- Volume and Data Layout
- File Models
- Dependencies
- Network Access and Interfaces
- Installation and First-Run Flow
- Actions
- Tasks
- Health Checks
- Backups and Restore
- BLAKE2b Hard Fork Support
- Limitations and Differences
- Quick Reference for AI Consumers
Image and Container Runtime
Two images. The node's is built here: a signed release tarball is unpacked onto a slim Debian base and the Sling plugin is added; the web UI's is pulled as published. lightningd comes from the v26.06.9-blake2b.9 release tarball of privkeyio/lightning, with its checksum pinned in the lightningd-dist stage and taken from a GPG-verified manifest. bitcoin-cli is pinned and checksummed the same way in the bitcoin-cli stage: plugin-bcli and the check-synced health check both exec it, and the image it used to come with no longer supplies it.
| Property | Value |
|---|---|
| Images | Built from Dockerfile on debian:bookworm-slim, plus ghcr.io/elementsproject/cln-application |
| Architectures | x86_64, aarch64 — both images declare emulateMissingAs: 'aarch64' |
| Entrypoint | lightningd with an explicit config path; the UI runs its own server |
One plugin is dropped into the plugin directory at build time: sling (rebalancing), an upstream release binary pinned by SLING_VERSION and its sha256 in the Dockerfile. Nothing is fetched at runtime, so the image is self-contained.
| Subcontainer | Purpose |
|---|---|
lightning-sub |
lightningd and every oneshot, the one to attach to |
cln-application-sub |
The web UI, which talks to the node over its RPC and rune |
Volume and Data Layout
One volume, holding everything.
| Volume | Mount Point | Purpose |
|---|---|---|
main |
/root/.lightning |
The lightning directory: config, the node's hsm_secret and channel database, the UI's own data, and store.json |
Bitcoin's data directory is mounted read-only at /mnt/bitcoin, which is how lightningd reads its RPC cookie without a password ever being stored.
File Models
Three models. One is the node's own configuration, one is the web UI's, and one is StartOS-side state.
| File | Format | Modelled | Written by |
|---|---|---|---|
/config |
INI | Yes — FileHelper.raw |
Every init, the config actions, and watchHosts on address changes |
/store.json |
JSON | Yes — FileHelper.json |
Every init, the rescan action, main, and restore |
/data/app/config.json |
JSON | Yes — FileHelper.json |
Every init, and the Reset UI Password action |
config
Enforced — rewritten whenever the package writes the file: network, bitcoin-datadir, bind-addr, grpc-port, grpc-host, and clnrest-protocol. bitcoin-rpcuser and bitcoin-rpcpassword are modelled as "must be absent" and deleted if present, because authentication is by the cookie read through the mount.
clnrest-protocol is the one enforced value that is an override rather than a constant: upstream defaults CLNrest to HTTPS, and this package forces plaintext. A Tor onion address already encrypts, and wallets reaching the node that way cannot validate a StartOS-issued certificate; LAN and clearnet callers still get TLS, terminated by StartOS at the edge. See Network Access and Interfaces.
Derived, and rewritten whenever the underlying address changes: proxy (Tor's SOCKS address), announce-addr (the onion and public addresses published on the peer interface, or your custom external host in place of the IPs), and bitcoin-rpcconnect / bitcoin-rpcport. Editing any of these by hand does not stick.
Everything else — alias, color, fee policy, channel minimums, the plugin selection, the experimental flags — is yours, through the config actions. The only override install makes is switching clnrest on.
Two interactions are worth knowing because they produce a state neither setting explains alone. A custom external host is dropped rather than written while Tor Only is enabled: always-use-proxy disables lightningd's DNS resolution, and an announce-addr it cannot resolve is a fatal startup error rather than a warning — so the package omits it and raises a health check saying so. And enabling the Clams websocket adds a second ws:: bind address rather than replacing the first.
store.json
customExternalHosts is the announced address override, and rescan and restore are one-shot request flags.
Those two flags are deliberately not cleared when main reads them. A session where lightningd never comes up must not consume a request, or it vanishes silently — which is how a rescan requested during a crash loop used to be lost. A oneshot clears them only once the node answers RPC, and main ignores that clearing write so it does not bounce the service.
config.json
The web UI's config.json carries its display preferences and its password hash.
Dependencies
One, and it is required.
| Dependency | Kind | Health checks | Mount | Why |
|---|---|---|---|---|
| Bitcoin | running |
bitcoind, sync-progress |
/mnt/bitcoin, read-only |
Chain data, and the RPC cookie |
Both health checks are required, not just "running": a node that is up but still syncing cannot serve a Lightning node correctly, and the sync state is surfaced again in this package's own check-synced.
Bitcoin's RPC address is resolved from its own binding over the service bridge, so nothing is configured by hand and a Bitcoin update does not move it. When Bitcoin is absent the address keys are cleared rather than left stale, and lightningd fails to connect until it returns.
The node additionally restarts when Bitcoin writes a replacement RPC cookie, but not when the cookie merely disappears — an absent cookie means Bitcoin is down, and stopping lightningd at that moment hangs its shutdown.
Network Access and Interfaces
Four interfaces always, and two more depending on what is enabled.
| Interface | Id | Type | Port | Present |
|---|---|---|---|---|
| Web UI | ui |
ui | 4500 | always |
| RPC | rpc |
api | 8080 | always |
| Peer | peer |
p2p | 9735 | always |
| gRPC | grpc |
api | 2106 | always |
| CLNrest | clnrest |
api | 3010 | when CLNrest is enabled (it is, at install) |
| Clams Websocket | websocket |
api | 7272 | when the Clams remote websocket is enabled |
gRPC is passed through, not terminated. The plugin performs its own mutual TLS, so StartOS must not terminate at the edge — doing so would present the device certificate and strip the client's. The binding is configured for raw passthrough deliberately; a conventional HTTPS binding would look correct and silently break client authentication.
CLNrest carries its own credential in the address. The interface's URL includes the rune the package generated, and its scheme is overridden to clnrest+https or clnrest+http so that a wallet reading the scheme knows which transport to use — a bare clnrest:// is assumed to be TLS, which would be wrong for the Tor address.
Installation and First-Run Flow
Install seeds the three models, switches CLNrest on, and starts the node — there is no wizard, and no credential is asked for. Wallet creation is lightningd's own: it generates hsm_secret on first start.
The one piece of setup the package performs is the web UI's credential. A oneshot creates a rune scoped to the application and records it alongside the node's public key, regenerating it only if the node's identity changes or the rune is missing. The UI cannot start until that has happened.
The ordering that matters is Bitcoin's: the node starts, but check-synced reports Bitcoin's progress and then its own until both are caught up, which on a fresh Bitcoin node is the length of an initial block download.
Actions
Ten actions. Three configure the node, and the rest are recovery and information.
Configuration — General Settings, Plugins, Experimental Features
Three actions writing /config, grouped together. Each writes only the fields it presents, costs seconds plus a restart, and is safe to re-run — the forms are pre-filled from the current file.
- General Settings carries node identity, fee policy, channel minimums, Tor Only, the custom external host, and the CLNrest and Clams toggles. Two combinations produce a visible consequence rather than an error: Tor Only with a custom external host drops the host and raises a health check, and the Clams toggle changes the bind addresses.
- Plugins toggles CLNrest and selects whether the built-in Sling plugin loads.
- Experimental Features exposes upstream's experimental flags, which are not standardized across implementations and may break between releases.
Create Rune, Revoke All Runes
Create Rune mints an access credential for an external application, with the restrictions you specify. Available only while running; each run produces a new rune and does not affect existing ones.
Revoke All Runes invalidates every rune this node has issued including the web UI's, which is regenerated automatically on the next start. Run it when a credential may have been exposed. It is not selective — that is the point of it — so anything you have connected must be re-authorized afterwards.
Display BIP-39 Seed
Shows the seed words backing the on-chain wallet, for disaster recovery. Note what it is not: the seed alone cannot recover channel funds.
- Visibility: hidden entirely when no wallet exists yet, and shown as disabled with an explanation on a node whose wallet predates BIP-39 seeds — such a wallet cannot be given one, and moving the funds to a fresh install is the only route.
- Repeat safety: read-only.
Rescan Blockchain
Re-scans the chain for wallet outputs. Run it after a restore, or when an on-chain balance is missing.
- Input: a depth from the tip, or an absolute block height written with a leading hyphen.
- What it changes: sets the
rescanrequest flag, which the next start turns into alightningdargument and then clears. - Cost: hours.
check-syncedstays red for the duration; leave the node and Bitcoin running. - Repeat safety: safe to re-run. Because the flag is only consumed once the node answers RPC, a request survives a failed start rather than being silently dropped.
Reset UI Password
Sets a new password for the web UI, writing its config.json. It does not touch the node, its runes, or any external application's access.
Delete Gossip Store
Deletes the network gossip database, which the node rebuilds from peers. Run it if gossip is suspected corrupt.
- Availability: only while stopped, since the file is open in use.
- Cost: the node re-learns the network graph after starting, which takes time and affects routing until it does.
- Repeat safety: idempotent.
Node Info
Read-only, running only: the node's identity and current state.
Tasks
One task, raised by a restore rather than at install.
| Task | Severity | Raised when | Cleared when |
|---|---|---|---|
| Rescan Blockchain | important |
Immediately after a backup restore | The action runs |
The reason is that a restored node reports an on-chain balance of zero until the chain is rescanned, and nothing else in the interface explains why. important rather than critical: the node should keep running — indeed it must, for the rescan to proceed.
Health Checks
Three or four checks, plus one that appears only after a restore.
| Check | Displayed | Method | Present |
|---|---|---|---|
lightningd |
"RPC Interface" | lightning-cli getinfo succeeds |
always |
cln-application |
"Web Interface" | The UI's port is listening | always |
check-synced |
"Synced" | getinfo's sync warnings, and Bitcoin's block count |
always |
custom-external-host |
"Custom External Host" | Always fails, with an explanation | while Tor Only and a custom host conflict |
restored |
"Backup Restoration Detected" | Always fails, with an explanation | after an emergency recovery |
check-synced distinguishes three states, which is what makes it worth reading: Bitcoin not yet synced, the node catching up to Bitcoin (reported as a block count against Bitcoin's own), and synced. It fails only when lightning-cli itself errors, so a red check here is the node, not the chain.
Two checks are deliberate permanent failures, used as a way to say something the interface has nowhere else to put. custom-external-host reports that an announced address is being suppressed by Tor Only, and names both settings to change. restored reports that an emergency recovery has happened and that the node should be drained and reinstalled rather than kept in service — a state that is not a fault in the running software but is a serious one for the operator.
Backups and Restore
The main volume is copied wholesale — sdk.Backups.ofVolumes('main') — but the exclusions are the substance, because the channel database is deliberately not backed up.
- Excluded:
lightningd.sqlite3and its write-ahead sidecars, the RPC socket, the gossip store, and the application log. - Included:
hsm_secret,config,store.json, the emergency-recovery file, the UI's settings, and any watchtower data left by an earlier version, which holds its keys.
Restoring a Lightning node's channel database is dangerous — a stale copy claims a channel state the network has moved past — so this package does not restore one. What comes back is the node's identity and enough to recover funds, not a resumable node.
What a restore therefore does, automatically:
- The emergency-recovery file is copied aside before anything runs. Upstream's own plugin rewrites that file to describe the current channel set, so the restored copy is the last record able to reconstruct the pre-backup channels, and the copy is never touched again.
emergencyrecoverruns, and a permanently-failing health check appears saying what that means: all channels will be force-closed, funds swept on-chain, and the node should be drained and reinstalled afterwards rather than kept.- Ten thousand wallet addresses are pre-generated. A restored database restarts the address counter at zero, and the node only recognises addresses within a fixed window past the highest known-used index — so without this, a rescan silently misses outputs beyond the first gap. The window this widens applies to every later rescan too.
- The Rescan Blockchain task is raised, because until it runs the on-chain balance reads zero.
BLAKE2b Hard Fork Support
Stock Core Lightning assumes every block header is 80 bytes and hashes it with SHA256d. The BLAKE2b hard fork changes the proof of work at an activation height: from that block on, headers are 164 bytes and hashed with BLAKE2b, signalled by the top bit of the version field.
This is one chain with continuous history, not a second chain. Every block below the activation height keeps its original 80-byte SHA256d header permanently, so both forms coexist and each must be hashed with its own algorithm. Core Lightning computes block ids itself while following the chain, so a stock node cannot parse the activation block and stops there rather than following the wrong chain.
This build reads both forms. It also signs wallet transactions and new channels with the fork's opt-in SIGHASH_UNIFIED digest, so a channel funded after the fork, from coins that are themselves post-fork, cannot be replayed onto the old rules.
Four consequences worth knowing:
- The node will not connect to a peer that is still on the SHA256d rules. It advertises a required feature bit. That is deliberate: it stops you opening a channel with a peer that cannot follow the chain past activation. It also means you cannot cooperatively close a channel opened before activation with a counterparty still on the SHA256d rules.
- The feature numbers are provisional. They are not registered BOLT allocations and are expected to move. Channels opened under the current numbering may have to be closed and reopened once they are settled.
- Fund channels only from coins received past activation. A channel funded from a pre-activation UTXO has a funding transaction valid under both rule sets, which reopens the exposure unified signing exists to close.
- Downgrading is refused. A build without unified signing computes a different signature hash and could not close the channels this one opens, so the package declares the downgrade impossible rather than letting you strand a channel.
Limitations and Differences
- A restore is a recovery, not a resumption. Channels are force-closed by design; plan to sweep the funds and reinstall.
- The channel database is excluded from backups, deliberately.
- CLNrest is served as plaintext by the node, with TLS added at the edge for LAN and clearnet only.
- gRPC cannot be reached through a TLS-terminating path, because the plugin authenticates clients with their own certificates.
- A custom external host is incompatible with Tor Only and is dropped while both are set.
- No watchtower. The Eye of Satoshi's tower cannot read BLAKE2b block headers, so neither the tower nor its client is shipped.
- Plugins are those built into the image. Adding another means changing the image, not dropping a file on the volume.
- No riscv64 build, and on hardware without a native image the aarch64 build runs emulated.
Quick Reference for AI Consumers
package_id: c-lightning
image: ./Dockerfile # on debian:bookworm-slim; plus ghcr.io/elementsproject/cln-application
architectures:
- x86_64
- aarch64
subcontainers:
- lightning-sub # lightningd and every oneshot; the one to attach to
- cln-application-sub # the web UI
volumes:
main: /root/.lightning
file_models:
- /root/.lightning/config
- /root/.lightning/store.json
- /root/.lightning/data/app/config.json
startos_managed_env_vars:
- BITCOIN_NETWORK # web UI
- LIGHTNING_DATA_DIR # web UI
- APP_PROTOCOL # web UI
- APP_HOST # web UI
- APP_PORT # web UI
- APP_CONFIG_FILE # web UI
- APP_LOG_FILE # web UI
- LIGHTNING_VARS_FILE # web UI
- LIGHTNING_WS_PORT # web UI
- LIGHTNING_REST_PORT # web UI
- LIGHTNING_REST_PROTOCOL # web UI
- LIGHTNING_GRPC_PORT # web UI
dependencies:
- bitcoind # required; mounted read-only at /mnt/bitcoin
interfaces:
ui: { type: ui, port: 4500 }
rpc: { type: api, port: 8080 }
peer: { type: p2p, port: 9735 }
grpc: { type: api, port: 2106 } # TLS passthrough, not terminated
clnrest: { type: api, port: 3010 } # when enabled; URL carries the rune
websocket: { type: api, port: 7272 } # when the Clams websocket is enabled
actions:
- config
- plugins
- experimental
- createrune
- revoke-runes
- display-seed # hidden with no wallet; disabled on a pre-BIP-39 wallet
- rescan-blockchain
- reset-password
- delete-gossip-store # only-stopped
- node-info
tasks:
- { action: rescan-blockchain, severity: important } # raised after a restore
health_checks:
- lightningd # displayed "RPC Interface"
- cln-application # displayed "Web Interface"
- check-synced # displayed "Synced"
- custom-external-host # only while Tor Only conflicts with a custom host
- restored # only after an emergency recovery