Self-host a mempool.space explorer on a VPS with your own Bitcoin Core and electrs

· 8 min read

Self-host mempool with Docker Compose on Ubuntu 24.04: real sizing for an archival node with txindex, WireGuard to your Core and electrs, TLS, Tor onion.

Why self-host a mempool explorer

Every time you paste a transaction ID or an address into a public block explorer you tell its operator which coins you care about. The Mempool Open Source Project is the software behind mempool.space, and it runs just as well against your own node: the same fee estimates, block visualisations, RBF tracking and REST and websocket API, with nothing leaving your infrastructure. Applications that need fee rates or confirmation status can use your instance’s API instead of a third party.

A VPS is the natural public face for it: a hostname with a certificate, an onion address, and uptime that a home server behind NAT does not offer. The data behind it, however, is a different story.

Where the chain lives: sizing honestly

mempool requires Bitcoin Core with txindex=1, and Bitcoin Core refuses to combine txindex with pruning, so the node must be archival. Address lookups additionally need an Electrum server (electrs or Fulcrum), which also needs the whole chain. Here is the disk bill in October 2026.

ComponentDiskNotes
Bitcoin Core block filesabout 800 GBblk*.dat and rev*.dat; grows 60 to 80 GB a year.
Chainstate15 GBUTXO set.
txindexabout 65 GBRequired by mempool for transaction lookups.
electrs 0.12 index56 GB, 110 GB peakFor address pages. Fulcrum needs about 130 GB instead.
mempool backend, MariaDB, cache5 to 15 GBDerived data: block summaries, pool attribution, statistics. Rebuildable.

That is close to a terabyte, and the largest BitVPS plan has 960 GB of NVMe. The 160, 240 and 360 GB plans are out of the question for mainnet, and even the 960 GB BVG63 (from $1,550/mo) has no headroom once txindex and the electrs compaction peak are counted. So this guide does not put the chain on the VPS. The VPS runs the mempool frontend, backend, MariaDB, nginx and Tor; Bitcoin Core and electrs stay on a machine you already have with a 2 TB disk, whether that is a home node or the Electrum server from the companion guide, and a WireGuard tunnel joins the two so RPC never crosses the open internet.

For the VPS half, BVX21 (3 vCPU, 4 GB RAM, 80 GB NVMe, from $18/mo in Europe) or the Arm BVA21 (4 vCPU, 8 GB, 80 GB, $20/mo, EU only) is enough; the backend holds the live mempool in RAM and sits around 1 to 2 GB, MariaDB under 1 GB. The official images are published for both amd64 and arm64. Choose the location closest to your node to keep RPC latency low. For development against testnet4 or signet the whole stack, chain included, fits on one 80 GB server.

Prepare the server

Everything below assumes a fresh Ubuntu 24.04 image and a root shell (prefix sudo if you created a user). Patch first, then lock down SSH and the firewall before anything else is listening.

apt update && apt full-upgrade -y
apt install -y ufw unattended-upgrades curl gnupg

# SSH: keys only. Put your public key in /root/.ssh/authorized_keys first and
# confirm you can log in with it before disabling passwords.
cat > /etc/ssh/sshd_config.d/00-keys-only.conf <<'EOF'
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin prohibit-password
EOF
sshd -t && systemctl restart ssh

# Firewall: deny everything inbound except SSH for now.
ufw default deny incoming
ufw default allow outgoing
ufw allow OpenSSH
ufw --force enable

WireGuard between the VPS and your node

The VPS is the WireGuard server with a public endpoint; the node at home dials out to it and keeps the tunnel alive through NAT. The WireGuard guide on this site covers the protocol in more depth; this is the minimum. Generate a key pair on each side and exchange the public keys.

# On both machines:
apt install -y wireguard
umask 077 && wg genkey | tee /etc/wireguard/private.key | wg pubkey > /etc/wireguard/public.key

# On the VPS (10.66.0.1):
cat > /etc/wireguard/wg0.conf <<EOF
[Interface]
Address = 10.66.0.1/24
ListenPort = 51820
PrivateKey = $(cat /etc/wireguard/private.key)

[Peer]
# your node
PublicKey = NODE_PUBLIC_KEY
AllowedIPs = 10.66.0.2/32
EOF
ufw allow 51820/udp
systemctl enable --now wg-quick@wg0

