A NixOS configuration that deploys a Bitcoin Core node with USDT tracepoint monitoring. A BCC/eBPF script hooks into bitcoind's 16 USDT tracepoints, exports metrics to Prometheus, and visualizes them on a public Grafana dashboard over HTTPS.
bitcoind (USDT tracepoints)
|
v eBPF/BCC hooks
Python tracing exporter (root, systemd) :9435
|
v Prometheus scrape
Prometheus :9090
|
v data source
Grafana :3000 (anonymous read-only)
|
v reverse proxy
Caddy :443 (Cloudflare DNS ACME)
The exporter hooks into 16 of bitcoind's 20 USDT tracepoints (excluding coin_selection:*):
- Mempool (4): added, removed, replaced, rejected
- Network (7): inbound/outbound messages, inbound/outbound/closed connections, evicted, misbehaving
- Validation (1): block_connected
- UTXO Cache (4): flush, add, spent, uncache
- A VPS running NixOS (tested on Hetzner Cloud CX22)
- sops and age installed locally
- A Cloudflare API token with DNS edit permissions for your domain
- just command runner
git clone <this-repo>
cd usdt-dashEdit the settings block in flake.nix to match your server:
hostName-- your hostnamedomain-- your domain (used for Caddy and Grafana)networkInterface-- your server's network interfaceipAddress/prefixLength-- your server's IPgateway-- your provider's gatewaysshKeys-- your SSH public key(s)
In justfile, set host to your server's IP or hostname.
The included secrets.yaml is encrypted with keys you can't decrypt. Delete it and create your own:
# Generate a local age key (skip if you already have one)
age-keygen -o ~/.config/sops/age/keys.txt
# Get your local public key
age-keygen -y ~/.config/sops/age/keys.txtEdit .sops.yaml and replace both keys:
&local-- your local age public key&server-- leave as placeholder for now (filled after first deploy)
keys:
- &local age1your-local-key-here
- &server age1your-server-key-here
creation_rules:
- path_regex: secrets\.yaml$
key_groups:
- age:
- *local
- *serverFor the initial deploy, temporarily comment out the &server key and its reference so you can encrypt with just your local key:
rm secrets.yaml
# Create plaintext, then encrypt in-place
cat > secrets.yaml <<'EOF'
caddy-cloudflare-token: your-cloudflare-api-token
grafana-secret-key: some-random-hex-string
EOF
sops --encrypt --in-place secrets.yamljust deployThis uses nixos-anywhere to wipe and install NixOS on the target.
sops-nix automatically derives an age key from the server's SSH host key for decryption -- no manual key generation needed on the server. But you need the corresponding public key in .sops.yaml so that sops can encrypt secrets the server can read.
Get the age public key from the server's SSH host key:
ssh root@<your-server> "cat /etc/ssh/ssh_host_ed25519_key.pub" | nix-shell -p ssh-to-age --run ssh-to-agePaste the output into .sops.yaml as the &server key, uncomment it, then re-encrypt:
echo y | sops updatekeys secrets.yamljust switchThis rsyncs the config to the server and runs nixos-rebuild switch remotely.
The first deploy with kernel header changes needs a reboot for BCC to find them:
ssh root@<your-server> rebootAfter reboot, verify:
# Check tracing service
ssh root@<your-server> systemctl status bitcoind-tracing
# Check metrics endpoint
ssh root@<your-server> curl -s localhost:9435/metrics | head -20| File | Purpose |
|---|---|
flake.nix |
Nix flake with nixpkgs, disko, sops-nix inputs, and deployment settings |
configuration.nix |
All NixOS services: bitcoind, tor, prometheus, grafana, caddy, sops |
disk-config.nix |
Disk partitioning layout (disko) |
hardware-configuration.nix |
Hardware-specific config (generated by nixos-anywhere) |
tracing/bitcoind_exporter.py |
BCC/eBPF script hooking USDT tracepoints, Prometheus exporter |
tracing/service.nix |
NixOS module for the tracing systemd service |
grafana/dashboard.json |
Grafana dashboard with mempool, network, block, and UTXO cache panels |
.sops.yaml |
sops encryption key configuration |
secrets.yaml |
Encrypted secrets (Cloudflare token, Grafana secret key) |
justfile |
Command runner recipes for deploy, switch, sync, etc. |