Skip to content
Go back

BTCPay Server: Self-Hosted Crypto Payments

By SumGuy 12 min read
BTCPay Server: Self-Hosted Crypto Payments

So You Want to Accept Crypto Without Trusting Anyone

Here’s the problem with most “accept Bitcoin” solutions: they’re just Stripe with a Bitcoin logo slapped on. You hand your money to Bitpay, Coinbase Commerce, or Strike, and they handle custody, settlement, and KYC on your behalf. You’re not running anything. You’re not in control of anything. You’ve just replaced one intermediary with a slightly more ideologically confusing one.

BTCPay Server is the other path. Open-source, self-hosted, non-custodial Bitcoin (and Lightning) payment processing. Your server, your keys, your database, your node. Nobody in the middle taking a percentage or freezing your account because your product category triggered a compliance review.

It’s not simple to set up — I’m not going to lie to you. You’re running a full Bitcoin node, a Lightning implementation, a .NET application, and a Postgres database, ideally behind a reverse proxy. That’s a stack. But once it’s running, it’s genuinely yours.

This walkthrough covers what BTCPay actually is, what it costs you in hardware, and how to get it running without losing your mind.


What BTCPay Server Actually Is

BTCPay Server is a C# .NET application that acts as a payment coordinator between your store (website, POS, whatever) and the Bitcoin network. It talks to:

When a customer checks out, BTCPay generates a unique address (derived from your xpub — your wallet’s public key, not private key). It watches that address for payment, confirms it, and marks the invoice paid. You never expose your private keys to the server.

Supported currencies beyond BTC include Lightning Network (via LND or CLN), Liquid Network, Monero, and various ERC-20 tokens via plugins. The core install is Bitcoin + Lightning — that’s what this guide covers.


Hardware Reality Check

Before you get excited: a full Bitcoin node is not running on your Raspberry Pi 4 with a 32 GB SD card.

ComponentMinimumRecommended
Disk~750 GB (full node)1 TB+ SSD
RAM4 GB8 GB
CPUAny modern x644 cores helps
OSUbuntu 22.04/24.04Ubuntu 22.04 LTS

The 750 GB figure is the current full chain size as of mid-2026. It grows. Plan for 1 TB minimum if you want headroom.

Pruned mode drops that to around 10 GB by discarding old block data. You can still receive payments — Bitcoin Core validates new transactions without the full history. The trade-off: you can’t serve historical chain data to other nodes, and some indexing features are unavailable. For a payment processor that just needs to watch addresses and confirm transactions, pruned mode is workable.

Initial Block Download (IBD) — syncing the chain from scratch — takes several days on typical home internet. Fast NVMe + good bandwidth: maybe 24-48 hours. Spinning disk on a slow connection: a week. Plan accordingly.


Install Path 1: BTCPay Docker (The Right Way)

The official and recommended install method uses the btcpayserver-docker repository, which generates a docker-compose.yml from environment variables. It handles all the service wiring for you.

Prerequisites

Setup

Terminal window
# Clone the BTCPay docker generator
git clone https://github.com/btcpayserver/btcpayserver-docker /opt/btcpay
cd /opt/btcpay
# Set environment variables (these drive the config generator)
export BTCPAY_HOST="btcpay.yourdomain.com"
export NBITCOIN_NETWORK="mainnet"
export BTCPAYGEN_CRYPTO1="btc"
export BTCPAYGEN_LIGHTNING="lnd"
export BTCPAYGEN_REVERSEPROXY="nginx"
export BTCPAYGEN_ADDITIONAL_FRAGMENTS="opt-save-storage"
export LETSENCRYPT_EMAIL="[email protected]"
export BTCPAY_ENABLE_SSH="true"
# Run setup — this generates the compose file and starts everything
. ./btcpay-setup.sh -i

opt-save-storage enables pruned mode (keeps ~10 GB instead of 750+). Remove it if you want the full node.

BTCPAYGEN_LIGHTNING="lnd" deploys LND. Use clightning if you prefer Core Lightning.

The setup script writes a generated docker-compose.yml to /opt/btcpay/Generated/, starts the containers, and registers a systemd service (btcpayserver) that survives reboots.

Watching the sync

