Server and node
Set up Hetzner for the scripts; the tested node costs at most $102.29 a month.
Set up a Hetzner Cloud account, an API token and an SSH key before step 1. Hetzner is the only tested provider: the scripts call Hetzner’s hcloud command-line tool, so other clouds need different scripts.
What you need
Section titled “What you need”- A Hetzner Cloud account with an empty project.
- An API token for that project, Read & Write.
- Your SSH key uploaded to that project.
- On your computer:
hcloud,jq,ssh,curl, and Node.js 22.12 or later (22.x, 24.x or 26+) with npm. - A Hetzner volume limit of at least 1000 GB.
- About 14 hours during which the server can run without you.
- Only to post: a test Farcaster account that has an FID and paid storage. Reading needs no account. Registering an FID and paying for storage are onchain actions that this guide does not cover; see the Farcaster docs.
Hetzner account and project
Section titled “Hetzner account and project”Sign up at Hetzner, verify your email, and enter contact and billing details. Hetzner’s guide: Hetzner Account Getting Started.
In the Hetzner Cloud console, create a new project just for this, for example snapchain. A separate project keeps the token’s reach small and makes cleanup easy to check: when you are done, the project should be empty. Hetzner explains projects in its Cloud FAQ.
API token
Section titled “API token”Inside the project, create an API token with Read & Write permission. Hetzner’s steps: Generating an API token. Hetzner shows the token only once.
Read it into your shell with a hidden prompt, so it stays out of your shell history:
printf 'Hetzner token: '; stty -echo; read -r HCLOUD_TOKEN; stty echo; echo; export HCLOUD_TOKENRun that read line again in every new terminal.
| Tool | Why | Install |
|---|---|---|
hcloud | Talks to Hetzner | hetznercloud/cli (macOS: brew install hcloud) |
jq | Reads JSON in the scripts | brew install jq or sudo apt install jq |
ssh, curl | Reach the server | Already on macOS and most Linux systems |
| Node.js 22.12 or later (22.x, 24.x or 26+), npm | Builds the app | nvm install 24 && nvm use 24 (nvm), or nodejs.org/en/download |
hcloud version && jq --version && ssh -V && curl --version && node --version && npm --versionhcloud server list # works once HCLOUD_TOKEN is set; an empty list is fineNode.js 20.19 can run npm run dev, but not the tests that deploy-client.sh runs.
SSH key
Section titled “SSH key”If you do not have a key yet, make one, then upload the public half to the project:
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 # skip if the file existshcloud ssh-key create --name my-key \ --public-key-from-file ~/.ssh/id_ed25519.pubhcloud ssh-key listmy-key is the name you pass as SSH_KEY=my-key in step 1 of the Full run. Keys belong to one project, so upload it to the one you made above. The hcloud ssh-key create command is from the hcloud CLI tutorial.
Hetzner limits
Section titled “Hetzner limits”| Limit | What you see | What to do |
|---|---|---|
| New accounts may rent at most 1024 GB of volumes in total, across all projects. The official docs ask for 1.5 to 2 TB. Your limits: console.hetzner.cloud/limits. | volumes size limit exceeded (resource_limit_exceeded), or from provision.sh before it creates anything: would pass the 1024 GB account quota | Use VOLUME_GB=1000, the tested size. node.sh then adds two disk helpers so the snapshot fits. If you already run a node with a 1000 GB volume, a second one needs a higher limit. provision.sh only sees the volumes in this project; if Hetzner raised your limit, set VOLUME_QUOTA_GB to it. |
| A server type isn’t available to rent. | provision.sh refuses the type and lists what is for sale | See Server choice. |
Server choice
Section titled “Server choice”| Setting | Tested with | Why |
|---|---|---|
| Server type | cax31: ARM64, 8 vCPU, 16 GB RAM, 160 GB disk | The node needs 16 GB of RAM. The Snapchain, statsd and Grafana images all have ARM64 builds. |
| Volume | 1000 GB | Fits the new-account limit. The database reached 557.86 GB. |
| Location | fsn1 (Falkenstein) | The script’s default. Server and volume must be in the same location. |
| Image | Ubuntu 24.04 | The script’s default. |
Hetzner can’t always rent every type in every location. If the type you set isn’t available to rent in LOCATION, provision.sh stops and lists the types that are, cheapest first. With SERVER_TYPE="" it rents the first one it can. Step 1 has the order it tries and what to set when nothing is for sale.
What it costs
Section titled “What it costs”For the tested cax31, from Hetzner’s price list on 2026-10-18, in USD:
| Item | Price |
|---|---|
| cax31 | $0.040 an hour, at most $24.99 a month |
| Public IPv4 | $0.001 an hour, at most $0.60 a month |
| 1000 GB volume | $76.70 a month ($0.0767 per GB). Hetzner bills it by the hour but lists only a monthly price. Our estimate is $0.123 an hour (monthly price ÷ 625 hours, the ratio of the server’s own monthly and hourly prices) |
| Total | $0.164 an hour with the volume estimate. $102.29 a month if you leave it on (each item stops at its monthly price). |
Our cax31 run took 14 h 06 m from renting the server to a synced node: $2.31 at that hourly total, not an invoice.
cpx42 (x86, 8 vCPU, 16 GB RAM) lists at $0.1314 an hour, at most $81.99 a month. With it instead of cax31, the total is $0.255 an hour and $159.29 a month. cpx42 has not been tested, so its sync time is unknown.
To see the type it would pick and the price, before creating anything:
ESTIMATE_ONLY=1 SERVER_TYPE="" VOLUME_GB=1000 ./provision.shWhat step 1 does
Section titled “What step 1 does”Read the script first: provision.sh
- Checks that each entry in
SSH_SOURCE_CIDRSlooks like an address with a prefix, such as203.0.113.7/32. An empty$MY_IPstops it here. - Checks that
LOCATIONexists. WithSERVER_TYPE=""it picks the first type for sale there with at least 16 GB of RAM; with a type set, it checks that one. If nothing fits, it lists what is available and creates nothing. - Reads the hourly price from Hetzner’s live price list and prints it. It stops before creating anything if that price ×
EXPECTED_HOURS(default 24) is aboveMAX_USD(default 8, in your account’s currency). - Creates a firewall, the data volume and the server. The firewall opens SSH (22, only to
SSH_SOURCE_CIDRS), gossip (3382/udp) and the web ports 80 and 443. The node’s own API ports 3381 and 3383 stay closed; the app reaches the node through Caddy. - Starts
watchdog.shfirst, unless you setWATCHDOG=0. - Prints the server’s IP. If the project already has the server
$NAME, the volume$NAME-dataor a firewall$NAME-fwapplied to a server, it stops before creating or changing anything. WithREUSE=1it continues with them, and it does not start a second watchdog if one is already running.
From the tested run, with cax31 set by hand:
[provision] server type cax31: 8 vCPU (shared), 16 GB RAM, 160 GB disk, arm[provision] local disk 160 GB < 2000 GB: adding a 1000 GB volume total ~0.164 /h[provision] cost guard OK: projected <= MAX_USD=2[provision] WATCHDOG=0: no watchdog started (no automatic spending cap).[provision] done. Next (in a new terminal, get the IP back with: IP=$(hcloud server ip snapchain-read-node)):Check it worked: hcloud server list shows snapchain-read-node running, and hcloud volume list shows snapchain-read-node-data.
What step 2 does
Section titled “What step 2 does”Read the script first: node.sh. It downloads and runs the official snapchain-bootstrap.sh and prints that file’s sha256.
- Checks Ubuntu 22.04 or 24.04, RAM and free disk.
MIN_FREE_GB=900lets it accept the 1000 GB volume, which shows 984 GB. - Installs Docker from Docker’s own apt repository.
- Writes the two answers the bootstrap would ask for into
~/snapchain/.env: that you get no rewards, andSKIP_CRONTAB=trueunless you setOPERATOR_FID. A read node never makes blocks, so it earns no rewards; read the terms at Getting Started before you setSNAPCHAIN_AGREE_NO_REWARDS=yes. - On a disk under 1500 GB, installs two helpers.
disk-guard.shstops the node at 90% full.snapshot-space-saver.shdeletes snapshot files the node has already read. They are needed because the official restore keeps all chunks, one tar per shard and the database until it finishes, and that does not fit on 1 TB. - Runs the official bootstrap. It needs a terminal, so
node.shgives it one withscript, and stops following its log once it prints “Upgrade complete”. - Checks
/v1/infoonce a minute. It stops when every shard is under 10 s behind on 3 checks in a row, or after 24 hours. Then it prints a health check.
The automatic install of the two disk helpers has unit tests (test/node_sh_test.sh) but has not run during a real sync yet.
What it prints while syncing (heights and times are examples):
[node.sh] Data disk 984 GB: arming disk-guard and snapshot-space-saver.[node.sh] disk-guard: snapchain-disk-guard.timer is active.[node.sh] space-saver: snapchain-space-saver.service is active.[node.sh] Waiting for sync: all shards blockDelay < 10s for 3 polls (timeout 86400s).[node.sh] 04:31:10Z HTTP API not answering yet (snapshot download in progress?).[node.sh] 05:04:36Z SYNCING shard 0: height=48210601 blockDelay=79236 | shard 1: height=48906556 blockDelay=57766 | shard 2: height=48723458 blockDelay=76739...[node.sh] Node is in sync.================ Snapchain health check ================HTTP API http://127.0.0.1:3381/v1/info: OKsync: SYNCED (blockDelay < 10s on all shards)What step 3 does
Section titled “What step 3 does”Read the script first: deploy-client.sh, deploy/Caddyfile
- Builds the app on your computer (
npm ci --ignore-scripts, tests,vite build). - Copies the built app, the Caddyfile and a compose file to
/opt/snapchain-clienton the server and starts Caddy. - Caddy gets a Let’s Encrypt certificate and serves the app plus
/v1/*(passed to the node on127.0.0.1:3381) from one HTTPS address. Background: Why the app and the node share one address.
[deploy-client] building client (npm ci --ignore-scripts, typecheck, tests, build)[deploy-client] uploading to root@<ip>:/opt/snapchain-client[deploy-client] waiting for HTTPS on https://<ip-with-dashes>.sslip.io ...[deploy-client] OK: https://<ip-with-dashes>.sslip.io (client) and https://<ip-with-dashes>.sslip.io/v1/info (node, proxied)Check it worked (use the address it printed):
curl -sI https://<name>/ | grep -i content-security-policy # connect-src 'self'curl -s https://<name>/v1/info | jq .version # your node, through CaddyIn the browser, the status at the top says Connected to your node; Details lists how far behind each shard is. Under Enter your FID to see your feed, type an FID and press Show my feed, or press Try a sample feed. An account that follows thousands of others, such as FID 3, can take one to two minutes to load.
Delete everything
Section titled “Delete everything”CONFIRM="$NAME" ./teardown.sh$NAME is the name from step 1 (export NAME=snapchain-read-node in the Full run). If the server with that name has a different ID from the one provision.sh recorded in this folder, teardown.sh stops and deletes nothing. Check NAME first; OTHER_SERVER_OK=1 overrides the check.
teardown.sh deletes the server, its volume (with the node’s database) and the firewall, then lists what is left. Running it twice is safe. It has been tested only with DRY_RUN=1 and against a fake hcloud, so review it first (teardown.sh) and check these yourself afterwards, in the console or with the CLI:
hcloud server listhcloud volume list # volumes bill even when no server uses themhcloud firewall listhcloud primary-ip list # an unassigned primary IP still billshcloud ssh-key list # teardown keeps your key; delete it if you like:# hcloud ssh-key delete my-keyWhen all lists are empty (except your SSH key) you pay nothing more. Then delete the API token in the console if you no longer need it.
If you started the watchdog in step 1 (A trial server that deletes itself), it runs teardown.sh for you after 24 hours, before the bill reaches MAX_USD, or when the node has made no progress for 2 hours.