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.
| Component | Disk | Notes |
|---|---|---|
| Bitcoin Core block files | about 800 GB | blk*.dat and rev*.dat; grows 60 to 80 GB a year. |
| Chainstate | 15 GB | UTXO set. |
| txindex | about 65 GB | Required by mempool for transaction lookups. |
| electrs 0.12 index | 56 GB, 110 GB peak | For address pages. Fulcrum needs about 130 GB instead. |
| mempool backend, MariaDB, cache | 5 to 15 GB | Derived 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 enableWireGuard 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.1Core 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 50001Docker 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/mysqldocker 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/heightThe 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/hostnameOpen 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 onTo 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.