Workload · Running your own mesh VPN

Self-hosted NetBird: your own mesh VPN control server, set up safely

To run NetBird on your own server, you need a Linux machine with at least one CPU, 2 GB RAM, a domain name that points at it, and ports 80 and 443 over TCP plus 3478 over UDP open to the internet; the official install script then starts everything in Docker. Your devices still talk through their own WireGuard tunnels; the server introduces them, decides who may reach what, and relays traffic when two cannot connect directly.

The short version
  1. Small to start: one CPU and 2 GB RAM is the documented floor
  2. Three ports: 80 and 443 over TCP, 3478 over UDP
  3. Make the owner account yourself, from the server, seconds after NetBird starts
  4. One archive to keep: settings plus the data folder with its encryption key

What you are actually hosting

NetBird has two halves. Each device runs the NetBird agent, which sets up WireGuard, the encrypted tunnel. Your server runs the coordination side: a management service that keeps the list of devices, their addresses and your access rules; a signal service that lets two devices swap connection details; a STUN service that helps each device learn how it looks from the internet; and a relay for any pair that cannot connect directly, such as a phone behind a mobile carrier. That is the split the project README describes.

With the current install script, all four services live in one container, netbird-server, configured by one file, config.yaml. Beside it run the web dashboard and Traefik, which gets the HTTPS certificate from Let's Encrypt and routes requests.

The licence, as we read it (this is not legal advice). The netbirdio/netbird LICENSE file makes the repository BSD-3-Clause, apart from the directories named management, signal, relay and combined, which are AGPL-3.0; the separate dashboard repository is AGPL-3.0 too. BSD parts such as the client only ask that the copyright line and licence text travel with copies. For the AGPL parts, handing out copies, changed or not, means handing out their source, and a changed version offered to users across a network must give them its source too.

Will a small server do?

Yes, to begin with, if it is x86: the X-Gene cartridge, an ARM machine, comes only with Ubuntu 16.04 or 18.04, so these Ubuntu 24.04 steps will not run there. The quickstart sets the floor at one CPU and 2 GB RAM and gives no figures per device. What limits you later is the database: NetBird keeps its data in SQLite by default, which its configuration reference rates as suited to testing and small setups, since it cannot handle simultaneous writes or several management servers. For production it recommends PostgreSQL.

When two devices reach each other directly, their tunnel traffic skips your server entirely (they only keep in touch with it for management and signalling), but every pair that needs the relay sends all of its traffic through it. That is where a whole dedicated server with an unmetered port helps: there is no transfer allowance to run over, so a busy month of relayed traffic does not turn into a surprise on the invoice.

Firewall mistakes happen, so a console that works when SSH does not (screen and power button, no waiting for us) is worth having. Rack servers and blades have one, as do two cartridges, the m510 and the m710x; the other cartridges offer power control alone. IPv6 is free if you want devices to reach the server over it. A ready server is running within 30 minutes of approval, while a custom build takes hours, not minutes. You manage the server; we keep the hardware and the network running.

The ports to open, and why

  • TCP 80: the quickstart wants it open; here it simply sends plain HTTP visitors on to HTTPS. The certificate check itself happens on 443, because the generated Traefik uses Let's Encrypt's TLS challenge.
  • TCP 443: everything web-shaped goes through Traefik on this one port: the dashboard, the management API, the signal and management gRPC streams, and the relay over WebSocket.
  • UDP 3478: STUN, which devices use to work out their own public address. It is plain UDP, so no web proxy can carry it; it has to be open on the server itself.
  • UDP 51820: only needed if you switch on the optional NetBird Proxy service, which this guide leaves off.
  • The pre-v0.29 ports (TCP 33073, 10000 and 33080, UDP 49152-65535): this install does not listen on them, so opening them achieves nothing. Update any device still on a client older than v0.29 instead.

A Docker detail: a published port listens on IPv4 0.0.0.0 and IPv6 [::]. IPv4 traffic to it never meets ufw's rules; IPv6 traffic skips them too if IPv6 is turned on in Docker's own network settings, and otherwise passes docker-proxy, which ufw does filter. Opening the three ports in ufw covers both. NetBird's metrics and health ports stay inside the container, because the generated compose file does not publish them.

From a blank server to your first login

