Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Appliance & Reference Stack

satd ships three ways to run it beyond a bare binary: a docker-compose reference stack, a downloadable appliance image, and packages for the Umbrel and StartOS app stores. They share one configuration and one certificate scheme, so what you learn from any of them applies to the others.

What it isWhere it livesSupport
Reference stackcompose: satd plus optional third-party overlayscontrib/stack/satd supported; overlays best-effort
Appliance imagea bootable VM with satd, wallets and Lightningcontrib/appliance/satd supported; bundled software best-effort
Store packagessatd, sat-cli, sat-tui and MCP onlycontrib/packaging/StartOS supported; Umbrel supported on x86_64; see below

The appliance image and the stack's overlays bundle third-party software (wallets, Lightning, ecash, and others) so you can try satd end to end. That software is included on a best-effort basis for evaluation and testing. It is not a production deployment: we do not track its security advisories in real time, and a critical fix in a bundled component may not appear in an appliance image until the next scheduled build. satd itself in this image is the same supported release as our tarballs and container image. For production, run satd from a release artifact or an app store package and operate the other components yourself.

The Umbrel and StartOS packages carry no such notice: they contain only satd.

Both store packages have been installed on a real server and driven through every interface they export — see What is checked, and how below for what that covered. Both on x86_64, and the StartOS package on aarch64 as well: built and installed on an arm64 machine, with every interface answering as it does on x86_64, and the binaries in the image genuinely aarch64 rather than emulated. Nothing there failed for a reason to do with the architecture.

The Umbrel package has not been installed on aarch64, which is why the table still qualifies that one. The arm64 half of the container image is exercised by the reference stack's own test suite on an arm64 host, but umbrelOS ships aarch64 only as a Raspberry Pi image, with no supported path to a VM.

Installing from an app store

Umbrel

satd is in a community app store rather than Umbrel's own:

  1. In umbrelOS, open the App Store, then ⋯ → Community App Stores.
  2. Add https://github.com/epochbtc/umbrel-apps.
  3. Open the satd store and install satd.

The app takes its own host ports, clear of every other app in the Umbrel store, so it installs alongside Bitcoin Node, Fulcrum and Ride The Lightning:

PortSurface
8430the status page, through Umbrel's proxy and login
8431Esplora, TLS
8433Bitcoin P2P
50012Electrum, TLS
8436JSON-RPC, TLS
8439MCP, TLS and a bearer token

Opening the app shows satd's status page: sync progress, whether a wallet can connect yet, and the connection strings to use.

Point a wallet at umbrel.local:50012 over SSL. Sparrow and Electrum pin the certificate on first use; a client that verifies against a CA needs the install's, and the MCP token lives beside it. The status page never shows either, since it carries nothing secret, so reading them takes SSH:

sudo cat ~/umbrel/app-data/epochbtc-satd/data/tls/ca.crt
sudo cat ~/umbrel/app-data/epochbtc-satd/data/secrets/mcp-token

See Trusting it for importing the CA. Other apps on the device reach JSON-RPC in plain text on the app network, as epochbtc-satd_server_1:8332 with the cookie at APP_SATD_RPC_COOKIE_FILE, which is how they reach Bitcoin Node too.

Earlier builds of the package used 8333, 50002, 3001, 8336 and 8339. A client configured against those needs the new port.

Umbrel backups leave out the chain and chainstate, which the node downloads again on its own, and keep the CA and the MCP token.

StartOS

satd is not yet listed in Start9's community registry. Until it is, build the package and sideload it as contrib/packaging/startos/README.md describes. Its Instructions tab covers the interfaces, the actions and what the package does not do.

The reference stack

cd contrib/stack
cp .env.example .env
docker compose up -d

That runs satd on signet with JSON-RPC, Electrum, Esplora and the metrics endpoint all enabled, each TLS-terminated by a certificate the install issues for itself on first start.

