Deployment12 min read

Netbird: Self-Hosted WireGuard Mesh VPN on a VPS

You manage servers spread across several client accounts and end up opening ports, maintaining separate UFW rules, or distributing static WireGuard keys by hand. Headscale addresses the same need for a flat network — we covered it in <a href="/blog/self-host-headscale-tailscale-vps">a dedicated guide</a>. Netbird takes a different approach, designed for multiple isolated networks: peer groups, per-network access rules, and a built-in web UI. This guide deploys the Netbird control plane (~28k GitHub stars, AGPLv3 server-side) on a dedicated VPS, then connects three nodes in a mesh without opening a single public port.

Static WireGuard, Headscale, Netbird: three different angles

Static WireGuard (see our WireGuard P2P guide) requires distributing a key pair per tunnel and editing /etc/wireguard/wg0.conf for every new peer. Workable for two or three fixed servers — unmanageable at agency scale.

Headscale reimplements the Tailscale coordination server: you keep the official Tailscale clients and get an unlimited-user flat network. One network per instance, no native isolation between different clients. The right choice for a single team that wants to stay in the Tailscale ecosystem.

Netbird takes the opposite approach: it ships its own client, its own control plane (Management + Signal + relay), and a concept of named networks with granular access rules. A single instance can host multiple fully isolated networks — making it the natural tool for an agency managing separate client environments. The web UI is included out of the box.

Netbird architecture: four components

A self-hosted Netbird deployment relies on four components, all provided in the same repository (netbirdio/netbird):

Management Server — the control plane brain. It distributes WireGuard keys, enforces access rules, and exposes the REST API consumed by the web UI. SQLite storage by default, MySQL or PostgreSQL as options.

Signal Server — the peer-to-peer signaling server. It facilitates the exchange of ICE connection descriptors between nodes at WireGuard tunnel setup time. No application traffic ever crosses it.

COTURN — the STUN/TURN relay server. It acts as a relay when a direct connection between two peers is impossible (double NAT, restrictive corporate network). Traffic only goes through COTURN when the direct attempt fails.

Dashboard — the web interface (React SPA) that consumes the Management API. Create networks, add peers, define access rules, and generate enrollment keys without touching the command line.

All traffic between clients flows over end-to-end encrypted WireGuard — Management and Signal only see enrollment metadata, never application traffic.

Prerequisites

Dedicated VPS for the control plane. Plan a separate VPS from your client nodes: 2 vCPU, 2 GB RAM minimum. A {{vps.start.name}} VPS is sufficient to start.

Ports to open on the control VPS:

443 (TCP) — Management and Dashboard behind an HTTPS reverse proxy.
3478 (UDP) — COTURN STUN/TURN.
49152-65535 (UDP) — COTURN dynamic range for relay sessions.

Client nodes need no inbound ports open: the Netbird client establishes outbound connections to the control plane.

