Skip to content

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.

  • 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.

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.

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:

your computer
printf 'Hetzner token: '; stty -echo; read -r HCLOUD_TOKEN; stty echo; echo; export HCLOUD_TOKEN

Run that read line again in every new terminal.

ToolWhyInstall
hcloudTalks to Hetznerhetznercloud/cli (macOS: brew install hcloud)
jqReads JSON in the scriptsbrew install jq or sudo apt install jq
ssh, curlReach the serverAlready on macOS and most Linux systems
Node.js 22.12 or later (22.x, 24.x or 26+), npmBuilds the appnvm install 24 && nvm use 24 (nvm), or nodejs.org/en/download
check
hcloud version && jq --version && ssh -V && curl --version && node --version && npm --version
hcloud server list # works once HCLOUD_TOKEN is set; an empty list is fine

Node.js 20.19 can run npm run dev, but not the tests that deploy-client.sh runs.

If you do not have a key yet, make one, then upload the public half to the project:

your computer
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 # skip if the file exists
hcloud ssh-key create --name my-key \
--public-key-from-file ~/.ssh/id_ed25519.pub
hcloud ssh-key list

my-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.

LimitWhat you seeWhat 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 quotaUse 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 saleSee Server choice.
SettingTested withWhy
Server typecax31: ARM64, 8 vCPU, 16 GB RAM, 160 GB diskThe node needs 16 GB of RAM. The Snapchain, statsd and Grafana images all have ARM64 builds.
Volume1000 GBFits the new-account limit. The database reached 557.86 GB.
Locationfsn1 (Falkenstein)The script’s default. Server and volume must be in the same location.
ImageUbuntu 24.04The 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.

For the tested cax31, from Hetzner’s price list on 2026-10-18, in USD:

ItemPrice
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:

See the price, create nothing
ESTIMATE_ONLY=1 SERVER_TYPE="" VOLUME_GB=1000 ./provision.sh

Read the script first: provision.sh

  • Checks that each entry in SSH_SOURCE_CIDRS looks like an address with a prefix, such as 203.0.113.7/32. An empty $MY_IP stops it here.
  • Checks that LOCATION exists. With SERVER_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 above MAX_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.sh first, unless you set WATCHDOG=0.
  • Prints the server’s IP. If the project already has the server $NAME, the volume $NAME-data or a firewall $NAME-fw applied to a server, it stops before creating or changing anything. With REUSE=1 it 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.

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=900 lets 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, and SKIP_CRONTAB=true unless you set OPERATOR_FID. A read node never makes blocks, so it earns no rewards; read the terms at Getting Started before you set SNAPCHAIN_AGREE_NO_REWARDS=yes.
  • On a disk under 1500 GB, installs two helpers. disk-guard.sh stops the node at 90% full. snapshot-space-saver.sh deletes 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.sh gives it one with script, and stops following its log once it prints “Upgrade complete”.
  • Checks /v1/info once 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: OK
sync: SYNCED (blockDelay < 10s on all shards)

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-client on the server and starts Caddy.
  • Caddy gets a Let’s Encrypt certificate and serves the app plus /v1/* (passed to the node on 127.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):

Terminal window
curl -sI https://<name>/ | grep -i content-security-policy # connect-src 'self'
curl -s https://<name>/v1/info | jq .version # your node, through Caddy

In 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.

your computer
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:

Terminal window
hcloud server list
hcloud volume list # volumes bill even when no server uses them
hcloud firewall list
hcloud primary-ip list # an unassigned primary IP still bills
hcloud ssh-key list # teardown keeps your key; delete it if you like:
# hcloud ssh-key delete my-key

When 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.