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.
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:
| Layer | What it does |
|---|---|
| Cloudflare proxy | L3/L4 and L7 DDoS protection in front of the origin |
| Cloudflare WAF skip rule | Stops managed bot challenges from breaking legitimate RPC clients |
| Caddy rate limit | 500 requests / 10s per IP for RPC, 30 / 60s for WebSocket upgrades, keyed on CF-Connecting-IP |
| Caddy connection caps | 400 HTTP + 1500 WS concurrent, summing below the node's 5000 so saturation is impossible |
| Origin firewall | 80/443 from Cloudflare only; 9944 and 9615 denied |
| Container limits | RPC_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:
| Variable | Notes |
|---|---|
RPC_NAME | Shown in telemetry. Use rpc-1, rpc-2, … per node |
RPC_NODE_KEY | openssl rand -hex 32 — unique per node |
RPC_DOMAIN | The domain from step 2; Caddy serves it |
TELEMETRY_URL | Already set correctly — see Telemetry |
RPC_NODE_KEY stableIt 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
| URL | Use |
|---|---|
https://<your-domain> | HTTP JSON-RPC |
wss://<your-domain> | WebSocket — Polkadot.js, Talisman, SubWallet |
Both terminate at Caddy on the same domain.