Software required on the control VPS: Docker Engine 24+ and Docker Compose v2, a domain name pointing to the VPS IP, and a TLS certificate (Let's Encrypt via the provided script or your usual reverse proxy).

On each client node: the netbird binary (Debian/RPM package or static binary), root or sudo access.

Deploy the control plane and connect three nodes

01

Clone the repository and run the bootstrap script

On the control VPS, fetch Netbird's official script and let it generate the complete Docker Compose stack:

curl -fsSL https://github.com/netbirdio/netbird/releases/latest/download/getting-started.sh -o getting-started.sh
bash getting-started.sh

The script asks for your domain (e.g. netbird.your-domain.com), generates docker-compose.yml, config.yaml, dashboard.env and the COTURN configuration, then starts the stack. At the end it prints the web UI URL and a first Setup Key — keep it.

02

Verify all four services are running

Once the script finishes, confirm the containers are up:

docker compose ps

You should see four running services: netbird-management, netbird-signal, netbird-coturn and netbird-dashboard. Test the Management API from the VPS:

curl -s https://netbird.your-domain.com/api/v1/peers \
  -H 'Authorization: Token <your-PAT>'

An empty JSON response [] confirms the service is responding and no peers are enrolled yet.

03

Create a network and a Setup Key in the web UI

Open https://netbird.your-domain.com in a browser. Log in with the account created during setup (or via your configured OIDC provider).

Under Setup Keys, click Create Setup Key. Name it (vps-client-a), choose type Reusable (to enroll multiple machines with the same key) and an expiration period. Copy the key — you will need it on each node.

04

Enroll node 1

On the first client VPS, install the Netbird client:

curl -fsSL https://pkgs.netbird.io/install.sh | bash

Then connect it to your control plane:

netbird up \
  --management-url https://netbird.your-domain.com \
  --setup-key <YOUR_SETUP_KEY>

Confirm the connection:

netbird status

You should see Status: Connected and a mesh IP in the 100.64.x.x range assigned by your server.

05

Enroll nodes 2 and 3

Repeat exactly the same procedure on the two other client VPS. Same curl command to install the client, same netbird up command with the same --management-url and --setup-key (if it is of type Reusable).

Once all three nodes are enrolled, verify from node 1 that peers are visible:

netbird status --detail

The output lists each peer with its mesh IP, state (Connected or Connecting) and latency.

06

Test mesh connectivity without any open public port

Before testing, check the firewall state on node 1 — no inbound port should be open toward the other nodes:

sudo ufw status numbered

Only port 22 (SSH) should appear. Now ping node 2 via its mesh IP (visible in netbird status --detail, e.g. 100.64.0.2):

ping -c 3 100.64.0.2

The ping traverses the WireGuard tunnel established between the peers. If both nodes are behind strict NAT, COTURN provides the relay — the ping works in both cases without any additional UFW rule.

07

Verify direct vs relay connections

To distinguish a direct connection from a COTURN relay:

netbird status --detail

The Connection type column shows P2P for a direct connection or Relayed when COTURN is involved. P2P is the nominal state between two VPS with direct public IPv4 addresses. Relayed means Netbird had to go through the COTURN server — check that UDP ports 3478 and the 49152-65535 range are reachable from the nodes.

Multi-client isolation: peer groups and access rules

Netbird's strength over Headscale is its concept of isolated network per group. By default all peers enrolled with the same Setup Key join a common group. To isolate client A's servers from client B's servers:

1. Create a group per client in the web UI (Networks → Groups → Add Group). Name them client-a, client-b.

2. Assign each peer to its group. In the peer details, under Assigned Groups, add the corresponding group and remove the All group if you want no cross-group communication.

3. Define access rules (Access Control → Policies). A client-a-internal policy allows traffic between peers in the client-a group. No rule is created between client-a and client-b: the two networks are hermetic.

You can also define Network Routes: one peer acts as a router for a private subnet (e.g. 192.168.10.0/24) and exposes that subnet to other peers in the group, without those peers needing a Netbird client installed on every machine in the subnet.

Day-to-day operations

Renew or revoke a Setup Key. In the web UI, Setup Keys → your key → Revoke. Already enrolled peers keep their connection; new enrollment attempts with this key are rejected. Create a new key for future enrollments.

Revoke a peer. Peers → select the peer → Delete. The node is immediately removed from the mesh. On the client side, netbird status switches to Disconnected and WireGuard tunnels to that peer are torn down.

API access for automation. Netbird exposes a documented REST API. Generate a Personal Access Token (Settings → Access Tokens) and drive everything from your Ansible playbooks or CI pipelines:

curl -s https://netbird.your-domain.com/api/v1/peers \
  -H 'Authorization: Token <PAT>'

Monitoring. The Management Server exposes Prometheus metrics at /metrics. Connect Grafana to this endpoint to track connected peer count, active COTURN sessions, and signaling latency.

Hardening: 2FA on the Dashboard and Management database backup

The Netbird Dashboard supports OIDC (Keycloak, Authentik, Azure AD) — enable it to enforce MFA for all control plane administrators. Without SSO, the local account is protected by password only.

The Management Server's SQLite database is the only persistent state of your network: losing this file means re-enrolling all your peers. Mount a named Docker volume (netbird_management) and back it up daily:

docker run --rm \
  -v netbird_management:/data \
  -v /opt/backups:/backup \
  alpine tar czf /backup/netbird-$(date +%Y%m%d).tar.gz /data

Keep at least 7 days of rotation.

Troubleshooting

COTURN unreachable — peers stay Relayed or never connect.
Verify that UDP ports 3478 and the 49152-65535 range are open in the control VPS firewall (ufw status). Test from a client node: nc -u -z netbird.your-domain.com 3478. No response = filtered traffic. Some providers block wide UDP ranges by default — open them explicitly.

Peer stuck in Connecting.
This means Management/Signal communication succeeded (the peer enrolled) but the WireGuard tunnel cannot form. Common causes: the control VPS public IP is wrong in config.yaml (COTURN's --turn-external-ip field), or the UDP dynamic range is closed. Re-run getting-started.sh with an explicit --external-ip if the VPS is behind NAT.

DNS resolution fails between peers.
Netbird includes a DNS resolver that distributes <hostname>.netbird.cloud names to each peer. If ping node2.netbird.cloud fails while ping 100.64.0.2 works, check that the netbird service is running on the peer (systemctl status netbird) and that its DNS is active: resolvectl status | grep netbird.

Double NAT — no direct connection, COTURN overloaded.
If both peers are behind strict NAT (typically: cloud VPS behind a provider load balancer), direct WireGuard connections are impossible and all traffic goes through COTURN. Make sure the control VPS has a direct public IPv4 and that --turn-external-ip points to it. For client nodes behind strict NAT, nothing to do — COTURN is designed exactly for this case.

Stack update — nodes temporarily disconnected.
A Management Server update disconnects peers for a few seconds during container restart. Schedule updates outside traffic windows, or enable restart: always on all containers to minimize downtime.

What mesh networking changes for fleet management

A self-hosted Netbird mesh replaces three layers you were maintaining by hand: WireGuard key distribution, inter-server UFW rules, and cross-access documentation. Each new client VPS enrolls in one command; every revocation is instant and centralized.

Group isolation lets you scale without collision risk: servers from two different clients cannot see each other, even running on the same infrastructure. And the control plane stays under your control — no SaaS dependency, no seat limits, no per-node subscription.

To go further, codify node provisioning in Ansible (see our Ansible guide): client installation and netbird up become idempotent tasks in a reusable role.

Manage multiple client fleets from a single agency space

ServOrbit brings your clients' domains, hosting, and VPS together in a white-label reseller space. Add a control plane VPS for Netbird and manage your mesh network from the same dashboard.

Need help?

Browse our help center and FAQ, or reach our team — callback, WhatsApp or email. Support in French, English and Arabic.