Skip to main content

Public RPC Node

The internet-facing role: serves wallets, dApps and explorers over HTTPS/WSS, and acts as a bootnode. It runs no consensus and holds no keys.

This is the one role that needs a domain and a Cloudflare account — it is the only node meant to be reachable from a browser.

Before you start

Docker and the node-deploy checkout — see Installation. Sizing is in Running a Node. The commands below assume the repo is cloned at /opt/node-deploy.


How the protection layers stack​

A public RPC accepts requests from anyone by design, so the defence is layered:

LayerWhat it does
Cloudflare proxyL3/L4 and L7 DDoS protection in front of the origin
Cloudflare WAF skip ruleStops managed bot challenges from breaking legitimate RPC clients
Caddy rate limit500 requests / 10s per IP for RPC, 30 / 60s for WebSocket upgrades, keyed on CF-Connecting-IP
Caddy connection caps400 HTTP + 1500 WS concurrent, summing below the node's 5000 so saturation is impossible
Origin firewall80/443 from Cloudflare only; 9944 and 9615 denied
Container limitsRPC_MEM_LIMIT=6g, RPC_CPUS=1.5 so a flood cannot starve Caddy and the OS

Caddy also replaces the node's CORS headers with its own, so browsers never see a duplicated *, *. --rpc-methods plays no part: the binary forces Unsafe regardless — see Node Flags.


1. Open the firewall​

P2P is public because this node is a bootnode. HTTP and HTTPS are open only to Cloudflare. The stack runs network_mode: host, so the node listens on every interface; the deny rules on its own ports are what contain it. Caddy reaches it over loopback.

sudo ufw allow 22/tcp        # SSH — restrict to your own IP if you can
sudo ufw allow 30333/tcp # P2P — public

# 80/443 only from Cloudflare's edge ranges
for ip in $(curl -s https://www.cloudflare.com/ips-v4); do
sudo ufw allow from "$ip" to any port 443 proto tcp
sudo ufw allow from "$ip" to any port 80 proto tcp
done

sudo ufw deny 9944 # RPC — Caddy uses loopback
sudo ufw deny 9615 # Prometheus — not public
sudo ufw enable

Cloudflare's ranges change. node-deploy ships a script that refreshes them (see Ports & Endpoints):

sudo /opt/node-deploy/scripts/sync-cloudflare-ufw.sh

# keep it current
echo '0 4 1 * * root /opt/node-deploy/scripts/sync-cloudflare-ufw.sh 2>&1 | logger -t cf-ufw' \
| sudo tee /etc/cron.d/cf-ufw-sync

If your host has IPv6, add the ips-v6 ranges too.


2. Point DNS at it​

One proxied A record:

rpc-1.testnet.orbinum.io   A   <your-public-ip>   Proxied

Only HTTPS goes through Cloudflare. P2P on 30333 uses the real origin IP, which is why that port is open to the internet directly.


3. Configure​

cd /opt/node-deploy/testnet/rpc
cp .env.example .env

The four values that matter:

VariableNotes
RPC_NAMEShown in telemetry. Use rpc-1, rpc-2, … per node
RPC_NODE_KEYopenssl rand -hex 32 — unique per node
RPC_DOMAINThe domain from step 2; Caddy serves it
TELEMETRY_URLAlready set correctly — see Telemetry
Keep RPC_NODE_KEY stable

It derives the PeerId, and this node's PeerId is published in the chain spec's bootnode list. Changing it makes the node appear as a different peer.


4. TLS: a Cloudflare Origin Certificate​

Caddy would normally get a certificate from Let's Encrypt automatically. Behind Cloudflare it cannot: the ACME challenge lands on Cloudflare's edge, not on Caddy, so the certificate can never be issued or renewed.

Use a Cloudflare Origin Certificate instead — issued by Cloudflare for the origin leg only, valid 15 years, no challenge involved.

In the Cloudflare dashboard, create one for your hostname, then:

cd /opt/node-deploy/testnet/rpc
nano origin.pem # paste the "Origin Certificate" block
nano origin.key # paste the "Private Key" block
chmod 600 origin.key

Three Cloudflare settings have to match:

  • SSL/TLS mode: Full (Strict) — so Cloudflare validates the origin cert.
  • Universal SSL does not cover a third-level host like rpc-1.testnet.orbinum.io. Order an Advanced Certificate for *.testnet.orbinum.io.
  • A WAF Skip rule for the RPC hostname, skipping managed rules and Super Bot Fight Mode. Otherwise Cloudflare challenges every non-browser client — which is every RPC consumer — with an HTML page instead of JSON.

5. Start​

Caddy is a custom build — the base image has no rate-limit plugin — so it is built locally the first time:

docker compose build caddy    # compiles the caddy-ratelimit plugin
docker compose pull # node image
docker compose up -d

Watch it come up:

docker compose logs -f orbinum-rpc-node
docker logs orbinum-caddy --tail 20

6. Verify​

From anywhere:

curl -s -H "Content-Type: application/json" \
-d '{"id":1,"jsonrpc":"2.0","method":"system_health","params":[]}' \
https://rpc-1.testnet.orbinum.io

Expect isSyncing: false and a non-zero peer count once it has caught up:

{"jsonrpc":"2.0","result":{"isSyncing":false,"peers":3,"shouldHavePeers":true},"id":1}

To test the node directly, bypassing Caddy and Cloudflare:

docker exec orbinum-rpc-node curl -s -H 'Content-Type: application/json' \
-d '{"id":1,"jsonrpc":"2.0","method":"system_health"}' http://localhost:9944

What you are serving​

URLUse
https://<your-domain>HTTP JSON-RPC
wss://<your-domain>WebSocket — Polkadot.js, Talisman, SubWallet

Both terminate at Caddy on the same domain.