Skip to main content

Reaching your node

A client never guesses where you are. It reads your node record on chain, gets a base URL like https://node1.yourname.algo.xyz:9090, resolves that hostname in DNS, dials the resulting IP on that port, and completes a TLS handshake against a certificate issued for that name. Those four values live in three places, and they must keep agreeing after setup: a cloud instance restarts onto a fresh IP, a certificate expires, or you change a port and forget the URL on chain. tls.mode: acme and ip_sync keep them in sync.

info

This page assumes you've already registered an operator record (see Registering on-chain), that you own an NFD, and that algod.network is testnet or mainnet. NFD-backed DNS and ACME are not available on localnet.

The four things that must agree​

WhatWhere it livesWho keeps it right
Hostname → IPAn A/AAAA record in your NFD's u.dns fieldYou once with zs-node nfd-dns, or the ip_sync loop continuously
CertificateThe node's ACME cache on disktls.mode: acme, renewing in the background
Base URLYour node record on chain, keyed by (operator id, node id)The operator dashboard once, or ip_sync.url continuously
PortThe base URL only — never a DNS recordserver.listen

DNS resolves a name to an address and nothing more. The scheme and port a client connects on come from the base URL on chain, so u.dns only ever needs a single A record, and you should never encode a port there.

Turning on tls.mode: acme and ip_sync together collapses the four into one value you control. Your NFD name plus zs.nfd_record_name derives the hostname; the hostname derives the certificate; the hostname plus server.listen derives the base URL; and the loop keeps the DNS record pointed at wherever the machine is. At the defaults, a node that moves to a new IP is reachable again in about two minutes with no action from you, and certificates renew automatically.

Pick your node's hostname​

NFD names are published under a public DNS suffix that depends on the network:

NetworkAn NFD named yourname.algo is reachable as
mainnetyourname.algo.xyz
testnetyourname.dotalgo.io
localnetNot supported — ACME and ip_sync refuse to start

Leave zs.nfd_record_name empty and your node lives at that apex. To be reachable at node1.yourname.algo.xyz instead, you have two routes:

  • Set zs.nfd_record_name: node1. The node writes a named A/AAAA record under the node1 label inside your existing NFD's u.dns. No second NFD needed.
  • Own the segment NFD node1.yourname.algo and point zs.nfd_app_id at it, leaving the label empty. The node then publishes at that NFD's apex.

Either way, the certificate domain and the base URL written on chain follow automatically.

The label is a bare DNS label, and the node validates it strictly:

  • Lowercase letters, digits, and internal hyphens. Uppercase is rejected, not silently lowercased, because record-name comparisons downstream are case-sensitive.
  • Dotted labels are fine for a deeper name (api.node1).
  • A literal @ is rejected; leave the value empty for the apex.
  • A value ending in .algo is rejected. That's an NFD name, and it belongs in zs.nfd_app_id.
  • It is validated on every boot, even with TLS off.

Keep it short. The base URL on chain has a hard 248-byte ceiling, and a long label eats into it.

One label per node​

danger

Two nodes must never publish at the same record. If you run two nodes under one operator and leave both at the NFD apex, each node's ip_sync loop believes it owns the apex A record and rewrites it by removing and re-adding. Node B overwrites node A's address, node A's next tick overwrites it back, and so on forever. Each tick costs one on-chain transaction per node, and clients reach whichever node wrote last.

Every update is also a read-modify-write of your NFD's entire u.dns document, so two racing writers can drop each other's unrelated records, including a live _acme-challenge TXT record mid-issuance, which fails the certificate.

Nothing detects this and nothing warns you. Give every node its own zs.nfd_record_name, or its own NFD.

Switching an existing node from the apex to a label (or back) leaves the old record behind: ip_sync manages only the record it is configured for and never deletes co-resident entries. Delete the stale one yourself:

zs-node nfd-dns delete --name=@ --type=a
zs-node nfd-dns delete --name=@ --type=aaaa # only if you published v6

Which account signs the DNS writes​

Read this before you decide which wallet owns your NFD. It is the one choice here that is awkward to undo.

Both kinds of record this page depends on, the _acme-challenge TXT that proves you control the name and the A/AAAA that resolves it, live in your NFD's u.dns field. An NFD can only be updated by the account that owns it. Before each write the node resolves the NFD's current owner and refuses to sign for anyone else:

nfd update "yourname.algo": keystore has no key for owner ABCD…XYZ

