Sansbank Sansbank Dev Docs ← Back to Dev Center

Deploy on a fresh VPS with Docker (verified)

This is a complete, step-by-step guide to a production deployment on a brand-new VPS (Ubuntu/Docker), verified end-to-end. Bring your own reverse proxy (Cloudflare Tunnel, nginx, …).

1. Provision the VPS

  • A small VPS is fine (2 vCPU / 4 GB RAM / 30 GB disk is sufficient — BTCPay + the plugin add ~2.8 GB RSS over Docker).
  • Install Docker and the .NET 10 SDK (only needed to build the plugin and .btcpay).
  • Create a dedicated Docker network and a Postgres container:
docker network create btcpay-network
docker run -d --name btcpay-pg --network btcpay-network --restart unless-stopped \
  -e POSTGRES_PASSWORD='<postgres-password>' -p 127.0.0.1:5432:5432 postgres:16
docker exec btcpay-pg psql -U postgres -c "CREATE DATABASE btcpayserver;"

2. Build the plugin

git clone https://github.com/sansbankdao/btcpay-dash-evolution.git && cd btcpay-dash-evolution
dotnet build Plugins/DashEvolution/BTCPayServer.Plugins.DashEvolution.csproj -c Release
OUT=Plugins/DashEvolution/bin/Release/net10.0

