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

Packaging satd

This document is the authoritative reference for downstream packagers (Umbrel, Start9, RaspiBlitz, MyNode, BTCPay, Debian/Fedora/Alpine, Homebrew, Nix). It describes file layout, signals, ports, config surface, runtime model, and the contract satd offers a packager.

The user-facing operator surfaces are documented elsewhere in this manual — see Observability & Metrics and Configuration, Tuning & Reload. The deviation catalog vs. Bitcoin Core is in CORE_DIFFERENCES.md. The not-yet-shipped ecosystem/packaging direction is tracked in ROADMAP.md.

Document status

This is PACKAGING.md v1. It covers what shipped today: the container, Type=notify systemd unit (with EXTEND_TIMEOUT_USEC heartbeats so reindex doesn't fight the start timeout), OpenRC and runit unit equivalents, on-disk layout, operational surface, release pipeline, signing across all three surfaces, reproducible build via Nix, CycloneDX SBOMs per binary, and a cargo-deny supply-chain gate.

Updated: 2026-05-07.

Binaries

satd ships two binaries:

BinaryPurpose
satdDaemon. Long-running process; opens RocksDB, runs P2P + RPC + optional protocol surfaces.
sat-cliJSON-RPC CLI client. Bitcoin Core-compatible flag shape (-rpcuser, -rpcpassword, -rpccookiefile, network selectors).

A third binary, sat-tui, is a curses-style operator dashboard. It is optional; packagers who don't want it can skip it.

There are deliberately no separate sat-electrum / sat-esplora companion binaries. Both protocols are subsystems of satd itself, gated by runtime flags (--electrum=1, --esplora=1). One process, one RocksDB, one log stream, one PID. This is a load-bearing design choice — see the Native Protocol Architecture chapter.

File layout

$DATADIR/                         # default: $HOME/.bitcoin (Core-compat)
└── <network>/                    # one of: <empty for mainnet>, testnet3, signet, regtest
    ├── blocks/
    │   ├── blk00000.dat          # flat-file block storage (state)
    │   ├── blk00001.dat
    │   └── ...
    ├── chainstate/               # RocksDB instance (state)
    │   ├── *.sst                 # SST files — the bulk of disk usage
    │   ├── CURRENT, MANIFEST-*   # RocksDB metadata
    │   └── ...
    ├── .cookie                   # RPC cookie auth (auto-generated, mode 0600)
    ├── mempool_history.log       # rolling mempool snapshot (state, derived-OK)
    ├── reorg.log                 # persistent reorg ledger (state, append-only)
    ├── debug.log                 # rotating diagnostic log (derived)
    ├── bitcoin.conf              # optional config file (Core-compat name)
    └── satd.conf                 # alternative config name (also accepted)

State — must be backed up to preserve consensus history: blocks/, chainstate/, reorg.log. These are load-bearing.

Derived / safe to nuke — regenerate from blocks/ via --reindex or --reindex-chainstate: everything inside chainstate/ (the RocksDB instance), mempool_history.log, debug.log, and the various *.complete index marker files inside chainstate/.

Single-instance RocksDB. Unlike Bitcoin Core, satd does not maintain separate LevelDB databases for the txindex, address index, or BIP 158 filter index. They are column families inside the one RocksDB instance, written atomically with each connect_block batch. This means:

  • Backup is simpler (one directory).
  • Index updates can never be visible without the corresponding tip update — the whole WriteBatch either commits or it doesn't.
  • An --reindex-chainstate rebuilds everything in chainstate (UTXO + indexes) but preserves the flat files.

Process model

  • One process. PID file is whatever the supervisor records; satd does not write its own PID file by default.
  • tokio async runtime; many tasks but a fixed-size worker pool.
  • rayon for script verification (CPU-bound parallelism).
  • File descriptors: RocksDB keeps many SST files mmapped; budget LimitNOFILE=65536 minimum. The systemd unit and the Docker image both pre-set this.

Signals

SignalBehaviour
SIGTERMClean shutdown. Flush RocksDB, fsync undo files, drain mempool snapshot, close listeners. May take up to 10 minutes under heavy IBD load — most shutdowns are sub-second.
SIGINTIdentical to SIGTERM.
SIGHUPLive config reload. Re-reads bitcoin.conf and applies the hot-reloadable subset without dropping the P2P swarm or flushing chainstate. satd logs to stdout (no debug.log), so SIGHUP is repurposed from Core's log-reopen to config reload — see Configuration, Tuning & Reload.
SIGUSR1Live TLS certificate reload. Re-reads the configured TLS leaf cert/key from disk and swaps it in atomically on every TLS surface, without restarting or dropping connections.
SIGKILLRocksDB recovers via WAL replay on next start. Avoid; one botched shutdown = one corrupted chainstate is the failure mode to design against.

Container supervisors should set a stop grace period of at least 10 minutes (--stop-timeout=600 for docker run, terminationGracePeriodSeconds: 600 for Kubernetes). The systemd unit ships TimeoutStopSec=10min for the same reason.

Network ports (defaults)

ServiceMainnetTestnetSignetRegtest
P2P8333183333833318444
JSON-RPC8332183323833218443
Esplora REST (--esplora)configurable, e.g. 3000
Electrum (--electrum)configurable, e.g. 50001
Metrics + health (--metricsport)configurable, e.g. 9332

The default RPC bind is loopback. Esplora, Electrum, and the metrics endpoint are off by default; turn them on per-deployment.

Health and readiness

When --metricsport=<port> is configured, satd exposes three unauthenticated HTTP endpoints on that port (default bind 127.0.0.1):

EndpointMeaning
GET /healthzProcess is alive and the event loop is responsive. Cheap.
GET /readyzRocksDB is open, headers are syncing, peers > 0. Returns 503 during IBD.
GET /metricsPrometheus exposition format.

These are the right surfaces to wire to a Docker HEALTHCHECK, Kubernetes liveness/readiness probes, or a systemd ExecStartPost= poll. The shipped Type=notify unit (see §"systemd" below) uses sd_notify(READY=1) for startup signalling; poll-based readiness against /readyz works equally well for non-systemd supervisors.

Configuration

Two files are accepted, both with Bitcoin Core's key=value / [network] syntax:

  • bitcoin.conf — Core-compat name. Same shape, same precedence.
  • satd.conf — preferred when running side-by-side with a Core install; identical syntax.

Resolution order: --conf=<path> if given, else <datadir>/bitcoin.conf, else <datadir>/satd.conf. CLI flags always win over file values.

The full flag matrix is in Configuration, Tuning & Reload. The container ships a sensible mainnet-loopback default; everything is overridable via -e SATD_* … see "Container" below.

Container

The repository ships a multi-stage Dockerfile at the repo root. Build:

docker build -t satd:dev .

Properties of the image:

  • Base: debian:bookworm-slim.
  • Runtime user: satd (UID/GID 2121, deliberately non-1000 to avoid bind-mount UID clash with the host operator user).
  • PID 1: tini, so SIGTERM forwards to satd cleanly.
  • Datadir: /var/lib/satd. Marked as a VOLUME.
  • Exposed ports: 8333 (P2P), 8332 (RPC). Other ports are off by default; map them with -p per deployment.

Example mainnet run with persistent state, RPC on loopback, metrics on loopback:

docker volume create satd-data
docker run -d --name satd \
  --restart unless-stopped \
  --stop-timeout 600 \
  -v satd-data:/var/lib/satd \
  -p 8333:8333 \
  -p 127.0.0.1:8332:8332 \
  -p 127.0.0.1:9332:9332 \
  satd:dev \
    --rpcbind=0.0.0.0 --rpcallowip=127.0.0.0/8 \
    --metricsport=9332 --metricsbind=0.0.0.0

CLI:

docker exec satd sat-cli getblockchaininfo

Multi-arch images. Tag-triggered releases publish linux/amd64 and linux/arm64 to ghcr.io/epochbtc/satd via the workflow at .github/workflows/release.yml. Tags follow docker/metadata-action defaults: <MAJOR>.<MINOR>.<PATCH>, <MAJOR>.<MINOR>, and latest on every release.

docker pull ghcr.io/epochbtc/satd:0.1.0
docker pull ghcr.io/epochbtc/satd:latest

These images are signed with cosign keyless OIDC, attested to the Rekor transparency log (verifier command under "Signed releases" below).

systemd

The repository ships contrib/systemd/satd.service. Install:

sudo install -Dm644 contrib/systemd/satd.service /etc/systemd/system/satd.service
sudo install -Dm755 target/release/satd /usr/local/bin/satd
sudo install -Dm755 target/release/sat-cli /usr/local/bin/sat-cli
sudo useradd --system --home /var/lib/satd --shell /usr/sbin/nologin satd
sudo systemctl daemon-reload
sudo systemctl enable --now satd

The unit ships with restrictive hardening (read-only /, private /tmp, syscall filter, no new privileges). A packager who needs to relax any of those — for example to write to a non-/var/lib/satd datadir — should override via a drop-in:

# /etc/systemd/system/satd.service.d/datadir.conf
[Service]
ExecStart=
ExecStart=/usr/local/bin/satd --datadir=/srv/bitcoin
ReadWritePaths=
ReadWritePaths=/srv/bitcoin

The unit is Type=notify. satd calls sd_notify(READY=1) after every listener (RPC, P2P, optional Esplora / Electrum / MCP / events surfaces) is bound, so dependent units (Tor onion services pointing at the RPC port, watchtower processes, monitoring agents) start at the right moment instead of racing the bind sequence.

Reindex resilience

--reindex-chainstate on a fully-synced mainnet node runs for hours. satd handles this without help from the operator:

  • The unit sets a finite TimeoutStartSec=3min — deliberately not infinity. It is large enough for the first heartbeat (at 30s) to land and push the deadline out, and small enough that a pre-heartbeat startup wedge is killed in bounded time. EXTEND_TIMEOUT_USEC only works against a finite TimeoutStartSec; an infinite startup timeout would let a wedged process hang indefinitely before READY=1.
  • Every 30s during the pre-bind phase, satd emits sd_notify(EXTEND_TIMEOUT_USEC=120000000, STATUS=...). The EXTEND_TIMEOUT_USEC resets systemd's internal kill-deadline; the STATUS line shows live phase + progress in systemctl status satd.
  • The heartbeat IS the liveness check. If satd goes silent for >120s (genuinely stuck, not just slow), systemd kills the unit and the on-failure restart loop kicks in.
$ systemctl status satd
● satd.service — Bitcoin full node
     Loaded: loaded (/etc/systemd/system/satd.service; enabled)
     Active: activating (start) since Wed 2026-05-07 18:44:19 UTC
     Status: "Replaying blocks (350000/800000, 43%)"
   Main PID: 12345 (satd)

This is identical behaviour to Bitcoin Core's bitcoind.service since v22.

Running multiple networks side by side

Until a satd@.service template unit lands, operators who need signet, regtest, and mainnet on the same host can copy the unit under different names with per-instance drop-ins:

# Mainnet — the default unit installed above (satd.service).

# Signet on the same host:
sudo cp contrib/systemd/satd.service \
        /etc/systemd/system/satd-signet.service

# /etc/systemd/system/satd-signet.service.d/instance.conf
sudo install -Dm644 /dev/stdin \
        /etc/systemd/system/satd-signet.service.d/instance.conf <<'EOF'
[Service]
ExecStart=
ExecStart=/usr/local/bin/satd --signet --datadir=/var/lib/satd-signet
StateDirectory=
StateDirectory=satd-signet
ReadWritePaths=
ReadWritePaths=/var/lib/satd-signet
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now satd-signet

Same pattern for --regtest. Each instance gets its own datadir, its own satd-<network> user (or share the satd user — your call), and its own RPC port (set via --rpcport=<n> in the drop-in).

A native satd@.service template unit (systemctl start satd@signet) is a candidate for v0.1.x once we have signal that the drop-in pattern isn't enough.

OpenRC

Alpine, Gentoo (with the openrc profile), Artix, and other distros that use OpenRC. The repository ships contrib/openrc/init.d/satd.

sudo install -Dm755 contrib/openrc/init.d/satd /etc/init.d/satd
sudo install -Dm755 target/release/satd /usr/local/bin/satd
sudo install -Dm755 target/release/sat-cli /usr/local/bin/sat-cli
sudo adduser -S -H -h /var/lib/satd -s /sbin/nologin satd
sudo install -d -m 0750 -o satd -g satd /var/lib/satd
sudo rc-update add satd default
sudo rc-service satd start

OpenRC has no notify protocol — it considers the service "started" once the daemon backgrounds via start-stop-daemon. That means reindex doesn't fight any startup timeout the way it does on systemd; the unit is just running for the entire reindex.

Per-instance config can be set via /etc/conf.d/satd:

# /etc/conf.d/satd
satd_args="--prune=550 --txindex=0"

runit

Void Linux, Artix-runit, and any s6-rc-compatible setup. The repository ships contrib/runit/satd/run and a log helper at contrib/runit/satd/log/run.

sudo install -Dm755 contrib/runit/satd/run     /etc/sv/satd/run
sudo install -Dm755 contrib/runit/satd/log/run /etc/sv/satd/log/run
sudo install -Dm755 target/release/satd        /usr/local/bin/satd
sudo install -Dm755 target/release/sat-cli     /usr/local/bin/sat-cli
sudo useradd --system --home /var/lib/satd --shell /sbin/nologin satd
sudo install -d -m 0750 -o satd -g satd /var/lib/satd
sudo ln -s /etc/sv/satd /var/service/satd

runit supervises foreground processes; no daemonization, no readiness gate, no timeouts to fight with. Reindex runs as long as it needs to.

Resource budget

Mainnet, fresh IBD, no optional indexes:

ResourcePi 5 (8 GB) targetServer target
Disk (chainstate + blocks)~700 GB at 2026-05 tipsame
RAM peak during IBD~3 GBunbounded by dbcache
RAM steady-state~1.5 GB~2 GB
CPU during IBD4 cores ≈ saturatedscales with cores
Network during IBD50–200 Mbpsnetwork-bound

Optional indexes (--txindex, --addressindex, --blockfilterindex) each add disk and a one-time backfill cost. Turning them on after the fact runs an online backfill — there is no "stop, reindex from scratch" ceremony; see node/src/index/<index>/backfill.rs for the cursors.

Pruning

--prune=<MiB> works the same shape as Bitcoin Core. Minimum 550 MiB.

Indexes that scan historical blocks (--txindex, --addressindex, --blockfilterindex) require unpruned blocks. satd refuses to start with a conflicting combination — same shape as Core.

Reproducible build via Nix

The repo ships a Nix flake at flake.nix that produces deterministic satd and sat-cli binaries on x86_64-linux and aarch64-linux.

Quickstart for a packager who already has Nix with flakes enabled:

# Build (produces ./result/bin/{satd, sat-cli})
nix build .#satd

# Hash the built binaries
sha256sum result/bin/satd result/bin/sat-cli

# Drop into a dev shell with the full toolchain (clang, libclang,
# cmake, openssl, rustc, cargo, rustfmt, clippy, cargo-watch,
# cargo-nextest)
nix develop

The toolchain pin (rust-toolchain.toml at the repo root) is the single source of truth — both rustup and the flake read it.

What "reproducible" means in v1

  • Two nix build invocations of the same commit on two hosts produce byte-identical result/bin/satd. CI proves this on every PR that touches flake.nix / flake.lock / rust-toolchain.toml / Cargo.lock via .github/workflows/nix.yml (a two-runner pair build + a third compare job that asserts SHA256 equality).
  • Local repro is one command: contrib/repro/diff-build.sh /path/to/clone-A /path/to/clone-B. Runs nix build in each, hashes the outputs, and falls back to diffoscope when they diverge.
  • Out of scope for v1: matching the rustup-stable tarball binary (the one .github/workflows/release.yml ships) byte-for-byte. That requires aligning linker / debug-info / build-id behaviour between two different build drivers; tractable but a separate PR.

Determinism hazards addressed

HazardHow the flake handles it
rocksdb-sys bindgen outputrustPlatform.bindgenHook sets up libclang + the stdenv's system include paths so bindgen's translation-unit parse is reproducible. Output is deterministic for a fixed libclang version.
RocksDB native codeWe use nixpkgs's pre-built rocksdb (via ROCKSDB_LIB_DIR / ROCKSDB_INCLUDE_DIR) rather than the librocksdb-sys-vendored C++ tree. nixpkgs builds rocksdb portably (no -march=native), so cross-runner CPU variance is a non-issue. The tradeoff is a minor version mismatch between librocksdb-sys's pinned 10.4.2 and whatever nixpkgs ships (regenerated bindings either way; major API drift would surface as a compile error).
cc-rs C/C++ compiles (secp256k1, bitcoinconsensus)Compiler version pinned via nixpkgs; SOURCE_DATE_EPOCH respected by cc-rs for any timestamped output.
OUT_DIR paths in generated codecrane builds inside a content-addressed /build/source; paths are stable across hosts.
Linker build-idRUSTFLAGS=-C link-arg=-Wl,--build-id=none drops the per-build random ID.
Cargo --release profileCARGO_PROFILE_RELEASE_STRIP=symbols strips deterministically inside the derivation.
tonic_build / proto generationevents/proto/*.proto files included in the source filter; protoc is vendored via protoc-bin-vendored so no host protoc dep.

Gating policy

The Nix workflow runs on tag pushes (v*), workflow_dispatch, and PRs that change flake-specific files (the flake itself, rust-toolchain.toml, the workflow, the repro helper under contrib/repro/). It deliberately does not trigger on Cargo.lock / Cargo.toml edits — every dependency bump touches those and burns hosted-runner minutes for low-signal runs.

The Nix and Release workflows fire in parallel at tag-cut time and don't gate each other; a Nix-side failure means the released tarball can't claim Nix-rebuilt provenance for that tag and should be fix-forwarded.

Reconsider both the trigger scope (broader PR-trigger) and a hard Release-gates-on-Nix dependency once the repo flips public (Actions minutes free).

flake.lock

The first PR that lands the flake intentionally does not commit flake.lock because the maintainer who lands it does not have Nix on their workstation. The CI workflow is gated to workflow_dispatch

  • flake-touching PRs + tag pushes; the first workflow_dispatch run by a Nix-capable maintainer (or from a CI runner) will generate the lock, after which it should be committed and the PR description updated. Subsequent PRs run against the committed lock.

Renovate (or a manual cadence) bumps the lock weekly. Bumps that change flake.lock re-trigger the repro check; if reproducibility breaks under a new input revision, the bump is reverted and the hazard is investigated.

What's intentionally not in this flake

  • macOS reproducibility (aarch64-darwin) — deferred until the repo flips public; macOS builds in the release workflow are also currently disabled for the same reason.
  • musl targets — same rocksdb-sys + musl cross-toolchain reason the release workflow defers them.
  • A NixOS module / Home Manager output — packagers should write their own service definitions; the contract this doc describes is the input.
  • A maintainer-owned binary cache (Cachix) — adds a key-custody surface we're not taking on yet. The CI uses the ephemeral magic-nix-cache action for speedup only.

Bitcoin Core uses Guix; satd targets Nix as the primary reproducible build because the workspace is pure-Cargo and Nix integration is substantially shorter to specify. A Guix manifest may follow if a downstream packager needs it.

Release artifacts

Tag-triggered (v*) releases produce, per tag, via .github/workflows/release.yml running on hosted GitHub runners:

  • satd-<version>-<target>.tar.zst for the targets currently shipped:

    • x86_64-unknown-linux-gnu
    • aarch64-unknown-linux-gnu
    • x86_64-unknown-linux-musl (statically-linked musl)
    • aarch64-unknown-linux-musl (statically-linked musl)
    • aarch64-apple-darwin (macOS Apple Silicon)

    x86_64-apple-darwin is not built in the standard release matrix — macos-13 is being deprecated by GitHub and Apple Silicon is the targeted macOS surface. Operators who need x86_64 darwin can cross-compile from an arm64 darwin host (cargo build --release --target=x86_64-apple-darwin).

    Each tarball contains stripped satd + sat-cli binaries and the authoritative reference docs (README.md, PACKAGING.md, CORE_DIFFERENCES.md, STABILITY_POLICY.md), plus a MANIFEST file pinning the build commit, target triple, Rust toolchain version, and build timestamp.

  • A per-tarball *.sha256 file alongside each artifact, plus an aggregate SHA256SUMS covering tarballs and SBOMs in the release.

  • A multi-arch container at ghcr.io/epochbtc/satd:<version> covering linux/amd64 + linux/arm64.

  • CycloneDX 1.5 JSON SBOMs for each shipped binary:

    • satd-v<version>.cdx.json
    • sat-cli-v<version>.cdx.json

    Each ships with a *.sha256 next to it (already in SHA256SUMS) and a *.minisig produced by the same maintainer-side contrib/release/sign-tarballs.sh flow that signs the tarballs.

The release workflow is triggered on tag pushes (v*) and manual triggers (workflow_dispatch), building the full set of binary, container, and SBOM artifacts in parallel.

Signed releases

Three independent signing surfaces. Verifier commands and key custody details live in SECURITY.md.

  • Tarballs — minisign Ed25519. Each .tar.zst ships with a detached .minisig. Pubkeys (primary + cold spare) in SECURITY.md. Maintainer signs offline with passphrases gated by 1Password + YubiKey 2FA — the signing key is never present in CI. Maintainer runbook: contrib/release/sign-tarballs.sh <tag>.

  • Container image — cosign keyless OIDC. No signing key in custody. The merge-manifest CI job mints a short-lived cert from GitHub Actions OIDC and the attestation is logged to Rekor. Verify with:

    cosign verify ghcr.io/epochbtc/satd:<version> \
      --certificate-identity-regexp \
        'https://github.com/epochbtc/satd/.github/workflows/release.yml@refs/tags/v.*' \
      --certificate-oidc-issuer https://token.actions.githubusercontent.com
    
  • Git tags — SSH signatures. Annotated tags are signed by the maintainer's SSH key. Source-of-truth for the trusted pubkey set is https://github.com/bkeroack.keys (delegating to GitHub avoids a stale pinned file as machines rotate). Verify with the bundled helper:

    contrib/release/verify-tag.sh v0.1.0
    

Software Bill of Materials

Each release ships a CycloneDX 1.5 JSON SBOM per binary:

# Authenticate the SBOM (same key + recipe as the tarballs)
minisign -Vm satd-v0.1.0.cdx.json \
  -P RWQeP6MczCgPh6tU03GEMm4HsnGbXte3VT2Bc52TBSR7Q+X7WnL5vfQ3

# Enumerate dependencies — name, version, license
jq -r '.components[] | "\(.name) \(.version) \(.licenses[0].license.id // .licenses[0].license.name // "?")"' \
  satd-v0.1.0.cdx.json | sort

The SBOM is generated from the same Cargo.lock that produced the released binary; the cargo cyclonedx invocation lives in the sbom job in .github/workflows/release.yml. The dep graph is identical across the gnu-linux release targets currently shipped (x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu), so a single SBOM per binary covers both tarballs.

If a future release adds musl or macOS targets — which can resolve different platform-specific deps (e.g. libc shim crates, security-framework on darwin) — the workflow will need to emit a per-target SBOM and the artifact filenames will gain a target-triple suffix. Track this when re-enabling the deferred targets in the release matrix.

Supply-chain policy

deny.toml at the repo root encodes the supply-chain policy enforced by cargo-deny:

  • Advisories — every RustSec advisory against any dep in the workspace fails CI by default. Exceptions are documented in [advisories.ignore] with a reason field naming the rationale.
  • Licenses — permissive only (MIT / Apache-2.0 / BSD / ISC / Unicode / CC0 / Zlib / Unlicense / MPL-2.0 family). GPL-* and AGPL-* are denied implicitly.
  • Bans — wildcards on crates.io deps are denied; workspace-internal path = "../foo" deps are allowed via allow-wildcard-paths because every workspace crate is publish = false.
  • Sources — only https://github.com/rust-lang/crates.io-index. Git deps require an explicit allowlist entry.

The policy runs as a hard gate in two places:

  • .github/workflows/deny.yml — every PR that touches Cargo.toml, Cargo.lock, deny.toml, or the workflow itself.
  • The supply-chain-gate job inside .github/workflows/release.yml — every release artifact (tarballs, SBOMs, container) needs: it. A new RustSec advisory landed during a quiet period between merges cannot ship a release.

Known deferred items

  • cargo-auditable — Embedding the dependency manifest directly into the compiled binaries for improved runtime supply-chain verification.

Stability contract

Shipped surfaces (RPC method shapes, CLI flag shape, bitcoin.conf syntax, file layout described above) are governed by STABILITY_POLICY.md. Tier 1 (Core-compat) is the strongest: a breaking change requires a deliberate, scoped proposal with a demonstrated migration story for downstreams.

Packaging contacts

If you are packaging satd for an ecosystem (Umbrel, Start9, Debian, Nix, Homebrew, etc.) and need a contract change, file an issue tagged packaging against the epochbtc/satd repo. We treat packaging breakage as a P1.


Versioning

This document is versioned alongside satd. Changes that shift the contract (file layout, signals, default ports) are called out in the release notes for the version that ships them.

VersionNotable changes
0.1.0 (current)Initial PACKAGING.md. Dockerfile + systemd unit shipped. Tag-triggered release workflow on hosted runners produces tarballs (gnu-linux + Apple Silicon) and a multi-arch GHCR image. Signing across all three surfaces (minisign tarballs, cosign keyless image, SSH-signed tags) shipped. Nix flake (flake.nix) shipped for reproducible builds with two-runner CI verification (x86_64-linux, aarch64-linux). CycloneDX 1.5 SBOMs per binary + cargo-deny supply-chain gate (PR-time on dep-graph PRs, hard gate at tag time) shipped. systemd unit upgraded to Type=notify with sd_notify heartbeats so --reindex-chainstate doesn't fight TimeoutStartSec; OpenRC and runit unit equivalents shipped.