These steps follow the official quickstart on Ubuntu 24.04 with its built-in Traefik. Three things are our own change, and we say so: Docker comes from Ubuntu's packages, you fetch the script and look through it rather than piping it straight into bash, and the owner account is made from the server through NetBird's setup API rather than on the public setup page. Work as root (sudo -i).

  1. At your DNS provider, give the server a name such as vpn.example.org with an A record. Do this first: the script only prints a warning when a name doesn't resolve, and the certificate then fails.
  2. Paste the first block. It turns on the firewall (IPv6 rules first, then SSH, then NetBird's three ports), installs Docker with its compose plugin plus jq, curl and openssl under their Ubuntu 24.04 package names, and downloads the install script. On another system, install Docker the way Docker's documentation describes.
  3. Open /opt/netbird/getting-started.sh and skim what it will do.
  4. Paste the second block. Its variables answer every question the script would ask, which the quickstart supports for unattended installs. The script returns once your server answers through Traefik; the block then waits for a valid certificate and makes you the owner with a long random password, printed once. Copy it somewhere safe straight away. If any step fails, the block stops NetBird so the setup page does not sit open.
# Run as root on Ubuntu 24.04. Safe to paste again if a step fails.
sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw && ufw allow OpenSSH && ufw --force enable \
 && ufw allow 80,443/tcp && ufw allow 3478/udp \
 && apt-get update && apt-get install -y docker.io docker-compose-v2 jq curl openssl \
 && docker compose version \
 && install -d -m 700 /opt/netbird \
 && curl -fsSLo /opt/netbird/getting-started.sh https://github.com/netbirdio/netbird/releases/latest/download/getting-started.sh \
 && echo "ok - read /opt/netbird/getting-started.sh, then paste the next block" \
 || echo "STOP - one of the steps above did not finish; fix it and paste again"
# Run as root. Put your own name and e-mail in the first line.
H=vpn.example.org; M=admin@example.org; DC="docker compose -f /opt/netbird/docker-compose.yml"
cd /opt/netbird && if [ "$H" = vpn.example.org ] || [ "$M" = admin@example.org ] || ! getent ahostsv4 "$H" >/dev/null || [ -e config.yaml ]; then
  echo "STOP - edit H and M, add the DNS record; /opt/netbird must not hold an install yet"
elif NETBIRD_DOMAIN="$H" NETBIRD_LETSENCRYPT_EMAIL="$M" NETBIRD_NON_INTERACTIVE=true bash getting-started.sh; then
  i=0; until curl -fsS -o /dev/null --resolve "$H:443:127.0.0.1" "https://$H/oauth2/.well-known/openid-configuration" || [ $i -ge 30 ]; do i=$((i+1)); sleep 10; done
  rc=; install -m 600 /dev/null /root/nb-setup.json && install -m 600 /dev/null /root/nb-setup.out && s=$(openssl rand -base64 24) && [ -n "$s" ] \
  && PW="$s" jq -n --arg e "$M" '{email:$e, name:"Owner", password:env.PW}' > /root/nb-setup.json \
  && rc=$(curl -sS -o /root/nb-setup.out -w '%{http_code}' --resolve "$H:443:127.0.0.1" -H 'Content-Type: application/json' -d @/root/nb-setup.json "https://$H/api/setup")
  if grep -q user_id /root/nb-setup.out; then echo "ok - you are the owner ($M). Password, shown once: $s"
  else $DC stop; echo "STOP - no owner account (HTTP ${rc:-none}); NetBird is stopped so nobody else can claim it"; fi
  rm -f /root/nb-setup.json /root/nb-setup.out; unset s
else
  [ -e docker-compose.yml ] && $DC stop; echo "STOP - the install script ended early; NetBird is stopped"
fi

Why both steps share one paste: NetBird starts with no users, and until the first account exists its setup endpoint asks for no login, so whoever reaches it first becomes the owner. Pasting them together keeps that gap short, though not zero. Once an account exists, setup requests get HTTP 412; if your STOP line shows 412, someone beat you to it. If the STOP line says no owner account, or that the install script ended early, NetBird is now stopped and nothing is lost yet: cd /opt/netbird && docker compose down --volumes, remove the three generated files (dashboard.env, config.yaml, docker-compose.yml), and paste the second block again. If it says /opt/netbird already holds an install, leave it alone: that is a working NetBird, and the block has not touched it. Then log in at your domain with your e-mail and that password.

Adding your team and their devices

No separate login system is needed: NetBird brings its own (an embedded Dex server), and you add people in the dashboard. If your team already signs in with Google, Microsoft, Okta, Keycloak or authentik, connect it under Settings, then Identity Providers, and copy the Redirect URL from that form into the provider, set up as a confidential client. Running your own sign-in server too? See our guides for authentik and Keycloak.

People join by running netbird up and signing in through the browser that opens. For servers, make a setup key under Settings, then Setup Keys; it can be one-off or reusable, and the form suggests an expiry 7 days out. With no key given, netbird up opens that browser sign-in, which needs a person to follow the link and log in; a setup key spares servers that. Hand the key over in a file with the --setup-key-file flag of the client (defined in client/cmd/root.go), typed in without echo so it never lands in your command history or the process list: install -m 600 /dev/null ~/setup-key && read -rsp "Paste the setup key: " key && echo "$key" > ~/setup-key && netbird up --setup-key-file ~/setup-key --management-url https://vpn.example.org; unset key; shred -u ~/setup-key. Using your own domain over https there is our adaptation: the docs show a http://localhost:33073 address. netbird status shows whether it worked.

Keeping a copy you can restore

NetBird's backup guide puts it plainly: lose the data folder and every account, device, rule and setup key goes with it. Keep the three settings files in /opt/netbird and /var/lib/netbird from the server container, where the database and its encryption keys live; Traefik fetches new certificates by itself. The block below pauses NetBird for a consistent copy, saves a dated archive only root can read, checks it, starts NetBird again whatever happens, and never deletes an older backup. Then copy the archive to another machine, since it holds every secret of your install.

# Run as root. NetBird pauses for the copy and comes back even if a step goes wrong.
( set -o pipefail; umask 077
  DC="docker compose -f /opt/netbird/docker-compose.yml"; OUT=/root/netbird-backups
  STAMP=$(date +%F-%H%M%S); TMP="$OUT/tmp-$STAMP"; ARC="$OUT/netbird-$STAMP.tar.gz"
  trap '$DC start >/dev/null 2>&1' EXIT
  mkdir -p "$TMP" && cp /opt/netbird/docker-compose.yml /opt/netbird/config.yaml /opt/netbird/dashboard.env "$TMP"/ \
  && $DC stop && $DC cp -a netbird-server:/var/lib/netbird/ "$TMP"/ && $DC start \
  && [ -n "$(ls -A "$TMP/netbird" 2>/dev/null)" ] \
  && tar -czf "$ARC" -C "$TMP" . && tar -tzf "$ARC" > "$TMP.txt" && grep -c '^\./netbird/.' "$TMP.txt" >/dev/null \
  && rm -rf "$TMP" "$TMP.txt" && echo "ok - saved $ARC; now copy it somewhere else" \
  || echo "STOP - no complete backup; check $TMP and run: $DC ps" )

Restore onto a fresh server under the same domain, and your devices find it without joining again. Back up just before the move, then add nothing to the old server (the copy would miss it), and keep it running until the new one checks out: it is your way back.

  1. On the new server, paste the first setup block (it only prepares the machine; skip the second), then copy the archive into /root.
  2. Make sure the archive is whole, and go no further unless you see ok. The listing goes into a file so a damaged archive makes tar itself fail: tar -tzf ~/netbird-STAMP.tar.gz > ~/netbird-check.txt && grep -q '^\./netbird/.' ~/netbird-check.txt && echo ok, using your archive's real name.
  3. Unpack it, never on top of an existing install: cd /opt/netbird && [ ! -e config.yaml ] && mkdir -m 700 restore && tar -xzf ~/netbird-STAMP.tar.gz -C restore && cp -p restore/docker-compose.yml restore/config.yaml restore/dashboard.env . && echo ok
  4. Put the data back and start it, still inside /opt/netbird: docker compose create netbird-server && docker compose cp -a restore/netbird/. netbird-server:/var/lib/netbird/ && echo copied && docker compose up -d
  5. Move the DNS record over to the new address, then, in /opt/netbird, run docker compose restart traefik so it asks for a certificate now that the name leads here. Check that your devices, rules and setup keys all show in the dashboard, and only then remove /opt/netbird/restore and ~/netbird-check.txt: the folder keeps your secrets in plain files.

Updating without surprises

Make a fresh backup, read the GitHub release notes of NetBird and its dashboard, and note exactly which images you run now with docker image ls --digests | grep netbirdio. Then update: cd /opt/netbird && docker compose pull dashboard netbird-server && docker compose up --detach --force-recreate dashboard netbird-server. Both images carry the latest tag, so this fetches whatever is newest.

Traefik is updated separately, and the upgrade guide warns that running the install script again upgrades nothing. If an update goes badly, an older tag will not reverse a database migration: restore the backup you took first, and pin each image: line to the digest you noted, such as image: netbirdio/netbird-server@sha256:..., because latest would pull the new release straight back.

Is NetBird the right tool for you?

One server with SQLite means one management instance; if NetBird must survive a server failure, plan for PostgreSQL and the project's scaling guide, which spreads the deployment over several nodes. Already using the Tailscale apps on your devices and only want your own control server for them? Read our Headscale guide. And if you need one VPN endpoint without any control plane, a WireGuard VPN server on its own is simpler.

Questions people ask

Is it fine to open only ports 80, 443 and 3478?
For this install, yes. TCP 443 carries the dashboard, the API, signalling and the relay, TCP 80 redirects to HTTPS, and UDP 3478 is STUN. Add UDP 51820 only for the optional NetBird Proxy service.
Do I have to set up Keycloak, authentik or another login provider?
No. NetBird includes its own login system, so you can create users straight from the dashboard. You can still add Google, Microsoft, Okta or any OpenID Connect provider later, next to the local accounts.
Will my devices send all their traffic through the server?
Usually not. Devices first try to connect to each other directly over WireGuard. Only when that fails, for example behind strict mobile networks, does the relay on your server carry the traffic.

Get a server for your own NetBird network

A whole machine you control, with root, an unmetered port for relayed traffic and no contract.

Get a server for NetBird

Read next