One command to run a Stacks stack — bitcoind, stacks-node, stacks-signer, the Stacks Blockchain API, the Stacks Mesh API, and Postgres — from a single config file.
stacksup config init # write a stacks.toml
stacksup start [service] # validate, render, start enabled services (--no-render to skip render)
stacksup status # state of every service (managed and external)
stacksup config check # config coherence + connectivity checks
stacksup logs [service] # follow service logs
stacksup logs export # shareable, redacted support bundle (logs + diagnostics)
stacksup stop [service] # stop enabled services, keep containers+logs (--destroy to remove)
stacksup restart [service] # stop + start so config/image changes take effect
stacksup pull # pull the latest images for every enabled service
stacksup upgrade # check registries for newer image versions (suggestions only)
stacksup config render # regenerate rendered/ without starting anything
stacksup chainstate wipe [service] # delete on-disk state, all or one service (asks first)
stacksup chainstate status # compare every service's chain tip (stacks + bitcoin heights)
stacksup chainstate download # seed chainstate from the Hiro Archive (resumable, verified)Grab a binary from GitHub Releases
(macOS arm64/x86_64, Linux arm64/x86_64 — the Linux builds are fully static)
and put stacksup on your PATH. Or run it as a container: the image drives
the host's docker daemon, and the compose file it renders uses bind
mounts the daemon resolves as host paths — so mount your deployment
directory at the same path inside the container:
docker run --rm -it \
-v /var/run/docker.sock:/var/run/docker.sock \
-v "$PWD":"$PWD" -w "$PWD" \
ghcr.io/stx-labs/stacksup startStandard network definitions (burnchain endpoint, chain id, epochs, seeded
balances, bootstrap peers) live in networks/ and ship embedded
in the binary — network = "testnet" in stacks.toml references them by name.
A new testnet (new chain id, new epochs) is a new file there, not a config
migration. Unknown names resolve as custom definition files next to your
stacks.toml (networks/<name>.toml), so you can define private networks
without a tool release.
| network | burnchain | chain id |
|---|---|---|
mainnet |
Bitcoin mainnet (bring your own bitcoind) | 0x00000001 |
testnet |
Hiro-hosted bitcoin regtest (krypton) | 0x80000000 |
staking-testnet |
Hiro-hosted custom bitcoin signet (PoX-5 / Bitcoin staking; stacks-core 4.0.4+) | 0x00000500 |
stacks.toml is the single source of truth. Every service has a mode:
| mode | meaning |
|---|---|
enabled |
run by this tool via docker compose |
external |
you run it elsewhere; we wire configs to it, health-check it, never touch it |
disabled |
not part of this stack (dependents fail validation) |
Everything lives under --data-dir (default: the current directory):
rendered/ holds the compose file and generated service configs, and chainstate/
holds service state (chainstate, Postgres, signer db) as bind mounts — so the
whole stack sits where you said, ready to back up or relocate as one unit.
Everything under rendered/ — the compose file, node Config.toml, signer
config, API env — is generated from stacks.toml. Cross-service invariants
(node↔signer auth token, event-observer endpoints, chain IDs) are correct by
construction because they derive from one file. If you outgrow this tool,
take rendered/ and leave: it's plain compose + config files.
When the node is external but the API or signer is enabled, the node must
be configured to push to them; stacksup config render emits
rendered/apply-to-your-node.toml with the exact blocks to add on your side,
and stacksup config check verifies the loop is closed.
Containers are segmented across three docker networks so a compromised
API-side container has no route to the signer or to bitcoind's RPC
interface: bitcoin (bitcoind + node), core (node + signer), and
services (API, mesh API, Postgres). The node joins each network only when
it's present — everything talks to the node. bitcoind's RPC port is
published loopback-only (unless the node is external and needs it
off-host), so containers can't sidestep the split via
host.docker.internal; the P2P port stays open on purpose — it exists to
accept peers from anywhere.
Any deployment with a node can enable Signer Sidekick, the PoX-5 operations dashboard that monitors registration, pool membership, rewards, and signer health against your node. A signer in the deployment is recommended (it feeds the Signer Health page's direct telemetry) but not required — sidekick can monitor any deployed, compatible signer-manager read-only:
[signer-sidekick]
mode = "enabled"
manager_principal = "SP....signer-manager" # your deployed PoX-5 signer-managerSidekick's database lives under chainstate/signer-sidekick/ like every
other service's state (the container runs as the directory's owner, so the
image's non-root user can write the bind mount). For an online backup while
sidekick is running — especially before relying on an operator-run gas
wallet — use its own tooling:
docker compose -f rendered/docker-compose.yml run --rm --no-deps signer-sidekick database backup /data/backup.sqlitestacksup wires it by construction: node RPC and (when managed) node/signer
telemetry endpoints, the managed stacks-api as its indexed API (falling back
to the network's Hiro API), the dashboard auth token from secrets.toml
([signer-sidekick] auth_token), and the release's manager/compatibility
profiles fetched once per version. The dashboard publishes loopback-only on
port 3997 (shifted by port_offset); stacksup chainstate wipe signer-sidekick wipes its database. The engine defaults to observe; set
engine_mode = "operator-run" only after reading sidekick's operator docs
(its gas wallet is created inside the dashboard, never in config files).
One machine can host several stacks side by side. Give each deployment its
own directory (config + --data-dir), a distinct name, and a
port_offset:
name = "testnet-b" # compose project + container prefix (default: "stacks")
port_offset = 100 # shifts every published HOST port; container-internal
# ports and service wiring never changeWith port_offset = 100 the node RPC publishes on 20543, the API on 4099,
postgres on 5532, and so on. The rendered compose file embeds the project
name, so stacksup commands (and bare docker compose -f runs) are always
scoped to the deployment whose directory you're in — stop, logs, and
chainstate wipe can't touch a neighbour.
stacksup start refuses to run before doing damage when it detects a
collision: it test-binds every port it is about to publish (pointing at
port_offset when one is taken) and rejects a name already in use by a
stack rendered from a different directory (which compose would otherwise
silently adopt).
Credentials never live in stacks.toml — the tool rejects them there. They go
in a secrets.toml beside it (plain text, mode 0600, gitignored), which is
merged into the config at load time:
[postgres]
password = "..."
[bitcoind]
rpc_user = "..."
rpc_password = "..."
[stacks-node]
auth_token = "..."stacksup config init generates one with random values when none exists;
an existing secrets.toml is yours and is never modified or overwritten
(not even by init --force) — if a required value is missing, the tool errors
out with a paste-ready snippet of exactly what to add. The file must be
owner-only (chmod 600), and values must be 8–128 characters of printable
ASCII without spaces, quotes, backslashes, $, or backticks (they are
interpolated into rendered TOML/env/compose files). At render time the
Postgres password is delivered as a compose secret file and the bitcoind
credentials become a derived -rpcauth hash, so no plain-text secret appears
in docker inspect.
cargo run -- config init
cargo run -- config render
cargo build --release # binary at target/release/stacksupstacksup chainstate download fetches the node chainstate tarball and/or the
API's Postgres dump from the Hiro Archive,
verifies sha256, and restores them (untar for the node, pg_restore for the
API). Downloads are resumable — Ctrl-C and re-run any time. Useful flags:
--service node|api|all, --archive <file|url|path> to pin a specific
archive, --check-only for a dry-run plan, --yes for unattended runs
(nohup stacksup chainstate download --yes &), --no-verify,
--skip-version-check, --keep-archives, and --start to render and
start the deployment as soon as the restore finishes (seed + boot in one
command: stacksup chainstate download --yes --start). Always resolves versioned archives (never -latest pointers); the archive's version must be ≤
the service's configured version in stacks.toml.
-
status --watch: live sync progress (bitcoind headers, node tip vs peers via/v3/health, API ingest lag) via bollard - Snapshot seeding on first
up: Hiro archive chainstate + matching API pg_dump, resumable, checksummed -
doctor: chain-id cross-checks, event-stream-flowing check, node↔signer auth verification - Secrets: user-owned
secrets.tomloverlay beside stacks.toml — pg password via compose secret file, bitcoind via rpcauth hash, node/signer auth token -
upgrade: image update with pre-upgrade pg backup, ordered restart, post-check -
snapshot: stop-consistent chainstate + pg_dump pairs with version metadata - Profiles:
exchange(readonly API replicas, pruned mode), richersigner(monitor-signers wiring) - Release: binaries on GitHub Releases + multi-arch image on GHCR (
.github/workflows/release.yml); Homebrew tap still to come - Pin real image tags (stacks-core, signer, API, mesh API)