Run a node
Running a node takes three steps: start the container, register your Star on chain, and check that everything connected. After that it works and earns on its own.
Open alpha runs on Arbitrum Sepolia. Chain id
421614, imagesingulanetwork/star:alpha. Gas comes from the Arbitrum Sepolia faucet. No real money is needed.
What you need
Section titled “What you need”- A Linux server with a public IP.
- Incoming TCP ports 443, 8443, 2053 and 2083 open. All four are used; cover presets explains why.
- A wallet with a little Sepolia ETH for one transaction. It becomes the Star’s owner and receives the payouts. Its key never goes near the server.
Step 1 — start the node
Section titled “Step 1 — start the node”The quickest way is the installer:
curl -fsSL https://stargate.singula.network/install-node.sh | sudo bashIt checks that Docker is there and the four ports are free, asks for a country code, starts the container, and prints the signer address you need in step 2. Run it again later and it updates the image while keeping the node’s identity.
It does not install Docker and does not set up a reverse proxy. REALITY terminates TLS itself and has to see the original ClientHello, so the node’s inbounds take the host’s ports directly.
Below is the same thing done by hand.
The star program serves every role and reads its role from StarRegistry at start. On the first run there is no role yet: the node creates its store, prints its identity and waits to be registered.
Everything the node owns lives in one directory: its store, the key the store is encrypted with, and, for the first hour, the recovery phrase. Mount that directory whole at /var/lib/star.
First create the master secret. The node will not start without one and will not invent one: if it quietly made a new secret, everything stored under the old one would be lost.
mkdir -p ~/.singulachmod 700 ~/.singulaopenssl rand -hex 32 > ~/.singula/master_keychmod 600 ~/.singula/master_keyservices: star-node: image: singulanetwork/star:alpha container_name: singula-star restart: unless-stopped network_mode: 'host' # As you, so the files in the mount stay yours. Left to itself the image # drops to its own user, which cannot write to a directory it does not own # — and the node fails on the first thing it does, opening its store. user: "${UID}:${GID}" volumes: - ~/.singula:/var/lib/star environment: - MASTER_SECRET_FILE=/var/lib/star/master_key - BLOCKCHAIN_RPC_URL=https://sepolia-rollup.arbitrum.io/rpc - CHAIN_ID=421614 - NODE_COUNTRY_CODE=DE # - REVERSE_PROXY=node-de.example.com:443UID="$(id -u)" GID="$(id -g)" docker compose up -d && docker compose logs -fNo root is needed anywhere, not even for port 443: Docker sets net.ipv4.ip_unprivileged_port_start to 0 and the image gives xray a file capability to bind ports. You only need an account that may use Docker: usermod -aG docker $USER.
network_mode: host is the simplest way to expose the four ports and report the right public IP.
Without Compose:
docker run -d --name singula-star --network host \ --user "$(id -u):$(id -g)" \ -v ~/.singula:/var/lib/star \ -e MASTER_SECRET_FILE=/var/lib/star/master_key \ -e BLOCKCHAIN_RPC_URL=https://sepolia-rollup.arbitrum.io/rpc \ -e CHAIN_ID=421614 -e NODE_COUNTRY_CODE=DE \ singulanetwork/star:alphaOr skip Docker and run the binary under systemd, with BADGER_DIR=/var/lib/star/badger and the same variables.
Settings worth knowing
Section titled “Settings worth knowing”| Variable | Default | What it does |
|---|---|---|
NODE_COUNTRY_CODE | XX | The node’s country. Used only when a client asks for a country by name |
NODE_MAX_SESSIONS | 100 | How many sessions the node carries at once |
REVERSE_PROXY | — | The public host:port clients should reach you at, when it differs from the machine’s own |
METRICS_HTTP_PORT | 9101 | Port for /status (JSON) and /metrics (Prometheus); 0 turns it off |
METRICS_BIND | 127.0.0.1 | Where that listens. Widen it only behind a firewall or a password — see below |
BADGER_DIR | /var/lib/star/badger | Local store |
Step 2 — register the Star
Section titled “Step 2 — register the Star”The node prints a banner in its log with its signer address, the node’s hot key:
STAR — MANUAL REGISTRATION REQUIRED Signer address : 0x...If the banner has already scrolled away, the local status page has the same address:
curl -s localhost:9101/status | jq -r .info.signerIn Stargate, connect the wallet that will own the Star, open Stars, press + Register Star, paste the signer address and sign the transaction.
That calls StarRegistry.register(signer). There is no role to pick and no queue: every new Star is a node. One signer address can belong to only one Star.
You do not need to restart anything. The node watches the chain and picks up its starId once the transaction lands.
Step 3 — check it
Section titled “Step 3 — check it”On the Star’s card in Stargate you should see the role Node, a green indicator, a public address and a recent heartbeat; the node sends one every 30 seconds.
The node’s log shows Registered with CP: it found a live Control Plane in CPEndpointRegistry, holds a connection to it and is waiting for sessions.
Watching a node
Section titled “Watching a node”A node reports on itself at two addresses on one port: /status returns JSON, /metrics the Prometheus format. The figures are the same either way: how many sessions it carries, whether it is connected to a Control Plane, whether it has found itself on chain, uptime, CPU and memory.
Next to the figures is an info block: the signer address, starId, country, REALITY keys, and the registry and RPC provider it uses. The key is stripped from the RPC address. In Prometheus the same fields arrive as labels on singula_info.
curl -s localhost:9101/status | jqThe page listens on localhost only, and it is best left that way. Its answer says role: node and its metrics are named singula_node_*, so one request to an open port tells anyone that a node lives here. Nodes work hard not to give themselves away: four cover presets, an imitation of a popular site on 443, and no node address anywhere on chain.
If you need to, widen access with METRICS_BIND, but only behind a firewall, an address allowlist or a reverse proxy with a password. The node notes it in the log when you do.
Cover presets
Section titled “Cover presets”A node always runs four REALITY inbounds, one per region. Each listens on its own port and looks like a visit to a site that is popular in that region. The set is built into the program; a preset cannot be switched off.
| Preset | Port | Cover site | Server names |
|---|---|---|---|
default | 443 | www.adobe.com | www.adobe.com, helpx.adobe.com, assets.adobe.com |
RU | 8443 | yandex.net | yandex.net, yastatic.net, mc.yandex.ru |
CN | 2053 | www.qq.com | www.qq.com, weixin.qq.com, www.gtimg.com |
IR | 2083 | www.aparat.com | www.aparat.com, www.digikala.com, asredanesh.com |
The client picks the preset. If it asks for none, the node uses default: nobody in the network works out where a client is.
Every preset is also served over xHTTP on the same port, at one path fixed for this node and preset. A client that is blocked on raw TCP switches to xHTTP without a new session.
Open all four ports. A closed port does not switch a preset off; it just leaves clients from that region unable to connect.
Behind a reverse proxy
Section titled “Behind a reverse proxy”REALITY terminates TLS itself and must see the original ClientHello, so a proxy in front of the node may route connections but never decrypt them. Route by SNI with TLS passed through:
# TLS passthrough, SNI → the right inboundservices: star: image: singulanetwork/star:alpha restart: unless-stopped user: "${UID}:${GID}" networks: [public] volumes: - ~/.singula:/var/lib/star environment: - MASTER_SECRET_FILE=/var/lib/star/master_key - BLOCKCHAIN_RPC_URL=https://sepolia-rollup.arbitrum.io/rpc - CHAIN_ID=421614 - NODE_COUNTRY_CODE=DE - REVERSE_PROXY=node.example.com:443 labels: - "traefik.enable=true" - "traefik.docker.network=public" - "traefik.tcp.routers.star-default.rule=HostSNI(`www.adobe.com`) || HostSNI(`helpx.adobe.com`) || HostSNI(`assets.adobe.com`)" - "traefik.tcp.routers.star-default.entrypoints=websecure" - "traefik.tcp.routers.star-default.tls.passthrough=true" - "traefik.tcp.routers.star-default.service=star-default" - "traefik.tcp.services.star-default.loadbalancer.server.port=443" - "traefik.tcp.routers.star-ru.rule=HostSNI(`yandex.net`) || HostSNI(`yastatic.net`) || HostSNI(`mc.yandex.ru`)" - "traefik.tcp.routers.star-ru.entrypoints=websecure" - "traefik.tcp.routers.star-ru.tls.passthrough=true" - "traefik.tcp.routers.star-ru.service=star-ru" - "traefik.tcp.services.star-ru.loadbalancer.server.port=8443" - "traefik.tcp.routers.star-cn.rule=HostSNI(`www.qq.com`) || HostSNI(`weixin.qq.com`) || HostSNI(`www.gtimg.com`)" - "traefik.tcp.routers.star-cn.entrypoints=websecure" - "traefik.tcp.routers.star-cn.tls.passthrough=true" - "traefik.tcp.routers.star-cn.service=star-cn" - "traefik.tcp.services.star-cn.loadbalancer.server.port=2053" - "traefik.tcp.routers.star-ir.rule=HostSNI(`www.aparat.com`) || HostSNI(`www.digikala.com`) || HostSNI(`asredanesh.com`)" - "traefik.tcp.routers.star-ir.entrypoints=websecure" - "traefik.tcp.routers.star-ir.tls.passthrough=true" - "traefik.tcp.routers.star-ir.service=star-ir" - "traefik.tcp.services.star-ir.loadbalancer.server.port=2083"
traefik: image: traefik:v3.1 restart: unless-stopped command: - --providers.docker=true - --providers.docker.exposedbydefault=false - --entrypoints.websecure.address=:443 ports: ["443:443"] volumes: - /var/run/docker.sock:/var/run/docker.sock:ro networks: [public]
networks: public: external: true# One public port; HAProxy routes by SNI without terminating TLS.services: star: image: singulanetwork/star:alpha restart: unless-stopped user: "${UID}:${GID}" networks: [edge] volumes: - ~/.singula:/var/lib/star environment: - MASTER_SECRET_FILE=/var/lib/star/master_key - BLOCKCHAIN_RPC_URL=https://sepolia-rollup.arbitrum.io/rpc - CHAIN_ID=421614 - NODE_COUNTRY_CODE=DE expose: ["443", "8443", "2053", "2083"]
haproxy: image: haproxy:2.9-alpine restart: unless-stopped depends_on: [star] ports: ["443:443"] volumes: - ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro networks: [edge]
networks: edge: {}
# haproxy.cfg — no TLS termination, REALITY must see the ClientHello# global# log stdout format raw local0# defaults# mode tcp# timeout connect 5s# timeout client 1h# timeout server 1h# frontend tls_in# bind *:443# tcp-request inspect-delay 5s# tcp-request content accept if { req_ssl_hello_type 1 }# acl is_ru req_ssl_sni -m end yandex.net yastatic.net mc.yandex.ru# acl is_cn req_ssl_sni -m end qq.com gtimg.com# acl is_ir req_ssl_sni -m end aparat.com digikala.com asredanesh.com# use_backend ru if is_ru# use_backend cn if is_cn# use_backend ir if is_ir# default_backend def# backend def# server star_def star:443# backend ru# server star_ru star:8443# backend cn# server star_cn star:2053# backend ir# server star_ir star:2083Identity and the recovery phrase
Section titled “Identity and the recovery phrase”All of the node’s keys — the signer, the REALITY keys, the session key — come from one secret created on the first run. A 24-word recovery phrase is written next to it, in /var/lib/star/recovery-phrase.txt, and deletes itself after an hour. Copy it straight away; it is kept nowhere else.
To restore a node, point NODE_MNEMONIC_FILE at a file holding the phrase. NODE_MNEMONIC works too, but the file is safer: a variable shows up in docker inspect and in compose files that end up in repositories. If the phrase has a passphrase, add NODE_MNEMONIC_PASSPHRASE; a different passphrase gives a different node.
The phrase restores the same keys, so the Star needs no new registration: the signer address matches the one already on chain.
The keys themselves are stored encrypted, under the key read from MASTER_SECRET_FILE — ~/.singula/master_key if you followed the steps above, /run/secrets/singula_master_key if you set nothing. Losing that file is fine while you have the phrase. If both are gone, the node needs a new identity and the owner rebinds the signer with updateSigner.
What happens next
Section titled “What happens next”The Control Plane starts sending sessions your way. Nodes are picked by load, heartbeat freshness and a random term — not by where the client is, which the Control Plane does not know. NODE_COUNTRY_CODE only matters when a client asked for your country.
Every four hours, and again shortly before an epoch ends, the Control Plane merges the intervals you served, asks the node to sign each one, and submits them to EpochAttestations. You are paid for time, not volume, but not all time counts: it is held back until 64 KiB has moved since the client’s last credited checkpoint, and if that does not happen within 10 minutes, it is dropped.
When the epoch closes, claimNode pays USDC to the owner wallet. Anyone can make the call, since the payee comes from the registry, so you do not need ETH on that wallet.
A second is paid at a fixed rate: about 0.2333 sUSDC today for a day of serving one session. See economics.
Getting no sessions is normal on a quiet network. It means there are few across the whole network right now, not that your region is wrong.