# On the node (10.66.0.2):
cat > /etc/wireguard/wg0.conf <<EOF
[Interface]
Address = 10.66.0.2/24
PrivateKey = $(cat /etc/wireguard/private.key)

[Peer]
# the VPS
PublicKey = VPS_PUBLIC_KEY
Endpoint = YOUR.VPS.IP:51820
AllowedIPs = 10.66.0.1/32
PersistentKeepalive = 25
EOF
systemctl enable --now wg-quick@wg0
ping -c 3 10.66.0.1

Core and electrs on the node side

Bitcoin Core must listen on the tunnel address as well as localhost, allow the VPS, and use a static rpcauth credential because the cookie file is not reachable across machines. Generate the credential with the rpcauth.py script from the Bitcoin Core source tree; it prints the rpcauth line for bitcoin.conf and the plaintext password for the client. Raise the RPC work queue because the mempool backend is a chatty client. electrs only needs to bind its Electrum port on the tunnel address.

# On the node:
curl -sSL https://raw.githubusercontent.com/bitcoin/bitcoin/master/share/rpcauth/rpcauth.py | python3 - mempool
# Paste the printed rpcauth=mempool:... line into bitcoin.conf and keep the password for the VPS.

cat >> /etc/bitcoin/bitcoin.conf <<'EOF'
txindex=1
prune=0
server=1
rpcbind=127.0.0.1
rpcbind=10.66.0.2
rpcallowip=127.0.0.1
rpcallowip=10.66.0.1/32
rpcworkqueue=128
rpcthreads=8
EOF
systemctl restart bitcoind
bitcoin-cli -rpcwait getindexinfo        # txindex builds in the background; wait for synced: true

# electrs: serve the Electrum protocol on the tunnel address.
sed -i 's|^electrum_rpc_addr.*|electrum_rpc_addr = "10.66.0.2:50001"|' /etc/electrs.toml
systemctl restart electrs

ufw allow from 10.66.0.1 to any port 8332 proto tcp
ufw allow from 10.66.0.1 to any port 50001 proto tcp

# From the VPS, confirm both are reachable:
curl -s --user mempool:THE_PASSWORD --data-binary '{"jsonrpc":"1.0","id":"t","method":"getblockcount","params":[]}' -H 'content-type: text/plain;' http://10.66.0.2:8332/
nc -zv 10.66.0.2 50001

Docker Compose for mempool

The project’s docker directory ships a docker-compose.yml with three services: web (the Angular frontend behind its own nginx), api (the Node.js backend) and db (MariaDB). The version below is that file with the backend pointed at your node over the tunnel, the Electrum backend enabled, the frontend bound to localhost for our nginx to front, and image tags pinned to the current release (v3.3.1 on the mempool releases page at the time of writing). Ubuntu 24.04 packages Docker and the compose plugin.

apt install -y docker.io docker-compose-v2
mkdir -p /opt/mempool/data /opt/mempool/mysql/data
chown -R 1000:1000 /opt/mempool/data /opt/mempool/mysql
cd /opt/mempool
# /opt/mempool/docker-compose.yml
services:
  web:
    image: mempool/frontend:v3.3.1
    user: "1000:1000"
    restart: on-failure
    stop_grace_period: 1m
    command: "./wait-for db:3306 --timeout=720 -- nginx -g 'daemon off;'"
    environment:
      FRONTEND_HTTP_PORT: "8080"
      BACKEND_MAINNET_HTTP_HOST: "api"
    ports:
      - "127.0.0.1:8080:8080"

  api:
    image: mempool/backend:v3.3.1
    user: "1000:1000"
    restart: on-failure
    stop_grace_period: 1m
    command: "./wait-for-it.sh db:3306 --timeout=720 --strict -- ./start.sh"
    environment:
      MEMPOOL_BACKEND: "electrum"
      ELECTRUM_HOST: "10.66.0.2"
      ELECTRUM_PORT: "50001"
      ELECTRUM_TLS_ENABLED: "false"
      CORE_RPC_HOST: "10.66.0.2"
      CORE_RPC_PORT: "8332"
      CORE_RPC_USERNAME: "mempool"
      CORE_RPC_PASSWORD: "THE_PASSWORD_FROM_RPCAUTH"
      CORE_RPC_TIMEOUT: "60000"
      DATABASE_ENABLED: "true"
      DATABASE_HOST: "db"
      DATABASE_DATABASE: "mempool"
      DATABASE_USERNAME: "mempool"
      DATABASE_PASSWORD: "CHANGE_ME_DB_PASSWORD"
      STATISTICS_ENABLED: "true"
    volumes:
      - ./data:/backend/cache

  db:
    image: mariadb:10.5.21
    user: "1000:1000"
    restart: on-failure
    stop_grace_period: 1m
    environment:
      MYSQL_DATABASE: "mempool"
      MYSQL_USER: "mempool"
      MYSQL_PASSWORD: "CHANGE_ME_DB_PASSWORD"
      MYSQL_ROOT_PASSWORD: "CHANGE_ME_ROOT_PASSWORD"
      MARIADB_AUTO_UPGRADE: "1"
    volumes:
      - ./mysql/data:/var/lib/mysql
