If you have been running ZeroTier for a while, you may have noticed that some of your node connections show RELAY instead of DIRECT in the zerotier-cli peers output. When a connection is relayed, your traffic bounces through ZeroTier’s public infrastructure before reaching the destination peer. This adds latency and means your network depends on ZeroTier Inc.’s servers being reachable.
There is a feature built into ZeroTier that most people never use: moons. A moon is a private root server that you control. It helps your nodes find and reach each other faster — without routing any of your actual traffic. Think of it as a private phone book that your nodes consult instead of, or in addition to, ZeroTier’s public ones.
In this tutorial, you will set up a ZeroTier moon server on an Ubuntu VPS. By the end, your ZeroTier nodes will orbit your private moon, improving peer discovery and reducing dependence on ZeroTier’s public infrastructure.
What Is a ZeroTier Moon?
To understand moons, you first need to understand how ZeroTier finds peers.
When two ZeroTier nodes want to connect, they need to discover each other’s real IP address so they can attempt a direct connection. ZeroTier’s public root servers — called planets — handle this discovery. Planets are ZeroTier’s fixed coordination servers, hardcoded into the client. They do not route your traffic; they just help nodes find each other.
A moon is your own private root server that works alongside the planets. When your nodes orbit your moon, they check it during peer discovery in addition to the public planets. This gives you several practical benefits:
- Lower latency for discovery. If your moon is geographically closer to your nodes than ZeroTier’s public planets (which are in the US), discovery rounds happen faster, so direct connections are established more quickly.
- Reliability. If ZeroTier’s public infrastructure is temporarily unreachable, your nodes can still find each other through your moon.
- Privacy. While ZeroTier does not see your traffic, the public planets do see the IP addresses of your nodes during discovery. With a private moon, you can reduce what is shared with ZeroTier’s infrastructure.
- Control. Some organizations require that all network coordination stay within infrastructure they manage. A moon satisfies that requirement.
A moon does not replace the ZeroTier network controller. Your network ID, authorization rules, and managed IPs are still handled by the controller (either hosted at my.zerotier.com or self-hosted separately). The moon only handles peer discovery.
Prerequisites
Before starting, make sure you have:
- A working ZeroTier setup with at least two nodes already joined to a private network. If you have not done that yet, set up basic ZeroTier connectivity first before proceeding.
- A dedicated Ubuntu VPS with a static public IP address to act as the moon. It does not need much power — even a 1-vCPU, 512 MB RAM instance is fine. Ubuntu 20.04 or 22.04 both work.
- Root or sudo access on all machines.
- UDP port 9993 open on the moon server’s firewall. This is the port ZeroTier uses for all communication.
- Basic familiarity with the Linux terminal and ZeroTier’s CLI.
In the examples below:
- Moon server public IP:
203.0.113.50 - Moon server Node ID:
d3a1f7c920 - Client node 1 ZeroTier IP:
192.168.100.1 - Client node 2 ZeroTier IP:
192.168.100.2
Step 1: Install ZeroTier on the Moon Server
The moon server is itself a ZeroTier node. Install ZeroTier on it the same way as any other node.
curl -s https://install.zerotier.com | sudo bash
Verify the service started:
sudo systemctl status zerotier-one
You should see active (running). Get the moon server’s Node ID:
sudo zerotier-cli info
Example output:
200 info d3a1f7c920 1.14.0 ONLINE
Write down this Node ID (d3a1f7c920 in this example). You will need it in the next step.
The moon server does not need to join your ZeroTier private network. It only needs to run the zerotier-one daemon to serve as a discovery root.
Step 2: Generate the Moon Configuration File
ZeroTier moons are defined by a JSON configuration file. You generate a template from the moon server’s own identity, then add the server’s public IP so nodes know where to reach it.
On the moon server, navigate to ZeroTier’s data directory and generate the template:
cd /var/lib/zerotier-one
sudo zerotier-idtool initmoon identity.public > /tmp/moon.json
This creates a JSON file describing the moon. Open it:
cat /tmp/moon.json
You will see something like this:
{
"id": "d3a1f7c920",
"objtype": "world",
"roots": [
{
"identity": "d3a1f7c920:0:long-public-key-string-here",
"stableEndpoints": []
}
],
"signingKey": "...",
"signingKey_SECRET": "...",
"timestamp": 0,
"updatesMustBeSignedBy": "...",
"waiting": true
}
The key field you need to fill in is stableEndpoints. This tells nodes where to reach your moon server. Edit the file:
sudo nano /tmp/moon.json
Change the stableEndpoints array from [] to include your server’s public IP and ZeroTier’s port:
"stableEndpoints": ["203.0.113.50/9993"]
The final roots section should look like:
"roots": [
{
"identity": "d3a1f7c920:0:long-public-key-string-here",
"stableEndpoints": ["203.0.113.50/9993"]
}
]
Save and close the file.
Step 3: Sign and Generate the Binary Moon File
ZeroTier moons are distributed as signed binary .moon files. Generate this file from the JSON you just edited:
cd /tmp
sudo zerotier-idtool genmoon moon.json
This produces a file named after the moon’s ID, for example:
000000d3a1f7c920.moon
The 000000 prefix is a standard ZeroTier convention — it is always six zeros followed by the 10-character Node ID of the moon.
Confirm the file was created:
ls -lh /tmp/*.moon
This binary file is what you will distribute to your other nodes. Keep the original moon.json somewhere safe as well — it contains the signingKey_SECRET needed to regenerate the moon file if you ever need to update it.
Step 4: Install the Moon on the Moon Server Itself
The moon server should also load its own moon definition so it can serve requests. Copy the file into ZeroTier’s moons directory:
sudo mkdir -p /var/lib/zerotier-one/moons.d
sudo cp /tmp/000000d3a1f7c920.moon /var/lib/zerotier-one/moons.d/
Restart ZeroTier to load the file:
sudo systemctl restart zerotier-one
Step 5: Open the Firewall on the Moon Server
The moon server must accept inbound UDP traffic on port 9993 from your nodes. If you use UFW:
sudo ufw allow 9993/udp
sudo ufw reload
If your VPS is behind a cloud provider’s security group (AWS, DigitalOcean, Hetzner, etc.), add an inbound rule for UDP port 9993 in the provider’s firewall console.
Verify ZeroTier is listening on that port:
sudo ss -ulnp | grep 9993
Expected output:
UNCONN 0 0 0.0.0.0:9993 0.0.0.0:* users:(("zerotier-one",pid=1234,fd=5))
Step 6: Distribute the Moon File to Your Nodes
You need to copy the .moon file to every node that should orbit your private moon. There are two ways to do this.
Option A: Copy the file directly
On Server A (Node 1), create the moons directory:
sudo mkdir -p /var/lib/zerotier-one/moons.d
From Server A, copy the moon file from the moon server over SSH:
scp [email protected]:/tmp/000000d3a1f7c920.moon /tmp/
sudo cp /tmp/000000d3a1f7c920.moon /var/lib/zerotier-one/moons.d/
Restart ZeroTier to load the new moon:
sudo systemctl restart zerotier-one
Repeat the same steps on Server B (Node 2) and any other nodes you want to orbit the moon.
Option B: Use the orbit command
If you prefer not to copy files manually, you can orbit a moon using the CLI. This tells ZeroTier to contact the moon server and fetch the definition automatically:
sudo zerotier-cli orbit d3a1f7c920 d3a1f7c920
The first argument is the World ID (the moon’s Node ID), and the second is the seed node to contact to fetch the moon definition. ZeroTier will reach out to the moon server on UDP 9993, download the definition, and begin orbiting it.
The CLI method works well when you can guarantee the moon server is already reachable on port 9993. The file method is more reliable for automated provisioning since it does not depend on live network connectivity at setup time.
Step 7: Verify the Moon Is Active
On any node that has orbited the moon, check the list of known moons:
sudo zerotier-cli listmoons
If the moon is loaded correctly, you will see output like:
[
{
"id": "000000d3a1f7c920",
"roots": [
{
"identity": "d3a1f7c920:0:...",
"stableEndpoints": ["203.0.113.50/9993"]
}
],
"timestamp": 1234567890123,
"signature": "...",
"updatesMustBeSignedBy": "...",
"waiting": false
}
]
The "waiting": false confirms the moon definition is active. An empty array [] means the file was not loaded — double-check the file location and restart zerotier-one.
Now check that your moon server appears in the peers list:
sudo zerotier-cli peers
You should see the moon server’s Node ID with a DIRECT path:
200 peers
<id> VER R LATENCY PATH
d3a1f7c920 1.14 - - DIRECT 203.0.113.50/9993
89a3f5b1c7 1.14 - 4ms DIRECT 198.51.100.10/52311
Root servers (planets and moons) display - for latency — that is expected. The important part is the DIRECT label, which confirms your node has a live connection to your private moon.
Common Mistakes and Troubleshooting
listmoons returns an empty array []
The .moon file is not in the right location, or ZeroTier has not been restarted since you placed it there. Check the directory:
ls -la /var/lib/zerotier-one/moons.d/
Make sure the file is present and readable by root. Then restart:
sudo systemctl restart zerotier-one
Moon server does not appear in zerotier-cli peers
The moon server is not reachable on UDP port 9993. Test reachability from a node:
nc -zu 203.0.113.50 9993 && echo "port open" || echo "port closed"
If it shows port closed, check the moon server’s UFW rules and cloud security group. Also confirm ZeroTier is actually running on the moon server with sudo systemctl status zerotier-one.
Nodes still show RELAY after orbiting the moon
A moon helps with peer discovery, not peer direct connectivity. If two nodes sit behind symmetric NAT or have restrictive firewalls that block peer-to-peer UDP, the connection will still be relayed even after the moon accelerates discovery. To reduce relay usage, ensure UDP port 9993 is not blocked on the client nodes themselves. You can check with:
sudo zerotier-cli peers
If a node still shows RELAY after a few minutes, the NAT situation between the peers is the likely cause, not the moon configuration.
The .moon filename is wrong
The binary moon file must be named exactly 000000 followed by the 10-character Node ID, for example 000000d3a1f7c920.moon. A misnamed file is silently ignored by ZeroTier.
zerotier-idtool initmoon command not found
This can happen on older ZeroTier versions. Make sure you are running ZeroTier 1.6 or later:
sudo zerotier-cli info
sudo apt update && sudo apt upgrade zerotier-one
Best Practices
Run at least two moons. A single moon is a single point of failure. If that VPS goes down, peer discovery falls back to ZeroTier’s public planets — so your nodes still connect eventually — but for production environments, run two moons on servers in different regions. Each node orbits both.
Place the moon geographically close to your nodes. The moon’s job is to speed up discovery. A moon in Frankfurt helps European nodes; one in Singapore helps Asian nodes. Match the moon’s location to where most of your nodes are.
Keep the moon server lean. The moon server only needs to run zerotier-one. Do not deploy application workloads on it. A stable, low-traffic server is a reliable moon.
Backup the moon files. Keep the .moon binary and the original moon.json (which contains the signingKey_SECRET) in secure storage outside the moon server. If the server is lost and you have no backup, you cannot regenerate an identical moon — you would have to create a new one and update every node.
Update ZeroTier on the moon server first. Since nodes depend on the moon for discovery, update the moon server during a low-traffic window and verify it is back online before rolling out updates to the rest of your fleet:
sudo apt update && sudo apt upgrade zerotier-one
Monitor port 9993 availability. Add an uptime check (using a tool like UptimeRobot or a Prometheus blackbox exporter) that probes UDP port 9993 on your moon server. If the moon goes down silently, you want to know before it affects your nodes.
Conclusion
You now have a private ZeroTier moon server running on Ubuntu. Your nodes orbit it alongside ZeroTier’s public planets, using it as an additional peer discovery root. This makes your ZeroTier network faster, more reliable, and less dependent on external infrastructure.
What you accomplished:
- Installed ZeroTier on a dedicated Ubuntu VPS as the moon server
- Generated and signed a moon definition file with the server’s public IP as its stable endpoint
- Distributed the moon file to existing nodes and verified they are orbiting it
- Configured the firewall to allow UDP 9993 traffic to the moon server
- Validated the moon appears as a direct peer on all orbiting nodes
From here, you can:
- Add a second moon on a different VPS for geographic redundancy and high availability
- Automate moon file distribution with a configuration management tool like Ansible when provisioning new nodes
- Explore self-hosting the ZeroTier network controller itself using the built-in controller API in
zerotier-one, so the entire ZeroTier control plane — both discovery and authorization — stays within your own infrastructure - Pair your ZeroTier overlay network with an internal DNS resolver (such as CoreDNS or Pi-hole) that maps hostnames to ZeroTier IPs, so nodes can reach each other by name instead of memorizing managed IP addresses