Grow your network onto additional machines over a WireGuard tunnel.

Adding Service Servers - node.you

One box eventually fills up. Additional service servers carry more profile containers while the coordinator keeps doing what it does: TLS, routing, the database, and deciding where new services go. This guide adds one server; repeat it for each.

The coordinator talks to a remote server's agent, and proxies profile traffic to its containers, over a WireGuard tunnel. That keeps the internal secret and user traffic encrypted between machines, and means the containers never need public ports.

1. Prepare the new server

On a fresh Ubuntu server, repeat the base steps from the Single-Server Setup: deploy user + firewall (step 2) and Docker (step 7). It needs no nginx, certificate, or PostgreSQL.

2. The WireGuard tunnel

Install WireGuard and generate a keypair on both machines:

sudo apt install -y wireguard
umask 077
wg genkey | sudo tee /etc/wireguard/privatekey | wg pubkey | sudo tee /etc/wireguard/publickey

Pick a subnet that neither machine already routes. Check first with ip route on both hosts: cloud providers often occupy 10.x ranges and Docker takes 172.x, so something like 192.168.99.0/24 is usually the safe island. The examples below use coordinator = 192.168.99.1, new server = 192.168.99.2.

Coordinator, /etc/wireguard/wg0.conf (keys are pasted literal values, not file paths):

[Interface]
Address = 192.168.99.1/24
ListenPort = 51820
PrivateKey = <coordinator privatekey contents>

[Peer]
PublicKey = <server publickey contents>
Endpoint = <server public IP>:51820
AllowedIPs = 192.168.99.2/32
PersistentKeepalive = 25

New server, /etc/wireguard/wg0.conf:

[Interface]
Address = 192.168.99.2/24
ListenPort = 51820
PrivateKey = <server privatekey contents>

[Peer]
PublicKey = <coordinator publickey contents>
Endpoint = <coordinator public IP>:51820
AllowedIPs = 192.168.99.1/32
PersistentKeepalive = 25

Lock down the files, open the firewall for the tunnel, and bring it up (both machines; swap the peer IP accordingly):

sudo chmod 600 /etc/wireguard/wg0.conf
sudo ufw allow from <peer public IP> to any port 51820 proto udp
sudo ufw allow in on wg0
sudo systemctl enable --now wg-quick@wg0

Verify: sudo wg show reports a handshake on both ends, and each machine can ping the other's tunnel address.

Back up /etc/wireguard/ somewhere safe. Losing the keys means regenerating and re-pairing.

3. Profile image and agent

On the new server, build the profile image and configure the agent exactly as in the single-server guide (steps 8 and 9), with two differences in the agent's .env:

WG_ADDRESS=192.168.99.2
INTERNAL_SECRET=<same value as the coordinator's .env>
SERVER_NAME=sa1
DOMAIN=yourdomain.site
NODE_API_URL=https://yourdomain.site
  • WG_ADDRESS is this server's tunnel address. The compose file publishes the agent, and binds every profile container, to that address, so nothing is reachable from the public internet. (This matters: Docker's published ports bypass ufw, so the bind address is the real protection.)
  • Leave the ports line as "${WG_ADDRESS}:8000:8000"; the remote agent uses port 8000 (only the coordinator's own agent runs on 8001).

Also create the data directories and Docker network (the relevant parts of prepare-profiles.sh; no sudoers rule is needed here, since nginx runs only on the coordinator):

sudo mkdir -p /var/lib/nodeyou/profiles /var/lib/nodeyou/compose
sudo chown -R you:you /var/lib/nodeyou
docker network create nodeyou

Start the agent and check it from the coordinator, across the tunnel:

docker compose up -d --build      # on the new server
curl http://192.168.99.2:8000/api/internal/health   # on the coordinator

When you later update the agent's code, docker compose up -d --build applies it. --force-recreate alone only re-applies env and compose changes on the old image; code changes need --build.

4. Register it

In the app's Admin pane, Service Servers > Add server:

  • Name: a label for the rows and logs (e.g. sa1).
  • Agent address: the tunnel IP, 192.168.99.2.
  • Agent port: 8000.
  • Max services: how many profiles this machine should carry. This is per-server, so size it to the hardware.
  • Public IP: optional, purely your own note (useful for remembering the WireGuard endpoint).

The moment the row exists with status active, placement includes it: new services go to the least-loaded active server with free capacity. See Network Administration for capacity changes and draining.

Operational notes

  • One coordinator per agent. An agent (and the profile containers it creates) is configured for exactly one network via DOMAIN/NODE_API_URL/INTERNAL_SECRET. Don't point two coordinators at one agent.
  • Provisioned profiles pin their address. A profile's nginx config (coordinator side) and container bind (agent side) are written at provision time. If you later change a server's tunnel address, re-point the nginx configs with python -m scripts.rewrite_profile_nginx in the API's venv on the coordinator, and re-provision the affected profiles so their containers re-bind (deactivate + activate; data files are preserved).
  • Winding down: set the server to draining, wait for its services to deactivate or migrate, then offline.
Appearance
Theme
—