To link an NFD to your operator record, the contract requires it to be owned by your operator owner address, the cold wallet these docs tell you to keep off the machine. The node holds only its hot signing key. So a node configured as the rest of the documentation describes starts and serves traffic normally, then fails on the first certificate renewal or the first IP change, with that message and nothing else. Neither zs-node init nor zs-node doctor checks for it.

Three ways out:

OptionWhat you doWhat it costs you
Give the node its own NFDHold a second NFD (or a segment NFD) on that node's signing address, and set zs.nfd_app_id to it explicitly in config.yaml. In this layout the explicit key is required: your operator record has no NFD linked, so the value the node would otherwise read from chain is 0, and ACME refuses to boot. One NFD per node also makes the clobbering above impossible.One NFD per node. The contract won't let the owner link an NFD it doesn't hold, so your operator record carries none and shows as a shortened address rather than a name.
Transfer your NFD to the signing addressNothing re-checks NFD ownership after registration, so moving the NFD to the node's signing address works and keeps your operator record's name linked.Your public operator identity now sits behind a hot key: whoever takes that key can rename, repoint, or transfer it. It also doesn't extend to a second node with a different signing key, and registering a new operator record later would fail the ownership check.
Don't use NFD DNStls.mode: manual (or a TLS-terminating reverse proxy) with your own DNS and your own certificate authority.You own renewals, the DNS record, and the base URL, by hand. See When you'd rather do it yourself.

Whichever you choose sets which account pays for the DNS writes; see What it costs.

Set it up​

Seed the DNS record​

Write the A record once, before you start the node. zs-node nfd-dns uses the same config and keystore as the daemon:

# Detect this machine's public IPv4 and write the apex A record
zs-node nfd-dns set-apex auto

# Under a label instead, matching zs.nfd_record_name
zs-node nfd-dns set-apex --name=node1 auto

# Both families, written in a single transaction
zs-node nfd-dns set-apex --ip-version=both auto

# Check what's there
zs-node nfd-dns list

The record is written with a 60-second TTL, matching what the ip_sync loop maintains, so a seeded record and a loop-managed one agree. Full flag reference: nfd-dns.

Turn on ACME​

server:
listen: ":9090" # bind all interfaces; this port
# is what gets published on chain
tls:
mode: acme
acme:
cache_dir: "/var/lib/zs-node/tls-cache" # absolute and persistent

domains is derived from your NFD and zs.nfd_record_name, and email defaults to operator_<id>@algo.xyz (a contact address for Let's Encrypt that need not be deliverable).

warning

cache_dir must be persistent, and the default probably isn't. It defaults to ./tls-cache, relative to the process working directory, and it holds your Let's Encrypt account key along with the issued certificates. A container without a volume, or a systemd unit whose WorkingDirectory moves, loses it and registers a new account on every boot until Let's Encrypt's rate limits cut you off. Point it at an absolute path on durable storage: /var/lib/zs-node/tls-cache under the generated systemd unit, or a path on the /app/data volume under Docker.

To rehearse without spending Let's Encrypt quota, first point directory_url at their staging endpoint (https://acme-staging-v02.api.letsencrypt.org/directory). Its certificates aren't publicly trusted, but every other part of the path is exercised.

Confirm the certificate​

Issuance runs in the background. The node logs listener up scheme=https and accepts connections before the first certificate exists, so early TLS handshakes fail, while /healthz on the plaintext private listener reads green the entire time, so a passing health check doesn't mean HTTPS works. Check from off-box instead:

curl -vI https://node1.yourname.algo.xyz:9090/v1/zs/details

Give it a few minutes. The DNS-01 challenge has to travel from an on-chain confirmation through the NFD indexer to the authoritative DNS zone before Let's Encrypt can see it. That's why propagation_delay defaults to 30s before the solver starts polling, and propagation_timeout to 6m. Don't shorten the delay: a resolver that queries too early caches an NXDOMAIN for the record you just wrote, and you get intermittent failures.

Turn on ip_sync​

server:
tls:
mode: acme
ip_sync:
enabled: true

The address families default to auto-detect, so the loop publishes A and/or AAAA for whichever families the host has a public address on, and skips the others.

Check your base URL on chain​

With ip_sync on, URL sync comes on with it. At startup the node derives https://<hostname>:<port> from your NFD, your label, and server.listen, compares it with your node record, and writes it if they disagree.

warning

The port is always appended, and it comes only from server.listen. Leave the default and you publish https://<hostname>:9090. Set listen: ":443" and you publish https://<hostname>:443. The host half of server.listen is discarded, so a loopback-bound node publishes an unreachable URL without warning. Bind a public address before you enable this.

Confirm with zs-node doctor, which checks that your on-chain base URL is set and that its port is the port the node listens on.

What ip_sync actually does​

The knobs are in Configuration. This is the behavior behind them.

Detection. Each tick, the node asks https://api64.ipify.org for its public address, once per active family, over a dialer pinned to IPv4 or to IPv6. The pinning is how the two families are told apart, and it means there is exactly one detection service, with no fallback. Detection also honors HTTP_PROXY and HTTPS_PROXY, so a node whose egress runs through a proxy publishes the proxy's address, which is usually not what you want.

The ipv4 / ipv6 tri-state. Omit them (the default) and each family is auto-detected: present ones are published, and absent ones are logged at debug, since a v4-only host with no public v6 is normal. Set one to false and that family is never probed. Set one to true to assert the host has a public address on it; a sustained detection failure then logs a WARN once, on the healthy-to-failing transition, then debug. Setting both to false while ip_sync is enabled is a startup error.

Writes. The loop replaces the A/AAAA entries at the one record it owns, the apex or your zs.nfd_record_name label. It leaves everything else in u.dns alone: other names, SRV records, TXT records, and the ACME challenge. That guarantee holds for one writer, not two; see One label per node.

Why ttl mirrors interval. TTL is how long resolvers may cache the record, so it bounds how long clients keep dialing your old address after the loop has published the new one. Matching it to the tick interval means recovery is limited by how quickly the loop detects the new address, rather than by how long resolvers cache the old one.

Cost. Steady state is one HTTPS probe per active family per tick and zero on-chain transactions. Only actual drift writes anything.

The base-URL side. When url.enabled is on (the default once ip_sync is enabled), the node also keeps your node record's base URL correct. It calls updateNodeUrl against your node box, which the contract authorizes for either the operator owner or the node's signing key, so your hot signing key drives it, at a flat single-fee transaction. It requires the contract un-paused and your operator status ACTIVE; an evicted operator produces a warning every tick. The 248-byte URL ceiling is checked at startup and is fatal there, so an over-long label fails loudly.

Set url.enabled: false when something in front of the node publishes a different port than server.listen, such as a CDN or a reverse proxy terminating on 443. The loop publishes the listen port verbatim, so left on it would advertise an endpoint nobody can reach. Manage the base URL from the operator dashboard instead.

warning

These preconditions refuse to start the node. They don't warn and skip.

  • ip_sync requires tls.mode to be manual or acme.
  • ip_sync requires algod.network to be testnet or mainnet.
  • url.enabled requires tls.mode: acme specifically, plus zs.escrow_app_id.

The last one catches people: url.enabled defaults to on, so a node using manual certificates with ip_sync enabled won't boot until you explicitly set url.enabled: false.

Once the node is running, the loop logs and tolerates every failure. Nothing gates serving, and the next tick retries.

What it costs​

EventOn-chain writesWhich account pays
Certificate issue or renewal2 NFD updates (write the TXT, then delete it)The NFD's owner
Your public IP changes1 NFD update (both families fold into one)The NFD's owner
Your base URL changes1 updateNodeUrl, flat single feeThe node's signing address
Steady stateNone—

Two different accounts pay, as a consequence of Which account signs the DNS writes. The node monitors and alerts on its signing balance. If a separate account owns your NFD, that account needs its own small ALGO float, and nothing is watching it for you.

When you'd rather do it yourself​

Use tls.mode: manual with your own cert_path and key_path if you already have a certificate pipeline; rotate by replacing the files and restarting. You run your own DNS and CA, and manage the base URL from the dashboard. If you still want the IP loop, set url.enabled: false.

A TLS-terminating reverse proxy in front of the node works too, as a single front door for a single node, never a fan-out across instances. See The operator/node split.

info

The loopback model. DNS-01 proves control of a name and never needs an inbound connection, so you can point your A record at 127.0.0.1 (zs-node nfd-dns set-apex localhost) and still hold a publicly trusted certificate for the public hostname. DNS returns loopback to everyone, so each user's browser reaches their own local node. Use it when each user runs a local node behind a shared web front end.

When it doesn't come up​