mkdir -p ~/btcpay-docker/plugins/BTCPayServer.Plugins.DashEvolution
cp $OUT/BTCPayServer.Plugins.DashEvolution.dll ~/btcpay-docker/plugins/BTCPayServer.Plugins.DashEvolution/
cp $OUT/BTCPayServer.Plugins.DashEvolution.deps.json ~/btcpay-docker/plugins/BTCPayServer.Plugins.DashEvolution/
cp Plugins/DashEvolution/BTCPayServer.Plugins.DashEvolution.json ~/btcpay-docker/plugins/BTCPayServer.Plugins.DashEvolution/
# Package the distributable .btcpay archive too (manifest + dll + deps.json at the zip root):
STAGE=$(mktemp -d)
cp $OUT/BTCPayServer.Plugins.DashEvolution.dll $OUT/BTCPayServer.Plugins.DashEvolution.deps.json $STAGE/
cp Plugins/DashEvolution/BTCPayServer.Plugins.DashEvolution.json $STAGE/
(cd $STAGE && zip ~/btcpay-docker/plugins/BTCPayServer.Plugins.DashEvolution.btcpay ./*) && rm -rf $STAGE

The plugin must be present as an EXTRACTED directory named by its Identifier (BTCPayServer.Plugins.DashEvolution/, containing BTCPayServer.Plugins.DashEvolution.json

  • the dll + deps.json). A .btcpay file alone is NOT loaded by the packaged base image. Place both in ~/btcpay-docker/plugins/.

3. Place the native library

mkdir -p ~/Workspace/native
cp libplatform_wallet_ffi.so ~/Workspace/native/   # see "Native Library Setup" above

4. docker-compose.yml

Create ~/btcpay-docker/docker-compose.yml:

version: "3.7"
services:
  btcpayserver:
    image: btcpayserver/btcpayserver:2.3.7     # check https://hub.docker.com/r/btcpayserver/btcpayserver/tags for the current release
    restart: unless-stopped
    ports:
      - "127.0.0.1:14142:23000"
    environment:
      BTCPAY_HOST: your-domain.example
      BTCPAY_CHAINS: btc                       # see note below — do NOT put dashe here
      BTCPAY_POSTGRES: Host=btcpay-pg;Port=5432;Database=btcpayserver;Username=postgres;Password=<postgres-password>
      BTCPAY_CHEATMODE: "false"
      BTCPAY_BIND: 0.0.0.0                     # required: the packaged image otherwise binds to localhost only
      DASHE_NATIVE_LIB: /native/libplatform_wallet_ffi.so
      DashEvolution__DapiAddresses: https://<mainnet-dapi-node>:443,https://<another>:443   # see note below
      DashEvolution__Mainnet: "true"
      DashEvolution__Mnemonic: <your 24-word BIP-39 mnemonic>
      DashEvolution__AccountIndex: "0"
      DashEvolution__ShieldedDbPath: /data/dash_shielded.sqlite
      DashEvolution__SyncIntervalSeconds: "15"
    volumes:
      - btcpay_data:/data
      - ./plugins:/root/.btcpayserver/Plugins
      - ~/Workspace/native:/native:ro
    networks:
      - btcpay-network

networks:
  btcpay-network:
    external: true

volumes:
  btcpay_data:

Critical gotchas (all verified on a fresh host):

  • BTCPAY_CHAINS: btc — the packaged base image does not know dashe and exits with Invalid chains "dashe" if you set it. The plugin registers DASHE at startup; the log then shows Supported chains: BTC,DASHE.
  • BTCPAY_BIND: 0.0.0.0 — without it the container only listens on localhost from its own perspective and nothing reaches port 14142 (connection refused).
  • Do NOT set DashEvolution__WalletIdHex. The wallet id is derived from the mnemonic and auto-created (Auto-created DashEvolution wallet id … in the log). Setting it manually with a mismatched value throws WalletIdHex mismatch at startup.
  • Secrets: keep the mnemonic out of git — either put this file in a private, gitignored directory, or split secrets into an un-tracked docker-compose.override.yml / .env file.
  • DapiAddresses — the IPs above are placeholders. Fetch the current enabled-node list from https://quorums.mainnet.networks.dash.org/masternodes and pick ~10-20 enabled mainnet nodes exposing their Platform DAPI on port 443, formatted as https://<ip>:443, comma-separated.
  • 8333 connection refused / NBXplorer errors in the log are harmless — the packaged image tries to SPV-sync BTC headers and there is no Bitcoin Core on this box. DASHE is unaffected.

5. First start & verification

cd ~/btcpay-docker && docker compose up -d
docker logs -f <project>-btcpayserver-1 2>&1 | grep -aE "Running plugin|Supported chains|sync started|Baseline seeded|Auto-created"

Expected lines (in order): Running plugin BTCPayServer.Plugins.DashEvolution - 1.0.0.0Supported chains: BTC,DASHEAuto-created DashEvolution wallet id …DashEvolution sync started for wallet …Baseline seeded on first sync pass: balance=… (no invoice matching on first pass). A New invoice created now survives restarts (see “Restart behavior” in Limitations).

6. Debugging first boot

If the app never listens on 14142, read the full log, not only ERR entries — a wallet-not-configured FATAL means the mnemonic env was missing/misparsed.

7. Bootstrap the first admin (API + one-time SQL fixes)

The initial admin cannot log in via Greenfield basic auth out of the box when created through the API — three documented gaps require one-time SQL fixes:

# 1) Create the user (allowed while the instance has no admin):
curl -s -X POST http://127.0.0.1:14142/api/v1/users \
  -H "Content-Type: application/json" \
  -d '{"email":"you@example.org","password":"<strong-password>"}'

# 2) Fix the three API-creation gaps (EmailConfirmed, basic-auth flag, ServerAdmin role)
#    — substitute the user id returned above:
cat > /tmp/fix_admin.sql <<EOF
UPDATE "AspNetUsers" SET "Blob2" = jsonb_set(COALESCE("Blob2",'{}'::jsonb), '{allowGreenfieldBasicAuth}', 'true'::jsonb) WHERE "Email" = 'you@example.org';
UPDATE "AspNetUsers" SET "EmailConfirmed" = true WHERE "Email" = 'you@example.org';
UPDATE "AspNetUsers" SET "Blob2" = jsonb_set("Blob2", '{showInvoiceStatusChangeWarning}', 'true'::jsonb) WHERE "Email" = 'you@example.org';
INSERT INTO "AspNetRoles" ("Id","Name","NormalizedName","ConcurrencyStamp")
  SELECT 'ServerAdmin','ServerAdmin','SERVERADMIN', gen_random_uuid()::text
  WHERE NOT EXISTS (SELECT 1 FROM "AspNetRoles" WHERE "Name"='ServerAdmin');
INSERT INTO "AspNetUserRoles" ("UserId","RoleId")
  SELECT '<user-id>', 'ServerAdmin'
  WHERE NOT EXISTS (SELECT 1 FROM "AspNetUserRoles" WHERE "UserId"='<user-id>' AND "RoleId"='ServerAdmin');
-- If login was attempted before these fixes, "FirstRun" may be stuck true:
DELETE FROM "Settings" WHERE "Id" = 'BTCPayServer.Services.PoliciesSettings' AND "Value"::jsonb ->> 'FirstRun' = 'true';
EOF
docker cp /tmp/fix_admin.sql btcpay-pg:/tmp/ && docker exec btcpay-pg psql -U postgres -d btcpayserver -f /tmp/fix_admin.sql

This is a BTCPay Server limitation, not a plugin issue: the Greenfield POST /api/v1/users endpoint creates the user with EmailConfirmed=false, allowGreenfieldBasicAuth unset, and NO roles — the first admin must be registered through the MVC UI to avoid this, or fixed with the SQL above.

8. Create the store (note the units)

SID=$(curl -s -X POST http://127.0.0.1:14142/api/v1/stores \
  -u 'you@example.org:<strong-password>' -H "Content-Type: application/json" \
  -d '{"name":"My Store","defaultCurrency":"USD","invoiceExpiration":54000,"paymentTolerance":2.0}' \
  | python3 -c "import json,sys; print(json.load(sys.stdin)['id'])")
  • invoiceExpiration is in SECONDS over the API (54000 = 15 hours). monitoringExpiration likewise.
  • paymentTolerance percent: shielded receives arrive slightly under the invoice amount (the sender-side Orchard fee varies), so use 2.0 for the demo. Must be set BEFORE the invoices you intend to settle — existing invoices keep the tolerance baked in at creation.

9. Enable the DASHE-CHAIN payment method

The Greenfield endpoint PUT /api/v1/stores/{storeId}/payment-methods/DASHE-CHAIN currently rejects new payment-method ids for a store that has never had one (verified: paymentmethod-not-found). The only working method on a fresh store is a direct DB write (no BTCPay restart needed — the next invoice picks it up):

docker exec btcpay-pg psql -U postgres -d btcpayserver -c \
"UPDATE \"Stores\" SET \"DerivationStrategies\" = '{\"DASHE-CHAIN\": {\"walletIdHex\": \"<64-hex wallet id from the startup log>\"}}' WHERE \"Id\" = '$SID';"

Confirm with a test invoice (POST /api/v1/stores/$SID/invoices {"amount":0.25,"currency":"USD"}) and read back paymentPromptsDASHE-CHAIN should appear with the shielded address and a quoted rate.

10. Point the domain

Any reverse proxy works. With Cloudflare Tunnel (dashboard-created token tunnel): cloudflared service install <token> with the public hostname routed to http://localhost:14142. Then browse https://your-domain.example/i/<invoiceId> — the checkout shows the shielded address as a QR code and the wallet identity string.


BTCPay checkout page showing the shielded DASHE address + QR code