Everything not listed in this document should behave the same as upstream BTCPay Server. 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.
BTCPay Server is a self-hosted Bitcoin payment processor. This package bundles the pieces upstream's Docker deployment expects you to run yourself — PostgreSQL, the NBXplorer UTXO tracker and, for Monero, a wallet RPC — as private sidecars, and wires them, your Bitcoin node, and an optional Lightning or Monero node together without any addresses being typed in.
- Upstream repo: https://github.com/btcpayserver/btcpayserver
- Wrapper repo: https://github.com/Start9Labs/btcpayserver-startos
- 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
- Limitations and Differences
- Quick Reference for AI Consumers
Five unmodified published images. Three run always; the Monero wallet RPC only while Monero is enabled, and the Shopify deployer only while that plugin is switched on.
| Property | Value |
|---|---|
| Images | btcpayserver/btcpayserver, nicolasdorier/nbxplorer, btcpayserver/postgres, btcpayserver/shopify-app-deployer, ghcr.io/sethforprivacy/simple-monero-wallet-rpc |
| Architectures | x86_64, aarch64 |
| Entrypoint | Each image's own, except monero-wallet-rpc, which runs monero-wallet-rpc directly |
| Subcontainer | Purpose |
|---|---|
btcpay |
The btcpay daemon — the server and its web UI, and the one to attach to |
nbx |
NBXplorer, the UTXO tracker that watches the chain on BTCPay's behalf |
postgres |
The private database, shared by BTCPay and NBXplorer as two separate databases |
monero-wallet-rpc |
The wallet RPC that BTCPay's Monero plugin keeps its view-only wallet in; present only while Monero is enabled |
shopify |
The Shopify app deployer; present only while that plugin is enabled |
Startup is ordered: postgres first, then nbxplorer, then btcpay, and shopify after that. While Monero is enabled, monero-wallet-rpc comes up before btcpay, and btcpay restarts whenever it does: BTCPay's Monero plugin opens its wallet only as BTCPay starts. One oneshot, reset-start-height, runs once NBXplorer is up — see Actions. While Monero is enabled, a second, chown-monero-wallets, hands the wallet directory to the image's monero user, which the wallet RPC runs as.
Postgres listens on loopback only and runs with trust authentication, which is safe because that loopback is inside this service's own network namespace: nothing outside the service can reach it, and the two databases are never exposed. The Monero wallet RPC listens on 127.0.0.1:18082 without RPC authentication, on the same terms.
Four volumes, and one of them never enters a container. The same volume can appear at different paths in different subcontainers.
| Volume | Mounted at | Purpose |
|---|---|---|
btcpayserver |
/datadir in btcpay, with its Plugins subdirectory also at /root/.btcpayserver/Plugins, and its btcpayserver/altcoins/monero/wallets subdirectory at that same path under /datadir in monero-wallet-rpc |
BTCPay's data directory, its settings.config, installed plugins, and the Monero wallet files |
nbxplorer |
/datadir in nbx, and /root/.nbxplorer in btcpay |
NBXplorer's data directory, its settings.config, and its cookie — which BTCPay reads to authenticate to it |
db |
/var/lib/postgresql in postgres |
The PostgreSQL data directory |
main |
— (host side) | store.json, any superseded-* directory set aside by the legacy-layout move, and the old btcpayserver/altcoins/monero directory it leaves in place; never mounted into a container |
Dependency volumes are mounted in as needed:
| Mount | Source | Access | Used for |
|---|---|---|---|
/root/.bitcoin (in nbx) |
Bitcoin's main volume |
read-write | NBXplorer reads the node's RPC cookie |
/mnt/lnd |
LND's main volume |
read-only | The admin macaroon and TLS certificate |
/mnt/cln |
Core Lightning's main volume |
read-only | The lightning-rpc unix socket |
/mnt/eclair |
Eclair's main volume |
read-only | eclair.conf, for its API password |
Three models. Two are the applications' own INI configuration, and in both the package owns the wiring while you own the handful of settings the actions expose.
| File | Format | Modelled | Written by |
|---|---|---|---|
btcpayserver:/Main/settings.config |
INI | Yes — FileHelper.ini |
Every init, every start, and the Lightning, Altcoin actions |
nbxplorer:/Main/settings.config |
INI | Yes — FileHelper.ini |
Every init, every start, the Resync action, and its oneshot |
main:store.json |
JSON | Yes — FileHelper.json |
Every init, and the Plugins action |
Both files are wiring, not tuning. They carry the addresses and credentials linking BTCPay, NBXplorer, Postgres, and your nodes — values that must be correct rather than chosen — plus the few settings the actions expose. Everything else about BTCPay is configured inside BTCPay's own UI and stored in its database, not in a file this package can rewrite.
Enforced — rewritten to a fixed value whenever the package writes the file: network, bind, btcexplorercookiefile, explorerpostgres, postgres, debuglog, dockerdeployment, updateurl, and the two Monero wallet-daemon paths. BTC.explorer.cookiefile is modelled as "must be absent" and is deleted if present — the correctly-cased key above replaces it.
Two of those keep BTCPay from advertising updates it cannot perform here, since StartOS does the updating: dockerdeployment=false hides the Maintenance page and its update button, and an empty updateurl disables the daily GitHub release check — which also removes the "check releases on GitHub" toggle from Server Settings → Policies and stops first-admin registration from switching it on. Plugin updates are a separate mechanism and still work normally from BTCPay's UI.
Written on every start, from addresses resolved over the service bridge rather than stored: socksendpoint (Tor's SOCKS proxy), XMR_daemon_uri (when Monero is enabled), and the server= half of btclightning when the backend is LND, or the whole of it when the backend is Eclair. Editing any of these by hand does not survive a restart, and does not need to — they heal themselves when a dependency is installed, removed, or re-ported.
Yours, through an action: btclightning selects the Lightning backend, chains enables Monero, and ssrfexceptions stores trusted private destinations. ssrfexceptions defaults to empty and survives restarts and backups; it is not generated from dependency addresses. The configured internal Lightning node uses upstream's trusted client and needs no exception. XMR_daemon_username and XMR_daemon_password default to empty and are not exposed.
Enforced: port, bind (loopback), network=mainnet, btc.rpc.cookiefile, and postgres. The obsolete mainnet key is removed when the file is written. btc.rpc.user and btc.rpc.password are modelled as "must be absent" — NBXplorer authenticates to Bitcoin with the cookie it reads through the mount, never with a stored password.
Written on every start: btc.rpc.url and btc.node.endpoint, both resolved from Bitcoin's own bindings.
Transient: btc.rescan and btc.startheight are set by Resync NBXplorer and reset to their defaults by the reset-start-height oneshot as soon as NBXplorer comes back up, so a rescan happens once rather than on every subsequent start.
plugins.shopify only — whether the Shopify sidecar runs. It lives on the main volume, apart from either application's data.
One is required; the rest appear according to what you have enabled.
| Dependency | Required? | Kind | Health check | Why |
|---|---|---|---|---|
| Bitcoin | always | running |
bitcoind |
Chain data for NBXplorer, over both RPC and P2P |
| LND | when selected as the backend | running |
lnd |
Lightning invoices |
| Core Lightning | when selected as the backend | running |
lightningd |
Lightning invoices |
| Eclair | when selected as the backend | running |
eclair |
Lightning invoices |
| Monero | when Monero is enabled | running |
monerod |
Monero payments |
NBXplorer connects to Bitcoin's whitelisted P2P binding, not its public one. It pulls blocks over that connection, and Bitcoin's ordinary peer binding grants no permissions — a connection there shares the pool with anonymous inbound peers, so it can be evicted to seat another peer or cut off by the upload target. The peer-local host is whitelisted, and neither applies.
Bitcoin Knots (pre-RDTS) satisfies the Bitcoin dependency as well as Bitcoin Core.
Enabling Monero also raises a task on Monero itself — see Tasks.
One interface. NBXplorer, Postgres, the Monero wallet RPC, and the Shopify deployer are all internal and never published.
| Interface | Id | Type | Port | Description |
|---|---|---|---|---|
| Web UI | main |
ui | 23000 | The BTCPay Server web interface |
The port is bound on the main MultiHost and is not masked. Updating an install carried over from StartOS 0.3.5 retires that package's port 80 binding on the same host, which nothing listens on any more.
Install seeds the three models with their defaults and nothing else — no credentials are generated and no task is raised. Account creation is BTCPay's own: the first visit to the web UI registers the server admin.
Two ordering points matter, and both come from dependencies rather than from setup:
- Bitcoin must be installed and running, and NBXplorer cannot report itself synced until Bitcoin is. On a fresh node that is the length of an initial block download, followed by NBXplorer's own scan — both are reported as progress rather than as failures. See Health Checks.
- Choosing a Lightning node is a two-step operation. The Choose Lightning Node action grants BTCPay access to the node; BTCPay then has to be told to use it, inside its own Lightning settings.
An install carried over from the older single-main layout has its data moved onto the four volumes on init, and that first start is long — it moves a database, and the Postgres image upgrades the cluster before accepting connections. The trigger is the old cluster still sitting under main, not a version, so an empty BTCPay on such an install — no stores, no accounts, every password rejected — means the move has not run yet, and restarting the service runs it. Where a destination volume was not already empty, what was in it is set aside under main/superseded-<timestamp>/ rather than overwritten. The Monero wallet directory of the old layout, main/btcpayserver/altcoins/monero, is owned outside the range the container can map, so the move can neither read nor delete it and leaves it where it is; a Monero wallet from that layout is set up again in BTCPay.
Six actions, all user-facing.
Selects which Lightning node BTCPay may use — LND, Core Lightning, Eclair, or neither. Run it after installing the node.
- What it changes:
btclightningin BTCPay's config, and through it the package's dependency set and its mounts. LND is recorded as a REST address resolved at that moment; Core Lightning as a path to the RPC socket it will mount. Eclair is recorded as its API address alone — the password that completes the connection string lives on Eclair's volume, which onlymainmounts, so it is filled in on the next start. - Cost: seconds, then a restart, since the node's data volume can only be mounted when the container is recreated.
- Repeat safety: safe to re-run and safe to reverse; selecting "None/External" removes the mount and the dependency.
- Guard: selecting LND fails with a message if LND is not yet reachable, rather than recording an address that does not work.
- What happens next: BTCPay does not start using the node on its own. Open its Lightning settings, choose the internal node, and save.
Turns Monero support on or off.
- What it changes:
chainsin BTCPay's config, and through it the Monero dependency, themonero-wallet-rpcdaemon, and the block-notify task described under Tasks. - Cost: seconds, then a restart.
- Repeat safety: safe both ways; disabling removes the dependency.
Turns the Shopify integration on or off.
- What it changes:
plugins.shopifyinstore.json, which decides whether the Shopify sidecar runs at all. - Cost: seconds, then a restart.
- Repeat safety: safe both ways.
Run when a private webhook, LNURL endpoint or custom Lightning connection is rejected by outbound-request protection.
- What it changes:
ssrfexceptionsin BTCPay's config; it replaces the complete allowlist without disabling SSRF protection. - Cost: seconds, followed by a BTCPay restart.
- Repeat safety: safe to repeat; clearing the field revokes all configured exceptions. Trust applies to the whole destination, not a URL path.
- Scope: the action accepts exact ASCII hostnames and IP addresses, optionally port-restricted, not CIDR ranges. An allowed hostname trusts every address it resolves to. It grants no access through StartOS's network controls.
Rescans the chain from a chosen block height. Run it when BTCPay is missing transactions it should have seen — typically after importing an existing wallet with history.
- What it changes: NBXplorer's
btc.startheightandbtc.rescan, then restarts the service. The oneshot clears both once NBXplorer is running again, so the rescan is not repeated on later starts. - Cost: the rescan itself, which is proportional to how far back you start; the service is restarted immediately.
- Repeat safety: safe to re-run. It does not destroy anything — a rescan re-reads the chain into NBXplorer's database.
Sets a temporary password on the first server-admin account. Run it when locked out.
- What it changes: that account's password hash in the BTCPay database, written directly over Postgres.
- Availability: only while the service is running, because it goes through the running database.
- Confirmation: asks before running, since the account's current password stops working.
- Repeat safety: safe to re-run; each run generates a fresh password.
- Outputs: the new password, masked and copyable, shown once. Change it after logging in.
- Guards: it refuses when no server admin exists, and refuses when more than one does — with more than one, the right recovery is for another admin to reset the account from inside BTCPay.
The package raises no task on itself. It raises them on other services, which is why they appear on a page that does not explain where they came from.
| Task | Raised on | Severity | Raised when | Cleared when |
|---|---|---|---|---|
| Auto-Configure (block notify) | Monero | important |
Monero is enabled here and its block-notify is not the command BTCPay needs |
Monero's config matches; it returns if changed again |
| Revoke Macaroons | LND | critical |
Upgrading to 2.4.2:1 with LND selected |
LND's action runs |
| Revoke Runes | Core Lightning | critical |
Upgrading to 2.4.2:1 with Core Lightning selected |
Core Lightning's action runs |
The Monero task is a standing condition rather than a one-off: it re-raises whenever Monero's block-notify setting stops matching, because without that callback BTCPay is not told about new blocks.
The two credential-rotation tasks were raised once, by the upgrade that shipped a security fix, on the reasoning that this server could read those credentials and so they should be rotated. They are critical, so BTCPay will not start until the node's action has run — including when the selected node is not installed yet, as after restoring BTCPay before it.
Five checks at most, and the two that matter to a user are both about sync.
| Check | Displayed | Method | Grace | Present |
|---|---|---|---|---|
btcpay |
"Web Interface" | HTTP GET /api/v1/health |
60s | always |
nbxplorer |
"UTXO Tracker" | NBXplorer's port is listening | 30s | always |
utxo-sync |
"UTXO Tracker Sync" | NBXplorer's status API, authenticated with its cookie | — | always |
postgres |
— internal | pg_isready |
— | always |
monero-wallet-rpc |
"Monero Wallet RPC" | The wallet RPC's port is listening | — | while Monero is enabled |
shopify |
"Shopify Plugin" | The deployer's port is listening | — | while the plugin is enabled |
utxo-sync is the one to read, because it distinguishes the two things that can be behind. While Bitcoin is still syncing it reports Bitcoin's own progress and says so explicitly — nothing is wrong, and NBXplorer cannot get ahead of its node. Once Bitcoin is synced it reports NBXplorer's own scan progress instead. It fails only when it cannot reach the node at all.
utxo-sync also restarts NBXplorer's sync when it stalls. If Bitcoin is synced but NBXplorer sits behind it without moving for 15 minutes, the check drops NBXplorer's P2P connection to Bitcoin, and NBXplorer reconnects and catches up. Each time, it logs UTXO tracker stuck at block N while Bitcoin is synced; reconnecting it to Bitcoin. NBXplorer can stall this way after Bitcoin catches up from an outage, and it never leaves that state on its own.
postgres has display: null — it exists so that a failed database restarts the service, not to be read. A service restarting with no failing check on screen is usually this one; the service logs name it.
btcpay failing after a long grace period means its health endpoint is unreachable, returns an HTTP error, or does not return the expected JSON boolean synchronized. The request times out after five seconds and rejects redirects. A valid response with synchronized: false still establishes readiness; utxo-sync reports chain progress separately. Because BTCPay starts only after Postgres, NBXplorer and, while Monero is enabled, the wallet RPC are ready, a failing readiness check points at BTCPay itself rather than at its sidecars.
monero-wallet-rpc failing keeps BTCPay stopped until it recovers. It passes whether or not Monero is reachable; the store's Monero settings page in BTCPay shows whether Monero is reachable and synced, and whether a wallet is loaded.
The strategy is mixed, and the distinction decides what a restore actually gives you.
dbis dumped, not copied.Backups.withPgDumpruns a logical dump of thebtcpayserverdatabase only. The volume's files are never captured, and the dump is replayed into a fresh database on restore — which is what lets a future Postgres image read it.btcpayserverandmainare copied wholesale — BTCPay's data directory, itssettings.config, installed plugins, the Monero view-only wallet,store.json, and anysuperseded-*directory.nbxploreris not backed up at all, and neither is thenbxplorerdatabase inside Postgres. Both are derived from the chain.
So a restored instance has your stores, invoices, users, and settings, but no UTXO tracker state. NBXplorer rebuilds it by scanning the chain, which takes time proportional to how much history your wallets have; Resync NBXplorer is there for when that scan needs to start further back than it did on its own. The Bitcoin node must be present and synced before any of it can begin.
- PostgreSQL and NBXplorer are private sidecars. They belong to this service, cannot be shared with another, and cannot be substituted for an external instance.
- Only a Lightning node on this server can be selected through the action. An external node is still possible, but it is configured inside BTCPay as "custom", not here.
- Monero requires a matching setting on the Monero service, which is why enabling it raises a task there rather than just working.
- NBXplorer state is not in backups. A restore is followed by a rescan.
- The Shopify integration runs as a separate sidecar and only when explicitly enabled.
- A
superseded-*directory is never reaped. Nothing removes it and it is copied into every backup; deleting it is the operator's call, once they have confirmed the data it displaced is back. - No riscv64 build. x86_64 and aarch64 only.
- The Monero plugin is not bundled. It is installed from BTCPay's Plugin Directory; Enable Altcoins sets up what surrounds it — the Monero dependency, its block-notify task, and the wallet RPC the plugin connects to.
- Monero's RPC credentials must stay disabled. BTCPay and its wallet RPC connect to Monero's restricted RPC without a username or password.
package_id: btcpayserver
image: btcpayserver/btcpayserver # plus nbxplorer, postgres, shopify-app-deployer, simple-monero-wallet-rpc
architectures:
- x86_64
- aarch64
subcontainers:
- btcpay # the server; the one to attach to
- nbx # NBXplorer
- postgres # private database
- monero-wallet-rpc # only while Monero is enabled
- shopify # only while the Shopify plugin is enabled
volumes:
btcpayserver: /datadir
nbxplorer: /datadir (in nbx), /root/.nbxplorer (in btcpay)
db: /var/lib/postgresql
main: host side (store.json)
file_models:
- btcpayserver:/Main/settings.config
- nbxplorer:/Main/settings.config
- main:store.json
startos_managed_env_vars:
- BTCPAY_DATADIR
- BTCPAY_SHOPIFY_PLUGIN_DEPLOYER
- LC_ALL
- NBXPLORER_DATADIR
- POSTGRES_HOST_AUTH_METHOD
dependencies:
- bitcoind # required
- lnd # when selected as the Lightning backend
- c-lightning # when selected as the Lightning backend
- eclair # when selected as the Lightning backend
- monerod # when Monero is enabled
interfaces:
main: { type: ui, port: 23000 }
actions:
- lightning-node
- enable-altcoins
- enable-plugins
- configure-private-destinations
- resync-nbx
- reset-admin-password # only-running
tasks: # all raised on other services, not on this one
- { action: autoconfig, severity: important } # on monerod
- { action: revoke-macaroons, severity: critical } # on lnd, at the 2.4.2:1 upgrade
- { action: revoke-runes, severity: critical } # on c-lightning, at the 2.4.2:1 upgrade
health_checks:
- btcpay # displayed "Web Interface"
- nbxplorer # displayed "UTXO Tracker"
- utxo-sync # displayed "UTXO Tracker Sync"
- postgres # internal
- monero-wallet-rpc # displayed "Monero Wallet RPC"; only while Monero is enabled
- shopify # displayed "Shopify Plugin"; only while enabled