Self-host the NetBird VPN server on a VPS
Run the NetBird mesh VPN on one VPS: DNS and TLS setup, the pinned quickstart script, setup keys for unattended peers, and how it compares to Headscale.
What self-hosting the NetBird VPN server gives you
Self-hosting the NetBird VPN server puts the control plane on a VPS you own: the part that holds the peer list, decides which machine may reach which, and helps two peers find each other behind NAT (network address translation). The tunnels themselves are still WireGuard, encrypted directly between your machines. What changes is that no outside company holds your device inventory or your login flow.
NetBird sits between two things you may already know. It is a mesh overlay, so peers connect to each other rather than sending everything through one gateway. It is also self-hostable end to end, which puts it against Headscale, the self-hosted Tailscale control server. If you have only ever run a single-gateway tunnel, read the difference between plain WireGuard and a mesh overlay first, because that mental model is what makes the rest of this page useful.
If what you actually want is one server that all your traffic exits from, a mesh is more machinery than the job needs. A plain WireGuard VPN on a single VPS or a Tailscale exit node does that with much less running.
What the stack actually runs
The layout changed recently, and most older write-ups describe the old one. As of August 2026, on release v0.76.2, the quickstart script writes a Compose file with three services by default.
netbird-servercarries the management API, the signal service, the relay with an embedded STUN listener, and an embedded identity provider. In older releases these were separate containers and the identity provider was a separate Zitadel install you had to build first.dashboardis the admin web console.traefikterminates TLS (transport layer security) and requests a certificate from Let's Encrypt on first start.
Two more services exist and stay off unless you say yes at a prompt. The NetBird Proxy service publishes internal services on public hostnames. CrowdSec filters abusive traffic. Neither is needed to build a working mesh, and both cost memory on a small box.
If you are coming from wg-easy in a single Docker container, this is a jump in parts count. What it buys you is access policies and per-user accounts, and peers that connect directly to each other instead of through one gateway.
What you need before you start
A public domain name is not optional. The dashboard, the API and the relay all ride HTTPS on port 443, and Traefik gets its certificate from Let's Encrypt using an HTTP challenge, which needs a name that resolves to this VPS from the public internet. A bare IP address will not work in this flow.
Create one A record, netbird.example.com pointing at the VPS public IPv4 address, and wait for it before you run anything.
dig +short netbird.example.comThat must print your server's address. Running the installer before DNS propagates means the certificate request fails on first start, and repeated failed validations run into Let's Encrypt rate limits, so you then wait an hour to try again.
Three ports must be reachable from the internet: TCP 80 for the certificate challenge and the redirect to HTTPS, TCP 443 for the dashboard, the API, signal and relay traffic, and UDP 3478 for STUN.
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 3478/udp
sudo ufw reload
sudo ufw statusOpen them on your provider's network firewall too. That is a separate control in most VPS panels, and it is the reason a box whose own ufw status looks correct still refuses connections.
STUN (session traversal utilities for NAT) is how a peer learns the public address and port its own NAT assigned, so two peers can attempt a direct tunnel. Block UDP 3478 and peers still connect, over the relay on TCP 443, so nothing looks broken. You get Connection type: Relayed on every peer instead, and all traffic crosses your VPS rather than going peer to peer.
On the software side you need Docker with the Compose v2 plugin, plus jq and curl. The script checks for all of them and stops if one is missing. If Docker is new on this box, get Docker Compose working on the VPS first.
Ports if you skip the bundled reverse proxy
Running without Traefik means the individual services are exposed directly, and the port list grows:
- TCP 80, HTTP redirects
- TCP 443, HTTPS
- TCP 33073, management gRPC
- TCP 10000, signal gRPC
- TCP 33080, relay over WebSocket or QUIC
- UDP 3478, STUN
Pick this only when the box already terminates TLS for something else. Otherwise the bundled Traefik is fewer rules and fewer mistakes.
Install the NetBird server with the quickstart script
The documented one-liner pipes the newest release straight into a shell:
curl -fsSL https://github.com/netbirdio/netbird/releases/latest/download/getting-started.sh | bashPin it instead. latest moves, so the same command run two weeks apart produces two different installs, and nothing on disk records which one wrote your config. Download a tagged release, read it, then run it.
mkdir -p ~/netbird
cd ~/netbird
curl -fsSL -o getting-started.sh \
https://github.com/netbirdio/netbird/releases/download/v0.76.2/getting-started.sh
less getting-started.sh
bash getting-started.shThe script asks for the domain first:
Enter the domain you want to use for NetBird (e.g. netbird.my-domain.com):Then it asks how TLS will be handled:
Which reverse proxy will you use?
[0] Traefik (recommended - automatic TLS, included in Docker Compose)
[1] Existing Traefik (labels for external Traefik instance)
[2] Nginx (generates config template)
[3] Nginx Proxy Manager (generates config + instructions)
[4] External Caddy (generates Caddyfile snippet)
[5] Other/Manual (displays setup documentation)
Enter choice [0-5] (default: 0):Take [0]. Options 2 through 5 write a config snippet and leave the wiring to you, which is correct on a box that already runs a proxy and wrong on a fresh one. Option 0 then asks for a Let's Encrypt email address, used for expiry notices.
Say no to the NetBird Proxy service on a first install. It wants two more DNS records, proxy.netbird.example.com and the wildcard *.proxy.netbird.example.com, and it does nothing for a plain mesh. Say no to CrowdSec as well. Both can be added later.
The script writes into the current directory: docker-compose.yml, config.yaml with mode 600, dashboard.env, and traefik-dynamic.yaml when you chose the bundled Traefik. Treat that directory as state you keep, because config.yaml holds the key that encrypts data in the store. Losing it is not something a reinstall fixes.
docker compose ps
docker compose logs -f netbird-serverEvery service should read running, and the server log should settle rather than restart in a loop. Watch the certificate separately:
docker compose logs traefik | grep -i acmeACME (automatic certificate management environment) is the protocol Traefik uses to get the certificate. Errors here are almost always DNS or a closed port 80.
Create the first admin account
Open https://netbird.example.com. On a fresh install this lands on a setup page rather than a login form. Enter an email address, a name and a password, then click Create Account. That becomes the first admin, and the page redirects to the login form.
That account lives in NetBird's own user store, powered by an identity provider embedded in the netbird-server container. Nothing external is involved. This is the biggest change from the self-hosted NetBird of a year ago, when a working install meant standing up Zitadel or Keycloak first and copying four OIDC (OpenID Connect) values into setup.env before anything would start at all.
If you get a browser certificate warning instead of the setup page, the certificate did not issue. Fix that before going further, because the dashboard talks to the API over the same hostname and fails in confusing ways behind a bad certificate.
Join your first peer
Install the client on any Linux machine, including the VPS itself if you want it in the mesh:
curl -fsSL https://pkgs.netbird.io/install.sh | shOn Debian and Ubuntu that script configures NetBird's package repository and then installs the client through apt, so the package manager ends up owning it either way. If piping a script into a shell bothers you, save it first with curl -fsSL -o install.sh https://pkgs.netbird.io/install.sh and read it before running sh install.sh. Either way, confirm what landed:
apt-cache policy netbirdnetbird is the command line client and the daemon. netbird-ui is the desktop tray app, and a headless server has no use for it.
Now point the client at your server:
sudo netbird up --management-url https://netbird.example.comOmit --management-url and the client registers with NetBird's hosted service, because that is the compiled-in default. The command still succeeds, the machine still gets an address, and your self-hosted dashboard stays empty. This catches nearly everyone once.
The command prints a URL to open in a browser to finish the login. After that:
netbird status
ip addr show wt0Read four lines from netbird status: Management: Connected, Signal: Connected, a Relays: line reporting every relay available, and a NetBird IP: in the overlay range. wt0 is the WireGuard interface NetBird creates, and it should carry that same address.
Join a second machine unattended with a setup key
Browser login does not work for a machine with no browser and nobody in front of it. A setup key is a pre-authentication token that registers a machine without the interactive step. Create one in the dashboard under Setup Keys.
There are two kinds. A one-off key authenticates exactly one machine and is then spent. A reusable key registers many, with an optional cap on how many. Both take an expiry, and both can auto-assign the new peer to a group, so the access rules on that group apply the moment the machine appears.
sudo netbird up --setup-key <SETUP-KEY> \
--management-url https://netbird.example.com \
--hostname build-runner-01--hostname sets the name shown in the dashboard. Without it the peer takes whatever the machine calls itself, and a fleet of entries all named ubuntu helps nobody.
For containers and short-lived build agents, mark the key ephemeral when you create it. Peers registered with an ephemeral key are removed automatically once they have been offline for more than 10 minutes, which keeps dead entries out of the peer list.
One limit to understand before you plan around setup keys: expiring or deleting a key stops new registrations, and it does not disconnect machines that already registered with it. Removing a machine's access means removing that peer.
Do you still need a separate identity provider?
For a small install, no. The built-in user store handles accounts created from the dashboard, and that is enough for a handful of people.
You want an external identity provider when you already have one and do not want a second list of users. NetBird accepts any provider that speaks OIDC. Register a confidential OIDC client in your provider, then add it in the NetBird dashboard with four values: name, client ID, client secret and issuer. NetBird hands you a redirect URL to paste back into the provider. Named integrations exist for Google, Microsoft Entra ID, Okta, Zitadel, Keycloak, Authentik and Pocket ID, and anything else goes in as generic OIDC. If you already run Authentik as your self-hosted single sign-on, this is the path that keeps one account list instead of two.
Local login stays available after you add a provider, and every configured provider appears on the login page. Keep one local admin account with a strong password. A broken OIDC configuration then still leaves you a way in.
NetBird or Headscale: which control plane should you run?
Both remove the same dependency, the hosted control server your clients would otherwise phone home to. They are not the same shape of project.
Headscale reimplements the Tailscale control server, and you keep using the official Tailscale clients. There is no official web console. You manage users and pre-authentication keys with the headscale command against a config file. Community web interfaces exist, and they are not part of the project. That suits people who want their state in files and their changes in version control.
NetBird ships the whole product: its own client, its own dashboard, an embedded identity provider, and access policies edited in a browser. That is more moving parts on your VPS, and it is far less work to hand to a colleague who is never going to open a terminal.
Run Headscale if you are already invested in Tailscale clients or you want the smallest possible control plane. Run NetBird if several people need to manage peers and you want a console and SSO without assembling one.
How small a VPS can run this?
The documented minimum is 1 CPU and 2 GB of memory. NetBird's own notes put the current floor near 1 GB of RAM now that user management is local, against the 2 GB to 4 GB the older layout needed when a full Zitadel deployment was part of the stack. Buy 2 GB. The extra headroom is what lets an upgrade pull new images while the old ones are still on disk.
Three things are safe to leave out on a small box. Decline the NetBird Proxy service, which exists to publish internal services on public hostnames and has nothing to do with peers connecting. Decline CrowdSec, which is worth adding to an exposed box later rather than on day one. Keep the default SQLite store in the netbird_data volume, and move to PostgreSQL only when you split the deployment across machines or hit real concurrency, which is documented as a migration you can do later.
The relay is the one component you cannot drop. Two peers whose NAT assigns a different port for every destination will never establish a direct tunnel, so the relay is the only path that makes them work at all. Disabling it saves very little memory and breaks connections in a way that is hard to trace.
When one box stops being enough, relays are the first thing to move off it. A standalone relay runs with NB_LISTEN_ADDRESS, NB_EXPOSED_ADDRESS, NB_AUTH_SECRET and NB_ENABLE_STUN. The shared secret must be identical on the relay and on the main server, or clients fail to authenticate to it.
Failure modes, and what you will see
The dashboard shows a certificate warning. Traefik did not obtain a certificate. Run docker compose logs traefik | grep -i acme. There are two causes. Either dig +short netbird.example.com does not return this VPS yet, or TCP 80 is closed somewhere between Let's Encrypt and the container, usually on the provider's network firewall rather than on ufw. Fix the cause before retrying in a loop, because failed validations are rate limited and you will lock yourself out of retries for an hour.
The client says it connected and the dashboard is empty. The client registered with NetBird's hosted service, because --management-url was missing. Run netbird status --detail and read the Management: line, which names the server it is actually talking to. Seeing Management: Connected to https://api.netbird.io:443 means it went to the cloud. Run sudo netbird down, then sudo netbird up --management-url https://netbird.example.com again.
Every peer shows Connection type: Relayed. No direct tunnels are forming, so all traffic crosses your VPS and adds a hop of latency. Check UDP 3478 on the VPS firewall and on the provider firewall, since STUN is what lets a peer learn its own public address and port. netbird status --detail also prints Direct: false and the ICE (interactive connectivity establishment) candidate types for each peer, which shows how far the attempt got. On some networks relayed is the only outcome available and nothing is wrong.
A peer joins and cannot reach anything. Being in the mesh does not mean two peers may talk. Access policies decide that, and a group with no policy attached reaches nothing. Check the policy in the dashboard before you start debugging routes and firewalls.
netbird status reports a daemon problem. The service is not running. Use sudo netbird service status and sudo netbird service start. Client logs are at /var/log/netbird/client.log. For anything you cannot place, netbird debug bundle --anonymize --system-info collects logs, status, routes, DNS settings and firewall state into one archive.
Backups and upgrades
Two things carry the whole install: the directory holding docker-compose.yml and config.yaml, and the Docker volume holding the database and the encryption keys. Back them up together. config.yaml holds the key that encrypts data in the store, so a database copy without it restores to nothing you can read.
docker volume ls
docker compose down
sudo tar czf netbird-config.tgz -C ~ netbird
docker run --rm -v netbird_netbird_data:/data -v "$PWD":/backup \
alpine tar czf /backup/netbird-data.tgz -C /data .
docker compose up -dCompose prefixes volume names with the project directory, so the volume documented as netbird_data usually appears as netbird_netbird_data. Run docker volume ls first and use the name it prints, or the docker run above fails by silently creating an empty volume and archiving nothing. Keep the archives off the VPS. If you already have a backup tool, restic or BorgBackup handles the offsite half.
Upgrading the server is a pull and a recreate:
docker compose pull
docker compose up -d
docker compose psBefore you rely on that, run docker compose config | grep image:. Any tag reading latest should be pinned to a version, for the same reason you pinned the install script: you want to know what is running, and you want a version to go back to when an upgrade misbehaves. Clients upgrade through whichever package manager installed them.
FAQ
Do I need my own identity provider to self-host NetBird?
No. Current releases include a built-in user store, so you create the first admin account in the browser at https://netbird.example.com and add users from the dashboard afterwards. An external OIDC provider is optional and can be added later with four values: name, client ID, client secret and issuer. Guides that tell you to deploy Zitadel or Keycloak before NetBird describe a setup that is no longer required, and following them costs you a whole extra service to run.
Why do all my peers show Connection type: Relayed?
Direct connections are not forming, so traffic goes through the relay on your VPS. The usual cause is UDP 3478 being blocked, which is the STUN port peers use to discover their own public address and port. Open it on the VPS firewall and on your provider's separate network firewall, then run netbird status --detail again and read the Direct: line. On a network whose NAT assigns a different port per destination, relayed is the only possible outcome and nothing is misconfigured.
My client connected but the dashboard shows no peers. What happened?
The client registered with NetBird's hosted service instead of your server, which is what happens when --management-url is left off. netbird status --detail prints the server it is talking to on the Management: line, so a value like https://api.netbird.io:443 confirms it. Run sudo netbird down, then sudo netbird up --management-url https://netbird.example.com, and the peer appears in your dashboard.
How is self-hosted NetBird different from Headscale?
Both replace a hosted control server with one you run. Headscale is a control plane only: you manage it with the headscale command and a config file, there is no official web console, and it drives the official Tailscale clients. NetBird ships its own client, an admin dashboard and identity provider integration in the same stack. Headscale is smaller to run and keeps its state in files. NetBird is easier to hand to people who will not use a terminal.
What size VPS does a self-hosted NetBird server need?
The documented minimum is 1 CPU and 2 GB of memory, and 2 GB is the number to buy. The practical floor dropped to around 1 GB in recent releases because the identity provider is now embedded rather than a separate deployment. Decline the optional proxy and CrowdSec services during install, and stay on the default SQLite store until you genuinely need PostgreSQL.