Terminal window
# Check container status
docker ps
# Tail Bitcoin Core sync progress
docker logs -f generated_bitcoind_1 2>&1 | grep -E "progress|height"
# LND logs
docker logs -f generated_lnd_1

Bitcoin Core logs UpdateTip: new best with a progress percentage. When it hits 1.000000, you’re synced.

Updates

Terminal window
cd /opt/btcpay
. ./btcpay-update.sh

That’s it. The update script pulls new images, regenerates the compose file if needed, and restarts cleanly.


Install Path 2: Manual (BYO Node)

If you’re already running Bitcoin Core and don’t want another instance eating your disk, you can connect BTCPay to your existing node. This is more work and easier to misconfigure, but legitimate if you know what you’re doing.

You’ll need to configure bitcoin.conf with RPC credentials and ensure txindex=1 is set (required for BTCPay’s address tracking). Then run BTCPay pointing at your node’s RPC endpoint instead of the bundled one.

Honestly, unless you have a specific reason, use the Docker path. The compose generator handles service discovery, TLS, and restart policies. DIY-ing all that takes an afternoon and introduces more failure surfaces.


Lightning Network Setup

Once the chain is synced, the LND container is running but has no channels. You need:

  1. A funded on-chain wallet (seed the LND wallet with sats)
  2. At least one channel opened to a well-connected peer
  3. Inbound liquidity if you want to receive payments (counterintuitive, but you need the other side to have balance)

Get your LND node info

Terminal window
# Execute inside the LND container
docker exec -it generated_lnd_1 lncli getinfo

Note your identity_pubkey and uris (your Lightning address). The URI includes your Tor address if Tor is configured — useful for inbound connectivity without exposing your home IP.

Autopilot: leave it off

LND’s autopilot automatically opens channels. Don’t enable it. Autopilot opens channels to random peers based on graph heuristics. You want to open channels intentionally — to exchanges, routing nodes, or services you actually use. Poorly chosen channels drain your on-chain fees and provide terrible routing.

Channel backups

LND has a channel.backup file that’s critical for recovery. Losing this means losing funds in open channels. Back it up somewhere not on the same machine:

Terminal window
# Copy from container to host
docker cp generated_lnd_1:/root/.lnd/data/chain/bitcoin/mainnet/channel.backup \
/root/lnd_channel_backup_$(date +%Y%m%d).backup
# Then rsync/S3/wherever offsite

BTCPay also has built-in SCB (Static Channel Backup) management under Server Settings > Services > LND Seed Backup.


Setting Up Your First Store

Once BTCPay is running and synced:

  1. Navigate to https://btcpay.yourdomain.com
  2. Create an account (first account gets admin)
  3. Create a Store — give it a name, set your currency (for display/accounting, not settlement)

BTCPay uses your xpub (extended public key) to derive payment addresses. This means the server can generate fresh addresses for every invoice without ever knowing your private key. Your hot wallet stays on your hardware wallet, phone, or wherever.

In your wallet software (Electrum, Sparrow, BlueWallet), find your xpub or zpub (for native SegWit) and paste it into BTCPay under Store > Wallet > Bitcoin.

BTCPay will generate addresses from that key. It never has signing capability — it can only watch.

Generate a test invoice

In the BTCPay dashboard, go to your store and create an invoice manually:

You get a QR code and address. Send a small amount to verify the flow end to end before you go live.

Enable Lightning

In Store Settings > Lightning, connect to your LND node. If you’re on the Docker setup, BTCPay already knows the LND endpoint internally — just hit “Use internal node.”


Integrations and Checkout

Embed a checkout button

BTCPay has a JavaScript widget for payment buttons:

<script src="https://btcpay.yourdomain.com/modal/btcpay.js"></script>
<button
data-url="https://btcpay.yourdomain.com"
data-store-id="YOUR_STORE_ID"
data-currency="USD"
data-amount="25.00"
data-description="Sticker Pack"
onclick="window.btcpay.showInvoice(this)">
Pay with Bitcoin
</button>

For WooCommerce, Shopify, or other platforms there are plugins that handle the BTCPay integration. WooCommerce has a first-party BTCPay plugin. The Shopify integration works via the BTCPay Shopify app (connects to your self-hosted instance).

For custom integrations, use the Greenfield API (more on that below).

Point of Sale (POS) app