Overlays add third-party software, combined with repeated -f:

docker compose -f compose.yml -f compose.lightning.yml -f compose.proxy.yml up -d
OverlayContents
compose.lightning.ymlLND in Neutrino mode, Ride The Lightning
compose.cln.ymlCore Lightning, as an alternative to LND
compose.cashu.ymla Nutshell mint backed by that LND
compose.btcpay.ymlPostgres, NBXplorer, BTCPay Server
compose.proxy.ymlCaddy, terminating TLS for the web UIs and metrics

Overlays that need a secret have no default and refuse to start without one, rather than shipping a value every deployment would share:

echo "RTL_PASSWORD=$(openssl rand -hex 24)" >> .env
echo "MINT_PRIVATE_KEY=$(openssl rand -hex 32)" >> .env
echo "POSTGRES_PASSWORD=$(openssl rand -hex 24)" >> .env
echo "ARK_POSTGRES_PASSWORD=$(openssl rand -hex 24)" >> .env

RTL_PASSWORD is the login for Ride The Lightning, which fronts LND's admin macaroon. Left unset, RTL generates a configuration whose password is the literal string password, so this one is required rather than defaulted.

satd-appliance enable <overlay> generates each of these into /var/lib/satd-appliance/overlay.env on first use, so the appliance needs none of this by hand.

Which ports are published

Plain RPC, Electrum, Esplora and metrics listeners bind the compose network and are not published. They exist because the overlay containers cannot be taught to trust a private CA. What leaves the host is TLS only:

PublishedSurface
8336JSON-RPC over TLS
50002Electrum over TLS
3001Esplora over TLS
9336metrics, health and the status page over TLS (satd 0.6.0 on)
8339MCP over TLS, when SATD_MCP=1
38333 (signet)Bitcoin P2P
443 / 8443 / 49393 / 9443RTL, Cashu mint, BTCPay and metrics, with compose.proxy.yml

BTCPay's own HTTP port binds 127.0.0.1 and RTL and the mint are not published at all, so the proxy is the only route to a web UI from another machine. A docker-published port is also not filtered by the appliance's inbound firewall chain, which is the second reason those bindings matter.

The internal RPC port is 8332 on every network so that overlays, the proxy and the store packages address one fixed port. The cost is that sat-cli inside the container needs -rpcport=8332 on any network but mainnet, since it derives its default from the chain:

docker compose exec satd sat-cli -rpcport=8332 getblockchaininfo
docker compose exec -it satd sat-tui -rpcport=8332

No pruning, anywhere

Electrum and Esplora both require txindex, and satd rejects txindex together with prune. So every deliverable here runs a fully indexed node. On mainnet that is the whole chain plus the address, spend and transaction indices — see Disk Footprint & Indices, and budget a 2 TB volume. Initial Block Download & Fast Sync covers loading an AssumeUTXO snapshot so the node is usable in hours rather than days.

signet is the default everywhere for this reason: it is the only network on which the whole stack is a one-evening exercise.

TLS

contrib/stack/tls/mkca.sh is the one certificate script. The compose stack's satd-init, the appliance's first boot, and both store packages run it, so all four produce the same material and the client instructions are identical everywhere.

It creates a CA for that install only, then issues one server certificate that every satd surface presents. That is why there are two certificates and not one self-signed: clients import the CA once, and every later reissue — after a hostname change, a new address, or a year — is signed by a CA they already trust, with nothing to accept again.

The certificate covers localhost, 127.0.0.1, ::1, the hostname, <hostname>.local, and the machine's non-bridge addresses. Prefer the mDNS name. A DHCP change invalidates an address in the SAN list; the name survives it.

Reissue happens automatically when the certificate expires within 30 days or the machine's names or addresses have changed. The CA is never rotated automatically — that would invalidate trust every client has established. Rotating it is a deliberate act: delete the CA files and re-run.

Trusting it

# compose
docker compose exec satd cat /var/lib/satd/tls/ca.crt > satd-ca.crt
# appliance
satd-appliance tls export-ca > satd-ca.crt

Then:

ClientHow
curl, python, Go, anything using the OS storeimport satd-ca.crt into the system trust store
sat-cli / sat-tui--rpctls --rpccacert=satd-ca.crt --rpcport=8336
Firefoxalready policy-configured on the appliance desktop; elsewhere, import it
Sparrow, Electrum, Lianathese pin the server certificate on first use; accept it once

-rpccacert wants the certificate that issued the one the server presents. For a self-signed node certificate that is the certificate itself; it is not the leaf of a chain, which cannot anchor its own path.

What TLS does not cover

Bearer tokens from an authfile still gate MCP, streaming and Esplora writes; the plain loopback RPC listener is cookie-authenticated. The local CA authenticates the appliance to clients, not clients to the appliance — every surface supports mTLS if you turn it on, but none requires it by default.

The metrics endpoint has native TLS from satd 0.6.0 on, on 9336 beside the plain listener; see Observability. The streaming WebSocket has none. It stays on loopback or the container network, and compose.proxy.yml fronts it.

The appliance image

A bootable VM: core is headless, desktop adds XFCE with Sparrow, Electrum and Liana already pointed at the node. On arm64 the desktop has Sparrow alone, because Electrum and Liana publish no arm64 build. Each bundled wallet is installed from its project's own release, with the download checked against a signature from a pinned key; the build fails rather than installing anything that does not verify.

Downloading a built image

Images are attached to the GitHub release for each version, alongside the tarballs, and are signed with the same minisign key:

Both architectures are published. Pick arm64 on Apple Silicon and on an arm64 server; amd64 on an Intel or AMD host. Running an image under emulation works but is slow enough to be unpleasant for a syncing node.

# core is headless and ~600 MB; desktop is ~1.4 GB.
ver=0.5.2
arch=arm64   # or amd64
base="https://github.com/epochbtc/satd/releases/download/v$ver"
curl -fLO "$base/satd-appliance-$ver-core-$arch.qcow2"
curl -fLO "$base/satd-appliance-$ver-core-$arch.qcow2.minisig"

minisign -Vm "satd-appliance-$ver-core-$arch.qcow2" \
  -P RWQeP6MczCgPh6tU03GEMm4HsnGbXte3VT2Bc52TBSR7Q+X7WnL5vfQ3

An arm64 guest is UEFI-only — there is no BIOS to fall back on — so give it a UEFI firmware. UTM on macOS does this for you; with plain QEMU, pass -machine virt and an AAVMF_CODE.fd in pflash.

The .qcow2 boots under QEMU/libvirt as it is — qemu-img already compressed it, so there is nothing to unpack. The .ova that accompanies the desktop flavour imports into VirtualBox or VMware.

The satd inside a released image is the same signed tarball published on that release, verified against the key above during the build — not a rebuild. Images you build yourself install the binaries from your working tree instead, and say so on the console.

Building one yourself

contrib/appliance/build-in-docker.sh --flavor core --out out/

No root, no KVM and no Packer: the image is built with mmdebstrap and a GRUB install onto a loop device, which runs in a container and on a hosted CI runner in minutes. contrib/appliance/README.md has the details.

First boot creates everything that must be unique to an install — the disk size, the console password, the CA and certificate, the MCP token — because an image that shipped any of those would be an image where every download shared them. The build asserts none of them exist in the artifact and refuses to finish otherwise.

Day-to-day operation goes through one command:

satd-appliance status
satd-appliance tls export-ca
sudo satd-appliance set-network mainnet    # refuses below 1.5 TB free
sudo satd-appliance enable lightning
satd-appliance logs satd

satd runs natively under systemd; the overlays run as containers from /opt/satd/stack, which is contrib/stack's overlay files unmodified.

The status page is at https://satd.local:9336/status from another machine, once the CA is imported, and http://127.0.0.1:9332/status on the appliance itself.

The firewall is default-deny inbound, and sshd is off until satd-appliance ssh enable.

Why LND runs in Neutrino mode

LND's bitcoind backend requires Bitcoin Core's raw ZMQ topics (zmqpubrawblock / zmqpubrawtx). satd does not implement them and rejects those settings; see CORE_DIFFERENCES.md. Neutrino needs no ZMQ — it pulls BIP 157/158 filter headers and filters over P2P, which satd serves because every deliverable here sets peerblockfilters=1.

Core Lightning is unaffected: its bcli plugin polls JSON-RPC, so it runs as an ordinary full-node client.

Ark

compose.ark.yml runs an Ark server against satd. Experimental — Ark is young, and every setting in that overlay was established by running the binary rather than read from a specification, so expect it to need attention on a version bump.

The chain is:

satd  ->  NBXplorer  ->  arkd-wallet  ->  arkd

arkd v0.9 splits the wallet into its own service, and that wallet's chain backend is NBXplorer — not Esplora, and not Core's ZMQ. Two things follow. satd implements no raw ZMQ topics, so a backend that needed them would have ruled Ark out entirely; and NBXplorer against satd is already a PR-gating canary in this repository, so the single link in that chain which touches satd is the link that is continuously tested.

First run is two steps, because arkd will not start without a signer key and its wallet must then be created and unlocked:

docker compose -f compose.yml -f compose.ark.yml run --rm ark-init   # prints the key
# add ARKD_SIGNER_KEY=... to .env
docker compose -f compose.yml -f compose.ark.yml up -d
docker compose -f compose.yml -f compose.ark.yml run --rm ark-init   # creates the wallet

Both the signer key and the wallet password are generated per install into the data volume. Neither is shipped.

What is checked, and how

Each bundled application is a compatibility claim, so each is exercised rather than asserted:

  • contrib/stack/tests/mkca-test.sh — the certificate script, including that it does not reissue a healthy certificate or rotate the CA.
  • contrib/stack/tests/smoke.sh — the stack on regtest, with every TLS listener probed from outside the container against the generated CA, LND syncing to the node's tip over Neutrino, and RTL served through the proxy.
  • contrib/appliance/tests/boot-test.sh — the built image booted under QEMU, checked through the guest agent and through forwarded ports.
  • contrib/stack/tests/compose-test.sh — static invariants of the compose files, including that the Umbrel package's ports are its own and mapped 1:1, and that its backup exclusions match the StartOS package's.
  • contrib/packaging/startos/test/ — the StartOS package's type check and unit tests, run by the app-store packages CI job. Two of them exist because a typecheck cannot see the defects they guard: a store read that made the Network action a no-op, and a ready gate pointed at /readyz, which is 503 for the whole of a sync.

The store packages were additionally installed and driven by hand — on StartOS 0.4.0.1 and on umbrelOS on x86_64, and on StartOS 0.4.0.1 again on aarch64. That is where those two defects were found, along with an image pin that named a tag predating satd-init: none of the three was visible to any static check, and the /readyz gate had passed an earlier spot check only because the node under it was minutes old. A fourth came out of the aarch64 install and was not about the architecture at all — MCP refused every request that arrived by hostname, because satd had left the transport's Host allowlist at its loopback-only default. See MCP. What was covered: satd-init producing this install's CA, certificate, MCP token and config; both health checks; Esplora and Electrum answering through the OS proxy against the server's root CA with Verify return code: 0 (ok); MCP refusing an unauthenticated call and completing an initialize with the token the package prints; switching a running node between chains; and satd returning by itself after a reboot. Not yet covered: aarch64 on Umbrel, and StartOS backup/restore.

Every probe that verifies a certificate is paired with the negative control that the same handshake without the CA must fail. A probe that would pass unverified proves nothing about the certificate.