We recently helped a client add Bitcoin payments to their WooCommerce store using BTCPay Server. Rather than use a hosted solution, they wanted full control over their payment infrastructure. Here’s how we built it.
Why Self-Host?
BTCPay Server is open-source payment processing software that lets you accept Bitcoin without third-party fees or custody.
You can use hosted options like Voltage or Luna Node, but self-hosting gives you:
- No transaction fees beyond network costs
- Full custody of funds (keys never leave your server)
- Privacy for you and your customers
- Control over uptime and infrastructure
The tradeoff is complexity. You’re running a Bitcoin full node, which needs significant disk space and ongoing maintenance.
Architecture Overview
We deployed on GCP with the following stack:
┌─────────────────────────────────────────────────┐
│ GCP Load Balancer │
│ (HTTPS/SSL) │
└─────────────────────┬───────────────────────────┘
│
┌─────────────────────▼───────────────────────────┐
│ Compute Engine Instance │
│ ┌───────────────────────────────────────────┐ │
│ │ Nginx (reverse proxy) │ │
│ │ ↓ │ │
│ │ BTCPay Server (.NET) │ │
│ │ ↓ │ │
│ │ NBXplorer (blockchain indexer) │ │
│ │ ↓ │ │
│ │ Bitcoin Core (full node) │ │
│ │ ↓ │ │
│ │ PostgreSQL (2 databases) │ │
│ └───────────────────────────────────────────┘ │
│ │
│ 1TB Persistent Disk (blockchain storage) │
└─────────────────────────────────────────────────┘
The key components:
- Bitcoin Core - Full node syncing the entire blockchain (~600GB at time of writing)
- NBXplorer - Indexes the blockchain for BTCPay’s use
- BTCPay Server - The payment processor itself
- PostgreSQL - Stores NBXplorer index and BTCPay data
- Nginx - Reverse proxy handling TLS termination
Infrastructure as Code
We used three tools to make this reproducible:
Packer - Building the Machine Image
Packer creates a GCP image with all software pre-installed. This means new instances boot ready to go, just needing to sync the blockchain.
source "googlecompute" "ubuntu" {
project_id = "your-gcp-project"
source_image_family = "ubuntu-2404-lts-amd64"
zone = "us-central1-a"
machine_type = "n2-standard-2"
ssh_username = "ubuntu"
image_name = "btcpay-{{timestamp}}"
image_family = "btcpay"
}
build {
sources = ["source.googlecompute.ubuntu"]
provisioner "ansible" {
playbook_file = "../playbooks/btcpayserver.yml"
user = "ubuntu"
}
}
Ansible - Configuring the Stack
The Ansible playbook handles the entire software stack: downloading Bitcoin Core, building NBXplorer and BTCPay from source, configuring PostgreSQL, setting up systemd services, and configuring Nginx.
Key sections of the playbook:
Bitcoin Core installation:
- name: Download Bitcoin Core tarball
get_url:
url: "https://bitcoin.org/bin/bitcoin-core-28.1/bitcoin-28.1-x86_64-linux-gnu.tar.gz"
dest: "/tmp/bitcoin-28.1.tar.gz"
checksum: "sha256:07f77afd326639145b9ba9562912b2ad2ccec47b8a305bd075b4f4cb127b7ed7"
- name: Unarchive bitcoin-cli and bitcoind to /usr/local/bin
unarchive:
src: "/tmp/bitcoin-28.1.tar.gz"
dest: /usr/local/bin
remote_src: yes
extra_opts: [--strip-components=2, --wildcards, "bitcoin-*/*/bitcoind", "bitcoin-*/*/bitcoin-cli"]
Building BTCPay from source:
- name: Clone BTCPay Server
git:
repo: "https://github.com/btcpayserver/btcpayserver.git"
dest: "/opt/btcpayserver"
version: master
- name: Build BTCPay Server
shell: |
cd /opt/btcpayserver
dotnet publish --no-cache -o BTCPayServer/bin/Release/publish/ \
-c Release BTCPayServer/BTCPayServer.csproj
Systemd services ensure everything starts on boot and restarts on failure:
[Unit]
Description=Bitcoin daemon
After=network-online.target
Wants=network-online.target
[Service]
User=ubuntu
ExecStart=/usr/local/bin/bitcoind -conf=/home/ubuntu/.bitcoin/bitcoin.conf -daemon=0
Restart=always
TimeoutStopSec=60s
[Install]
WantedBy=multi-user.target
The key settings: Restart=always ensures Bitcoin Core restarts if it crashes, and After=network-online.target waits for network before starting.
BTCPay Server’s service depends on NBXplorer, which depends on Bitcoin Core:
[Unit]
Description=BTCPay Server
Requires=nbxplorer.service
After=nbxplorer.service
[Service]
WorkingDirectory=/opt/btcpayserver
ExecStart=/opt/btcpayserver/BTCPayServer/bin/Release/publish/BTCPayServer
User=ubuntu
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
This dependency chain means systemctl start btcpay automatically starts the whole stack in the right order.
Testnet/Mainnet switching:
When you’re handling real money, you don’t want your first transaction to be a customer payment. We parameterized the Bitcoin network so we can spin up a complete testnet environment first:
vars:
bitcoin_network: "mainnet" # Options: mainnet, testnet, regtest
postgres_db: "nbxplorer_{{ bitcoin_network }}"
btcpay_db: "btcpay_{{ bitcoin_network }}"
The network variable flows through to the data directories and database names:
- name: Create BTCPay data directory
file:
path: /home/ubuntu/.btcpayserver/{% if bitcoin_network == 'mainnet' %}Main{% elif bitcoin_network == 'testnet' %}TestNet{% elif bitcoin_network == 'regtest' %}RegTest{% endif %}
state: directory
- name: Create NBXplorer data directory
file:
path: /home/ubuntu/.nbxplorer/{% if bitcoin_network == 'mainnet' %}Main{% elif bitcoin_network == 'testnet' %}TestNet{% elif bitcoin_network == 'regtest' %}RegTest{% endif %}
state: directory
This lets us spin up a testnet instance to verify the full WooCommerce checkout flow—generating invoices, sending test Bitcoin, confirming payments—before going live. Once everything works on testnet, switching to mainnet is a single variable change.
Terraform - Provisioning GCP Resources
Terraform handles the GCP infrastructure: compute instances, load balancer, firewall rules, and networking.
resource "google_compute_instance_template" "btcpay_template" {
name_prefix = "btcpay-instance-template-"
machine_type = "e2-medium"
region = "us-central1"
disk {
source_image = data.google_compute_image.ubuntu_os.self_link
auto_delete = true
boot = true
disk_size_gb = 1000 # Space for blockchain
}
network_interface {
network = data.google_compute_network.default_network.self_link
# No public IP - uses Cloud NAT for outbound
}
service_account {
email = google_service_account.btcpay_sa.email
scopes = ["logging.write", "monitoring.write"]
}
tags = ["btcpay-node", "http-server"]
}
We use a managed instance group with a single instance. This gives us:
- Automatic healing if the instance fails health checks
- Easy updates by rolling out new instance templates
- Load balancer integration
WooCommerce Integration
Once BTCPay Server is running, connecting it to WooCommerce is straightforward:
- Install the BTCPay for WooCommerce plugin
- Create a store in BTCPay and generate API credentials
- Configure the plugin with your BTCPay server URL and API key
- Enable Bitcoin as a payment method
Customers see Bitcoin as a checkout option. BTCPay generates unique addresses per order and monitors the blockchain for payments. Once confirmed, it notifies WooCommerce to mark the order as paid.
Costs
Running this on GCP costs roughly:
| Resource | Monthly Cost |
|---|---|
| e2-medium instance | ~$25 |
| 1TB persistent disk | ~$40 |
| Load balancer | ~$18 |
| Egress (minimal) | ~$5 |
| Total | ~$88/month |
Compare this to hosted BTCPay solutions at $10-30/month, and it’s more expensive. But you get full control, no transaction fees, and the infrastructure knowledge to troubleshoot issues yourself.
Lessons Learned
Initial blockchain sync takes days. Bitcoin Core needs to download and verify ~600GB of blockchain data. On a standard instance, expect 3-5 days for initial sync. We baked a partially-synced image to speed up new deployments.
Disk I/O matters. The blockchain sync is I/O intensive. We started with standard persistent disk and upgraded to SSD for faster sync times.
Memory sizing is tricky. NBXplorer and BTCPay are .NET applications that can be memory-hungry. We settled on e2-medium (4GB RAM) as a minimum.
Health checks need tuning. The load balancer health checks were initially too aggressive, marking the instance unhealthy during blockchain sync. We increased the check interval and failure threshold.
Bitcoin Core checksum verification is essential. Always verify the SHA256 checksum of the Bitcoin Core download. The Ansible playbook includes this check - if someone compromises the download, you’d be running malicious software handling real money.
NBXplorer needs to fully sync before BTCPay works. After Bitcoin Core syncs, NBXplorer has to index the blockchain. This takes additional hours. Don’t panic when BTCPay shows “NBXplorer is not synced” - just wait.
PostgreSQL connection strings are picky. BTCPay and NBXplorer each need their own database. We wasted time debugging connection issues that turned out to be wrong database names in the config.
ZMQ ports matter. Bitcoin Core notifies NBXplorer of new blocks via ZeroMQ. If these ports (28332, 28333) aren’t configured identically in bitcoin.conf and NBXplorer settings, you’ll get silent failures where payments never confirm.
Nginx timeout defaults are too low. BTCPay’s API can be slow during initial setup. We bumped proxy_read_timeout to 300s to avoid 504 errors during store creation.
Let’s Encrypt rate limits will bite you. If you’re iterating on the setup, you’ll hit the 5-certificates-per-week limit fast. Use --staging flag during development, switch to production only when ready.
Security Considerations
A few things we implemented:
- No public IP on the instance - all traffic through load balancer
- Cloud NAT for outbound traffic
- Firewall rules allowing only load balancer health checks
- Service account with minimal permissions
- Let’s Encrypt for TLS via Nginx
Things we’d add for production:
- Secrets management via GCP Secret Manager
- Cloud Armor for DDoS protection
- Automated backups for PostgreSQL
- Monitoring alerts for sync status and disk space
Conclusion
Self-hosting BTCPay Server is more work than using a hosted solution, but it’s entirely doable with modern infrastructure tools. Terraform, Packer, and Ansible make the setup reproducible and maintainable.
If you’re considering Bitcoin payments for your business and want full control over your payment infrastructure, this approach gives you that without vendor lock-in.
Need Help with Crypto Payment Infrastructure?
We help businesses accept Bitcoin and other cryptocurrencies. Whether you need self-hosted BTCPay Server, Lightning Network integration, or WooCommerce crypto payments, get in touch.