BTCPay includes a built-in POS interface at https://btcpay.yourdomain.com/your-store/pos. It shows your product catalog, accepts payment, and handles Lightning invoices. You can run it on a tablet at a market stall or pop-up. No third-party POS software needed.


Greenfield API Mode

BTCPay has two operational modes:

Classic mode — BTCPay processes the payment, customer sees the BTCPay checkout page. Your server handles everything end-to-end.

Greenfield API mode — you use the REST API to create invoices and handle webhooks programmatically, embedding BTCPay’s logic into your own UI.

The Greenfield API is documented at https://btcpay.yourdomain.com/docs:

Terminal window
# Create an invoice via API
curl -X POST https://btcpay.yourdomain.com/api/v1/stores/STORE_ID/invoices \
-H "Authorization: token YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": "25.00",
"currency": "USD",
"metadata": {"orderId": "12345"},
"checkout": {"speedPolicy": "LowSpeed"}
}'

The response includes a checkoutLink (send the customer there) and an id for webhook matching. Set up a webhook in Store Settings > Webhooks pointing to your order fulfillment endpoint — BTCPay will POST invoice status updates as payments come in.

speedPolicy controls confirmation requirements:


Privacy Trade-Offs (Be Honest With Yourself)

Here’s the part people skip over: Bitcoin is not private by default.

Every on-chain transaction is publicly visible. If you receive a payment to an address linked to your identity (your website, your store, your name), that transaction is on the blockchain forever. Chain analysis firms and governments can trace funds. On-chain Bitcoin payments to a public business are, practically speaking, public records.

Lightning Network is substantially better. Payments are routed through encrypted channels, balances aren’t public, and individual transactions aren’t recorded on-chain. For a self-hosted payment setup where privacy matters, pushing customers toward Lightning is the right call.

Custodial Lightning defeats the purpose. If you accept Lightning via Strike, Wallet of Satoshi, or another custodial service, you’ve re-introduced the intermediary. They know your payment volume, they can freeze funds, and in many jurisdictions they’re running KYC. Self-hosting BTCPay with your own LND node keeps the whole stack under your control.


The Tax Reality

BTCPay doesn’t file taxes. It doesn’t report payments to anyone. That’s a feature, not a bug — and also your problem, not BTCPay’s.

In the US, the IRS treats crypto received as payment as ordinary income at fair market value on the date of receipt. When you later spend or sell that crypto, you may also owe capital gains tax on any appreciation. Most other jurisdictions have similar frameworks.

BTCPay’s reporting tools (under Store > Reports) give you invoice history with amounts and timestamps. Export that to your accountant. If you’re running a real business through this, get a real accountant who understands crypto. The “it’s decentralized so nobody knows” approach has gone badly for a lot of people.


Should You Bother?

Honestly? It depends on what you’re trying to solve.

Good fit:

Not a great fit:

If you’ve already got a homelab machine with a 1 TB SSD sitting around, BTCPay is one of the more interesting things you can put on it. The stack is mature, the Docker setup is well-maintained, and running your own payment infrastructure is genuinely useful to understand even if you never charge a real customer through it.

The 2 AM version of yourself who gets hit with “payment processor account suspended pending review” will appreciate having thought this through beforehand.


Quick Reference

Terminal window
# Start/stop BTCPay
systemctl start btcpayserver
systemctl stop btcpayserver
# Check all container status
docker ps --filter "name=generated"
# Bitcoin Core sync status
docker exec generated_bitcoind_1 bitcoin-cli getblockchaininfo | grep -E "blocks|headers|verificationprogress"
# LND wallet balance
docker exec generated_lnd_1 lncli walletbalance
# LND channel balance
docker exec generated_lnd_1 lncli channelbalance
# BTCPay logs
journalctl -u btcpayserver -f
# Rebuild/update
cd /opt/btcpay && . ./btcpay-update.sh

The code is at github.com/btcpayserver/btcpayserver and the community on their Mattermost is genuinely helpful if you get stuck mid-IBD wondering why your Lightning node won’t sync. Fair warning: the rabbit hole is deep. Once you’re running your own payment node, the next logical step is running your own block explorer, then your own email server, and suddenly it’s 3 AM and you’re writing Ansible playbooks to back up your channel state to three different geographic locations. You’ve been warned.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Next Post
iperf3 + nload: Network Diagnosis

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts