Host a Nostr relay with strfry on a VPS and pay in Bitcoin or Monero

· 8 min read

Run a strfry Nostr relay on Ubuntu 24.04: build from source, systemd, nginx with Let’s Encrypt websockets, allowlist write policy, Tor onion, moderation.

Who should run a relay, and why on a VPS

Nostr clients publish signed events to relays and read them back from relays; there is no server in the middle that owns your identity, but there is nothing keeping your notes online except the relays that hold them. A personal relay fixes that: your client writes to it first, it keeps a complete copy of everything you have ever posted, and under the outbox model other people’s clients fetch your notes from it directly. A small community relay does the same for a group, and a strict write policy keeps the spam out.

Relays need a public address with a TLS certificate, open inbound ports and uptime, which is exactly what a home connection lacks and a VPS has. strfry is the usual choice: a C++ relay backed by LMDB that runs comfortably on the smallest plans and implements negentropy sync, so you can mirror your events between relays efficiently. Paying for the server in Bitcoin or Monero without creating an account fits the protocol’s point: no one issues you a relay, you just run one.

Sizing: small plans are enough

strfry stores events in a single LMDB file and leans on the kernel page cache rather than its own. Signature verification is the only CPU work worth mentioning. What varies is how much you accept and from whom.

Relay typeDisk after a yearRAMPlan
Personal: your pubkey plus replies to youunder 2 GB2 GBBVX11 (2 vCPU, 2 GB, 40 GB, from $10/mo) or BVCX23 (2 vCPU, 4 GB, 40 GB, $11/mo)
Community: allowlist of tens of users5 to 20 GB4 GBBVCX23 or BVA11 (Arm, 2 vCPU, 4 GB, 40 GB, $12/mo)
Open or paid public relay50 GB and up, grows fast8 GBBVCX33 (4 vCPU, 8 GB, 80 GB, $16/mo) or BVX31 (4 vCPU, 8 GB, 160 GB, from $32/mo)

Bandwidth is generous for this use: a personal relay moves a few GB a month, and even a popular public relay sits in the low terabytes, inside the 20 TB included on every plan. strfry builds and runs on aarch64, so the Arm plans are fine. Pick the location where most of your users are; websocket latency is noticeable in a chat-like client.

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

Build strfry from source

strfry is not packaged for Ubuntu, and the project’s own instructions are a short build from source. Ubuntu 24.04 ships a C++20-capable g++ and every dependency as a package. Check out the latest release tag from the strfry GitHub tags page (1.1.3 at the time of writing) instead of building master.

apt install -y git g++ make libssl-dev zlib1g-dev liblmdb-dev libflatbuffers-dev libsecp256k1-dev libzstd-dev
cd /usr/local/src
git clone https://github.com/hoytech/strfry && cd strfry
git checkout 1.1.3      # see https://github.com/hoytech/strfry/tags
git submodule update --init
make setup-golpe
make -j"$(nproc)"
install -m 0755 -o root -g root strfry /usr/local/bin/strfry
strfry --help | head -3

The build takes a few minutes on two vCPUs. If you prefer containers, the repository includes a Dockerfile: docker build -t strfry . in the checkout, then run it with /etc/strfry.conf and the database directory mounted as volumes and port 7777 published on 127.0.0.1 only. The rest of this guide assumes the native binary.

Configure strfry and run it under systemd

The repository’s strfry.conf documents every option with its default; only db is mandatory. The settings below keep the listener on localhost for nginx to front, tell strfry which header carries the real client IP, and fill in the NIP-11 information clients show when they inspect your relay. The writePolicy line points at the plugin written two sections down; leave it empty until then.

useradd --system --home-dir /var/lib/strfry --shell /usr/sbin/nologin strfry
install -d -m 0750 -o strfry -g strfry /var/lib/strfry /var/lib/strfry/db /etc/strfry

cat > /etc/strfry.conf <<'EOF'
db = "/var/lib/strfry/db/"

relay {
    bind = "127.0.0.1"
    port = 7777
    nofiles = 524288
    realIpHeader = "x-real-ip"

    info {
        name = "my relay"
        description = "Personal relay. Writes restricted; reads open."
        pubkey = "YOUR_HEX_OR_NPUB_PUBKEY"
        contact = "[email protected]"
    }

    writePolicy {
        plugin = ""
    }
}
EOF
chown root:strfry /etc/strfry.conf && chmod 0640 /etc/strfry.conf

cat > /etc/systemd/system/strfry.service <<'EOF'
[Unit]
Description=strfry Nostr relay
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=strfry
Group=strfry
ExecStart=/usr/local/bin/strfry --config /etc/strfry.conf relay
Restart=on-failure
RestartSec=5
LimitNOFILE=1000000
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
NoNewPrivileges=true

[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now strfry

# NIP-11 document, served on the same port when asked for application/nostr+json:
curl -s -H 'Accept: application/nostr+json' http://127.0.0.1:7777/

nginx with Let’s Encrypt for wss://, and a Tor onion

Clients connect with wss://, so you need a hostname and a certificate. Point an A (and AAAA) record at the server, open 80 and 443, and let certbot configure the TLS part. The server block below handles the websocket upgrade and passes the client address through in X-Real-IP, which matches the realIpHeader setting above. The long proxy_read_timeout matters: nostr subscriptions are idle most of the time and strfry pings every 55 seconds by default.

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

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

    location / {
        proxy_pass http://127.0.0.1:7777;
        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-Real-IP $remote_addr;
        proxy_read_timeout 300s;
        proxy_send_timeout 300s;
    }
}
EOF
ln -s /etc/nginx/sites-available/relay /etc/nginx/sites-enabled/relay
rm -f /etc/nginx/sites-enabled/default
nginx -t && systemctl reload nginx

certbot --nginx -d relay.example.com --agree-tos -m [email protected] --redirect
curl -s -H 'Accept: application/nostr+json' https://relay.example.com/

certbot rewrites the server block for 443, adds the redirect, and installs a timer that renews the certificate. For Tor users, add an onion service that points straight at strfry; onion connections are already encrypted, so clients use ws:// with the onion hostname and no certificate is involved.

apt install -y tor
cat >> /etc/tor/torrc <<'EOF'
HiddenServiceDir /var/lib/tor/nostr/
HiddenServiceVersion 3
HiddenServicePort 80 127.0.0.1:7777
EOF
systemctl restart tor
cat /var/lib/tor/nostr/hostname     # advertise as ws://THIS.onion

Write policy: an allowlist plugin

strfry keeps policy out of the core. Instead, a plugin reads one JSON line per incoming event on stdin and answers with accept, reject or shadowReject. The script below implements the usual personal-relay rule: accept events signed by allowlisted pubkeys, and accept events from anyone that tag an allowlisted pubkey so replies, mentions and DMs addressed to you are stored. Everything else is rejected with a message the client can show. Pubkeys are 64-character hex, not npub.

cat > /etc/strfry/policy.py <<'EOF'
#!/usr/bin/env python3
import json, sys

ALLOWED = {
    "YOUR_HEX_PUBKEY",
    "A_FRIENDS_HEX_PUBKEY",
}

for line in sys.stdin:
    req = json.loads(line)
    if req.get("type") != "new":
        continue
    ev = req["event"]
    res = {"id": ev["id"]}
    tagged = {t[1] for t in ev.get("tags", []) if len(t) > 1 and t[0] == "p"}
    if ev["pubkey"] in ALLOWED or (tagged & ALLOWED):
        res["action"] = "accept"
    else:
        res["action"] = "reject"
        res["msg"] = "blocked: this relay only accepts events from or to its members"
    print(json.dumps(res), flush=True)
EOF
chmod 0755 /etc/strfry/policy.py
chown root:strfry /etc/strfry/policy.py

sed -i 's|plugin = ""|plugin = "/etc/strfry/policy.py"|' /etc/strfry.conf
journalctl -u strfry -n 5     # strfry notices the config change and loads the plugin

Moderation, the acceptable use policy, and day-to-day operation

An open relay is a public write endpoint on the internet and will receive spam, scams and worse within hours. You, not the client developers, are responsible for what your server stores and serves. BitVPS’s acceptable use policy prohibits CSAM, spam and phishing, and that applies to content hosted on a relay just as it does to a website. An allowlist or a paid-access plugin is the simplest way to stay well clear of that line; if you do run an open relay, put a working contact in the NIP-11 information and act on reports.

# Remove everything by a pubkey (dry run first; drop --dry-run to apply):
sudo -u strfry strfry --config /etc/strfry.conf delete --filter '{"authors":["BAD_HEX_PUBKEY"]}' --dry-run

# Nightly export is the backup; LMDB files should not be copied while open.
sudo -u strfry strfry --config /etc/strfry.conf export > /var/lib/strfry/export-$(date +%F).jsonl

# Health
du -sh /var/lib/strfry/db
ss -tn state established '( sport = :7777 )' | wc -l       # live websocket connections
curl -s http://127.0.0.1:7777/metrics | head                # Prometheus metrics

Upgrades are git pull, make update-submodules, make, then copy the binary and restart. When a release changes the database format it refuses to open the old one; export with --fried on the old binary and import with --fried on the new one, as the README describes. Copy the nightly export off the server with scp or rsync; a relay database is cheap to rebuild from it. Ubuntu’s unattended-upgrades covers the OS; nginx and certbot renew without help. Nothing but 22, 80 and 443 should be open in ufw.

Which plan to pick

A personal or small-group relay is happy on BVX11 (2 vCPU, 2 GB RAM, 40 GB NVMe, from $10/mo) or BVCX23 (2 vCPU, 4 GB, 40 GB, $11/mo); the extra RAM in BVCX23 is worth the dollar for LMDB’s page cache. For an open community relay start at BVCX33 (4 vCPU, 8 GB, 80 GB, $16/mo) and watch the database size. Configure a server, pay in Bitcoin or Monero, and point your DNS at the IP in the magic link email.

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