docker compose up -d
docker compose logs -f api      # watch it connect to Core and electrs and load the first blocks
curl -s http://127.0.0.1:8080/api/v1/backend-info
curl -s http://127.0.0.1:8080/api/blocks/tip/height

The backend needs eight blocks of history before the front page looks right, so give it an hour or so. It also fetches two things from outside by default: exchange prices from mempool.space and the mining pool list from the project’s GitHub repository. The backend configuration reference documents how to disable or redirect those if you want zero third-party traffic.

nginx with TLS and a Tor onion

Point a DNS record at the VPS and let nginx terminate TLS in front of the frontend container, which already proxies the API and websocket paths to the backend. The websocket upgrade headers are required for the live front page. The onion service points at the same localhost port.

apt install -y nginx certbot python3-certbot-nginx tor
ufw allow 80/tcp && ufw allow 443/tcp

cat > /etc/nginx/sites-available/mempool <<'EOF'
server {
    listen 80;
    listen [::]:80;
    server_name mempool.example.com;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_read_timeout 600s;
    }
}
EOF
ln -s /etc/nginx/sites-available/mempool /etc/nginx/sites-enabled/mempool
rm -f /etc/nginx/sites-enabled/default
nginx -t && systemctl reload nginx
certbot --nginx -d mempool.example.com --agree-tos -m [email protected] --redirect

cat >> /etc/tor/torrc <<'EOF'
HiddenServiceDir /var/lib/tor/mempool/
HiddenServiceVersion 3
HiddenServicePort 80 127.0.0.1:8080
EOF
systemctl restart tor
cat /var/lib/tor/mempool/hostname

Open https://mempool.example.com in a browser: the front page should show the live mempool blocks filling in and the websocket indicator connected, and /api/v1/fees/recommended should return JSON. Over Tor Browser the onion hostname serves the same site. The frontend container’s nginx sets long-lived caching headers for its static assets, so a hard refresh after an upgrade is sometimes needed. If you only want the explorer reachable over Tor, skip the DNS and certbot steps, leave 80 and 443 closed in ufw, and keep the onion service alone.

Operate: upgrades, disk, backups, firewall

cd /opt/mempool
docker compose ps
docker compose logs --since 1h api | grep -iE 'error|warn'
df -h / && du -sh /opt/mempool/mysql
wg show        # tunnel handshake age; should be under two minutes with keepalive on

To upgrade, change the two image tags to the new release, then docker compose pull and docker compose up -d; read the release notes first, since major versions have changed database schemas. Everything in /opt/mempool is derived from the chain and can be rebuilt, so the automated backups add-on is optional here; back up the compose file and your nginx config. On the node side, watch the archival disk: the chain grows whether or not you look. The VPS firewall should show only SSH, 80, 443 and 51820/udp. Extend the prepaid term from the magic link page before the reminder email becomes a deletion.

Which plan to pick

For the explorer itself, BVX21 (3 vCPU, 4 GB RAM, 80 GB NVMe, from $18/mo) or BVA21 (4 vCPU, 8 GB, 80 GB, $20/mo, EU only) in the location nearest your node. If you insist on hosting the chain too, the only candidate is BVG63 with 960 GB (from $1,550/mo) and you should expect to run out of disk within the year. Configure a server, pay in Bitcoin, and the root credentials arrive by magic link once the payment confirms.

Ready to run this yourself?

Linux KVM servers with NVMe storage in the EU, US and Singapore. Root access arrives by magic link; no account, no KYC.

Configure a server — pay with Bitcoin, no account needed