Run a node on a VPS / dedicated server
This guide brings up a Sanect mainnet node (chain ID 7628, Cosmos chain sanect_7628-1) on any Ubuntu 22.04 / 24.04 machine you control: a cloud VPS, a bare-metal dedicated server, or a home lab box. No Railway account needed. The same container image the network's own nodes run is built from the public repo and started with Docker.
By default you get a full node: it stores recent chain state, validates every block, and serves JSON-RPC, Cosmos RPC and REST on your own machine. It does not sign blocks. When you want to become a validator, finish this guide first, then follow Run a validator.
Mainnet only
These steps target mainnet. The testnet uses a different chain ID and join settings (see Run a full / RPC node).
What you need
| Full / RPC node | Validator | |
|---|---|---|
| OS | Ubuntu 22.04 or 24.04 LTS | same |
| CPU / RAM | 4 vCPU / 8 GB | 4+ vCPU / 8+ GB |
| Disk | 100 GB SSD (NVMe is better) | 100 GB NVMe |
| Network | public IP, TCP 26656 reachable | same, stable uplink |
| Access | root or sudo | same |
Disk growth depends on chain activity; check df -h /data after a week and size accordingly. A node that keeps all history (an archive node) needs far more disk than the default pruned setup described here.
Time: about 15 minutes, plus the initial sync (state sync is normally a few minutes).
1. Prepare the server
sudo apt-get update
sudo apt-get install -y docker.io git jq curl ufw chrony
sudo systemctl enable --now docker chrony
# The clock must be accurate; CometBFT rejects blocks with skewed time.
chronyc tracking | grep 'System time'2. Persistent data disk
Chain data lives in /data. Use a real disk you control, not an ephemeral partition:
# With a separate disk (recommended), for example /dev/sdb:
sudo mkfs.ext4 /dev/sdb
sudo mkdir -p /data
echo '/dev/sdb /data ext4 defaults,noatime 0 0' | sudo tee -a /etc/fstab
sudo mount /data
# Or just use the root disk:
sudo mkdir -p /dataCheck with df -h /data.
3. Firewall
Only the P2P port must be reachable from the internet.
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp # SSH (restrict to your own IP if you can)
sudo ufw allow 26656/tcp # CometBFT peer-to-peer
sudo ufw --force enable
sudo ufw statusLeave 8545, 8546, 26657, 1317 and 8080 closed. They are RPC ports; section 8 shows how to publish an RPC endpoint safely if you want one.
4. Build the image
sudo git clone https://github.com/sanect/sanect-node.git /opt/sanect
cd /opt/sanect
sudo docker build --build-arg TARGET_EVM_CHAIN_ID=7628 -t sanect-node -f docker/Dockerfile .The build compiles sanectd from source and takes roughly 3–10 minutes.
Keep --build-arg TARGET_EVM_CHAIN_ID=7628
The EVM chain ID is baked into the binary at build time. Without this argument the image defaults to the testnet ID (76287): the node still syncs, but eth_chainId returns the wrong value, wallets and tools reject it, and EVM transactions can fail to validate. Always pass it for mainnet, on every rebuild.
5. Configure the node
First read the current values from the public RPC. Do not copy them from old notes: the P2P port belongs to a proxy and can change.
curl -s https://rpc.sanect.com/rpc/status | jq -r '.result.node_info.id, .result.node_info.listen_addr'
# 3446053b0b871ce94199a312f87ccdd17db22953 <- SEED_NODE_ID
# p2p.sanect.com:35998 <- SEED_PEER_HOSTCreate /etc/sanect/node.env:
sudo mkdir -p /etc/sanect
sudo tee /etc/sanect/node.env >/dev/null <<'EOF'
JOIN_NETWORK=true
CHAIN_ID=sanect_7628-1
EVM_CHAIN_ID=7628
HOMEDIR=/data/.sanectd
# Any name you like (shown to peers)
MONIKER=my-sanect-node
# How to reach the network: use the values printed above
SEED_NODE_URL=https://rpc.sanect.com
SEED_NODE_ID=3446053b0b871ce94199a312f87ccdd17db22953
SEED_PEER_HOST=p2p.sanect.com:35998
# Optional: tell peers your public address so they can dial you back.
# Needed if you want inbound peers (validators, public RPC). Use your real IP or a DNS name.
#EXTERNAL_P2P_ADDRESS=<YOUR_PUBLIC_IP>:26656
# Optional, faster start from a published snapshot. If it is unreachable the
# node falls back to state sync automatically.
#SNAPSHOT_MANIFEST_URL=https://snapshots.sanect.com/LATEST.json
# Memory. The defaults are tuned for big machines: a 16 GiB Go memory limit and a
# 10,000,000-entry state cache. On a smaller server, set these so the node
# cannot run the machine out of memory (about 70 % of RAM for MEMLIMIT_GIB):
#MEMLIMIT_GIB=5 # for an 8 GB server
#IAVL_CACHE_SIZE=781250 # smaller state cache (default 10000000)
# Inside the container, do not change.
PORT=8080
EOF
sudo chmod 600 /etc/sanect/node.envSEED_NODE_ID and SEED_PEER_HOST are required. Without them the node refuses to start.
6. Run it
sudo docker run -d \
--name sanect-node \
--restart unless-stopped \
--env-file /etc/sanect/node.env \
-v /data:/data \
-p 26656:26656 \
-p 127.0.0.1:8080:8080 \
sanect-node
sudo docker logs -f sanect-nodeWhat the options do:
--restart unless-stoppedbrings the node back after a crash or reboot.-v /data:/datakeeps the chain on your disk, outside the container.-p 26656:26656exposes P2P so peers can reach you.-p 127.0.0.1:8080:8080publishes the RPC only on the server itself.
On the first start you should see, in order: the node joining the existing chain, genesis fetched from the seed, peers wired, state sync configured, then Starting sanect node and blocks being committed. The first boot also prints your node's CometBFT ID and consensus public key. You can print the consensus key again at any time (you need it to register as a validator):
sudo docker exec sanect-node sanectd cometbft show-validator --home /data/.sanectd
# {"@type":"/cosmos.crypto.ed25519.PubKey","key":"..."}See Run a validator for what it is and what to do with it.
7. Check that it works
# catching_up should become false within a few minutes
curl -s localhost:8080/rpc/status | jq '.result.sync_info | {catching_up, latest_block_height}'
# compare with the network
curl -s https://rpc.sanect.com/rpc/status | jq -r '.result.sync_info.latest_block_height'
# peers (should be at least 1)
curl -s localhost:8080/rpc/net_info | jq -r '.result.n_peers'
# EVM JSON-RPC: the chain ID must be 0x1dcc (= 7628)
curl -s -X POST localhost:8080/ -H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1}'If eth_chainId returns 0x129ff (76287) the image was built without TARGET_EVM_CHAIN_ID=7628: rebuild it (section 4) and start over with an empty /data/.sanectd.
When catching_up is false and the height is within a few blocks of the network, you are synced. Chain ID is 7628; the base unit is asnct (18 decimals) and the symbol is SNCT.
State sync keeps recent history only
A node that joins by state sync starts from a recent state, so its earliest_block_height is near the tip, not 1. That is normal for a full node. It cannot answer queries about old blocks. Use the public archive RPC https://archive.sanect.com for those.
8. Optional: publish your RPC with TLS
Do this on a separate RPC node, never on a validator. Put a reverse proxy in front of the localhost-only port. With Caddy, which gets a certificate automatically:
sudo apt-get install -y caddy
sudo tee /etc/caddy/Caddyfile >/dev/null <<'EOF'
rpc.example.com {
reverse_proxy 127.0.0.1:8080
}
EOF
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo systemctl reload caddyPoint a DNS A record for rpc.example.com at the server first.
The node exposes the full EVM namespace set, including debug, and large log queries are expensive. For a public endpoint put rate limiting in front (Cloudflare or your proxy), and consider blocking debug_* requests. An unprotected public RPC can exhaust the node's memory under load.
List your node and get a free <name>.sanect.org address
You can show your node on the public network nodes page and claim a free name such as alice.sanect.org. Sanect runs a small proxy for these names, so you need no domain, DNS or TLS of your own. This is optional and only for nodes you want others to use.
cd /opt/sanect && sudo git pull
sudo bash scripts/sanect-publish-rpc.sh # choose 2) free community subdomainThe wizard asks for a name (3–32 characters: a-z, 0-9, -), sets EXPLORER_HEARTBEAT_URL and PUBLIC_RPC_URL in /etc/sanect/node.env, restarts the container, waits for the explorer to verify your node, and claims the name. By hand, the same thing is:
# in /etc/sanect/node.env (the URL must be reachable from the internet)
EXPLORER_HEARTBEAT_URL=https://scan.sanect.com/api/network/heartbeat
PUBLIC_RPC_URL=https://rpc.example.com # or http://<your-ip>:8080
# recreate the container (section 6), wait about 2 minutes, then:
TOKEN=$(sudo docker exec sanect-node cat /data/.sanectd/config/claim-token)
NODE_ID=$(curl -s localhost:8080/rpc/status | jq -r .result.node_info.id)
curl -s -X POST https://scan.sanect.com/api/network/claim-subdomain \
-H 'content-type: application/json' \
-d "{\"subdomain\":\"alice\",\"node_id\":\"$NODE_ID\",\"claim_token\":\"$TOKEN\"}"Things to know:
- Your node must be reachable. The explorer calls
PUBLIC_RPC_URLback to verify it, and the name proxies to it. A node behind a home router needs a port forward or a tunnel (for example a Cloudflare Tunnel). - Claim token. It is generated on first boot and stored in
/data/.sanectd/config/claim-token(root only). Whoever can read it can claim names for your node, so do not share it. One name per node; claiming a new one releases the old one. Claims are limited to a few per hour per IP. - Names lapse. If your node stops sending heartbeats for 24 hours, the name is released.
- What the public alias allows.
https://<name>.sanect.orgaccepts JSON-RPC on/and read-only requests on/rpc/*and/rest/*. It limits each client IP to 100 requests per second and refusesdebug_*,admin_*,personal_*,miner_*,txpool_*,trace_*andengine_*methods. These limits apply only to the alias: if you publish a plain IP and port, anyone can also call your node directly with no limits. To avoid that, put Caddy or a tunnel in front (see above) and only expose that.
9. Updating
cd /opt/sanect
sudo git pull
sudo docker build --build-arg TARGET_EVM_CHAIN_ID=7628 -t sanect-node -f docker/Dockerfile .
sudo docker stop sanect-node && sudo docker rm sanect-node
# then run the same `docker run` command from section 6Your chain data in /data is not touched. Watch the release notes: some upgrades need to happen at a specific block height.
10. Recovery: stuck, corrupted or far behind
If a node will not progress, a normal restart just resumes the broken state. The entrypoint has a one-time recovery switch that wipes the chain data and re-joins from a fresh state (it keeps priv_validator_key.json, priv_validator_state.json and node_key.json, so a validator never double-signs). To prevent accidental wipes it needs two variables: the switch and a confirmation that repeats the chain ID.
echo 'FORCE_SNAPSHOT_RESTORE=true' | sudo tee -a /etc/sanect/node.env
echo 'FORCE_SNAPSHOT_RESTORE_CONFIRM=sanect_7628-1' | sudo tee -a /etc/sanect/node.env
sudo docker stop sanect-node && sudo docker rm sanect-node
# run the `docker run` command from section 6 again and watch the logs; once it has caught up:
sudo sed -i '/^FORCE_SNAPSHOT_RESTORE/d' /etc/sanect/node.envRemove both lines after the node has caught up (the last command does it), or every restart will wipe the data again. With only one of them set, the node refuses to wipe and keeps your data.
Troubleshooting
numOutPeers=0 or "No addresses to dial". Test the path from your server:
nc -zv p2p.sanect.com 35998
sudo docker exec sanect-node grep -E '^(persistent_peers|seeds|external_address) ' /data/.sanectd/config/config.tomlIf persistent_peers is empty the env file was not read: check sudo docker inspect sanect-node | grep SEED_. If the TCP test fails, re-read the current port with the command in section 5.
The container exits immediately. Run sudo docker logs sanect-node. The most common cause is a missing SEED_NODE_ID or SEED_PEER_HOST.
address already in use on 26656. Find the other process with sudo ss -tlnp 'sport = 26656'.
Disk filling up. The default is pruned state (last 100,000 blocks). Grow the disk, or stop the container, copy /data to a larger one, and start it again.
docker build cannot resolve github.com (typically inside WSL or behind a local DNS resolver). The build containers cannot reach the host's resolver. Give Docker public DNS and restart it, then rebuild:
echo '{"dns":["1.1.1.1","8.8.8.8"]}' | sudo tee /etc/docker/daemon.json
sudo systemctl restart dockerThe explorer shows my node as "Lagging", or it falls further behind over time. Check with section 7. Older images batched outgoing P2P messages for 100 ms (flush_throttle_timeout), which made a following node commit slightly slower than the chain produces blocks, so it drifted behind by about a block every ten seconds until restarted. Current images set 10 ms for joining nodes automatically: git pull, rebuild (section 9) and recreate the container. To fix a running node without rebuilding:
sudo sed -i 's/^flush_throttle_timeout = .*/flush_throttle_timeout = "10ms"/' /data/.sanectd/config/config.toml
sudo docker restart sanect-node(FLUSH_THROTTLE_TIMEOUT in node.env overrides the value on current images.) A restart alone also catches the node up within seconds, but on an old image it starts drifting again. Lag does not affect correctness; it only makes answers a little stale.
Killed by the system (exit 137). The node ran out of memory. Use a machine with at least 8 GB, and do not expose an unprotected public RPC on it.
Next steps
- Run a validator: sign blocks and earn rewards (use a fresh server and a unique validator key; never copy a validator's data directory to a second machine).
- Run a full / RPC node: the Railway and local-devnet paths.