How to Deploy NetBird on a VPS
Updated Jul 2026
verified on Ubuntu 26.04 · Jul 2026Self-host NetBird on your own VPS — a WireGuard-based mesh VPN with SSO and a bundled identity provider, giving you a self-hosted Tailscale/Twingate alternative you fully control.
- A VPS with 2 vCPU / 4 GB RAM — the bundled management, signal, relay, dashboard, Zitadel IdP, and Postgres stack has a real ~2 GB floor, so give it headroom
- A fresh Ubuntu 26.04 server with root/sudo SSH access, Docker Engine + the Compose plugin installed (see Docker & Compose on Ubuntu)
- A domain — or a sslip.io test hostname — that resolves to the server's public IP; the installer's bundled Caddy needs it for automatic HTTPS
What NetBird is
NetBird is a self-hosted, WireGuard-based mesh VPN — a self-hosted alternative to Tailscale or Twingate. Install its client on your laptops, phones, and servers, and NetBird wires them together into one flat private network: every device gets a WireGuard peer connection to every other device it's allowed to reach, NAT and firewalls notwithstanding, with SSO login and group-based access policies controlling who can reach what. There's no central traffic bottleneck the way a traditional hub-and-spoke VPN has — peers connect directly to each other whenever a path exists, and fall back to a relay only when they can't.
NetBird's licensing is split, and it's worth knowing before you deploy: the client is permissive BSD-3-Clause, but the self-hosted server components you're standing up in this guide — management, signal, and relay — are AGPL-3.0. Running an unmodified instance for your own team doesn't obligate anything extra, but if you modify and redistribute the server as a network service, AGPL's source-disclosure terms apply.
The other thing that sets NetBird apart from a bare WireGuard setup: it ships with a bundled Zitadel identity provider. Zitadel is a full OIDC/SSO provider that the installer stands up alongside NetBird itself, so you get working SSO login and access policies out of the box — no external Okta, Google Workspace, or Authentik instance required to get started (you can still point NetBird at an external IdP later if you'd rather centralize auth elsewhere).
Server sizing — why this one needs more than a toy box
Unlike a lot of the single-container apps in these guides, NetBird's self-hosted stack is genuinely several services running together: management, signal, relay (the TURN/coturn fallback for peers that can't connect directly), the dashboard, the bundled Zitadel identity provider, and Zitadel's own Postgres database, all fronted by a Caddy reverse proxy the installer sets up for you. NetBird's official minimum is 1 vCPU and 2 GB RAM, and that's a real floor here, not a conservative buffer — Zitadel alone is not a lightweight process.
2 vCPU / 4 GB RAM is the comfortable starting point. A Hetzner CX22 fits well and leaves headroom for Zitadel's Postgres and the relay under load; Kamatera is the pick if you'd rather dial RAM precisely, and DigitalOcean's equivalent droplet works fine too. Don't try to squeeze this stack onto a 1 GB box — you'll spend more time fighting OOM kills than you save on the smaller instance.
Prepare the server
This guide assumes you've already worked through
Docker & Compose on Ubuntu — a fresh
Ubuntu 26.04 box with Docker Engine and the Compose plugin installed, and
ufw allowing SSH plus 80 and 443. NetBird needs two more things on top of
that base: the jq command-line tool, which the installer script uses to
generate and read its config, and two more firewall rules for the TURN
relay — 3478/udp for STUN/TURN signaling and the 49152:65535/udp range
NetBird's coturn relay uses for actual media traffic once two peers can't
reach each other directly.
mkdir -p ~/netbird && cd ~/netbird
sudo apt update && sudo apt install -y jq
sudo ufw allow 3478/udp
sudo ufw allow 49152:65535/udp
sudo ufw reload
Install
NetBird publishes a getting-started script that reads your target domain
from an environment variable and does the rest: it writes a
docker-compose.yml plus all the supporting config (Caddyfile, Zitadel env,
dashboard env, coturn config, management config) into the current directory,
generates the secrets each service needs, and brings the whole stack up with
Docker Compose. Run bare, it asks a few interactive questions (which reverse
proxy, a Let's Encrypt email); set a handful of env vars first and it runs
completely unattended — which is the path this guide uses so the install is
reproducible:
NETBIRD_DOMAIN— the public hostname, which skips the domain prompt.NETBIRD_LETSENCRYPT_EMAIL— the ACME email, which skips the email prompt.NETBIRD_AGENT_NETWORK=true— selects NetBird's turnkey preset (built-in Traefik with automatic TLS), which bypasses the reverse-proxy and add-on prompts. Leave it off if you'd rather answer the prompts by hand.
For this guide, use a sslip.io hostname that resolves straight to the box's own public IP, so you get a real, publicly-resolvable domain without touching DNS at all:
export SERVER_IP=$(curl -s https://ifconfig.me)
export NETBIRD_DOMAIN="netbird.${SERVER_IP}.sslip.io"
export NETBIRD_LETSENCRYPT_EMAIL="admin@${NETBIRD_DOMAIN}"
export NETBIRD_AGENT_NETWORK=true
echo "Installing NetBird on: $NETBIRD_DOMAIN"
curl -fsSL https://github.com/netbirdio/netbird/releases/latest/download/getting-started.sh | bash
(For production use, replace the sslip.io hostname with a real domain —
point an A record at the server's public IP first, the same DNS-before-
Caddy rule as any Let's Encrypt setup, then export that domain instead.)
The script takes a few minutes: pulling images, generating the Zitadel database and its initial admin account, and starting management, signal, relay, dashboard, Zitadel, and Caddy in order. Watch its output — it prints the dashboard URL and the initial Zitadel admin login (email and a generated password) once, in plaintext, and that's the only place you'll see that password. Copy it somewhere safe before you move on.
Confirm every service actually came up healthy:
docker compose ps
docker compose logs --tail 40
You should see containers for management, signal, relay (or its coturn
component), dashboard, Zitadel, its database, and Caddy, all Up.
HTTPS + domain
The installer's Caddy container handles TLS for you automatically — unlike
some of the other guides on this site, you don't hand-write a Caddyfile
here; the getting-started script generated one already, pointed at whatever
domain you exported as NETBIRD_DOMAIN. Caddy still needs that domain to
actually resolve before it can complete the ACME HTTP-01 challenge and issue
a certificate, which is exactly what the sslip.io hostname (or a real A
record) gives it.
Give Caddy a little time to issue the first certificate, then confirm it actually landed:
for i in $(seq 1 12); do
if curl -sk --max-time 5 "https://${NETBIRD_DOMAIN}/" | grep -qi "html"; then
echo "Dashboard is responding over HTTPS"
break
fi
echo "Waiting for Caddy to issue the certificate... ($i/12)"
sleep 5
done
curl -v "https://${NETBIRD_DOMAIN}/" 2>&1 | grep -E "subject:|issuer:|SSL certificate|HTTP/(1.1|2)"
An issuer line naming Let's Encrypt means the real certificate issued. If
it instead names a local/internal CA, the ACME challenge never completed —
almost always because the domain didn't resolve yet, or (on most cloud
providers) because a network-level firewall panel is blocking inbound 80/443
even though ufw allows them on the box itself.
First-run setup
Load https://<your-domain> in a browser. NetBird's dashboard redirects you
to Zitadel to sign in — use the admin email and generated password the
installer printed earlier. Change that password immediately once you're
in; it was shown once, in plaintext, in a terminal log.
From the dashboard:
- Open Setup Keys and generate a new reusable or one-off setup key — this is what lets a device join your network without an interactive SSO login each time.
- Open Access Control and confirm the default policy matches what you want (NetBird's default allows all peers to reach each other; tighten this with groups before you add anything sensitive).
Then, on an actual client device — your laptop, phone, or a second server, not the VPS you just built — install the NetBird client and join:
curl -fsSL https://pkgs.netbird.io/install.sh | sh
sudo netbird up --management-url "https://<your-domain>:443" --setup-key "<setup-key-from-dashboard>"
A device that connects and shows Connected in the dashboard's Peers list confirms the whole chain — public HTTPS, Zitadel SSO, management, signal, and either a direct WireGuard path or the relay fallback — is working end to end.
Backups
Three things live in ~/netbird, and all three matter: the generated
config files (docker-compose.yml, the Caddyfile, and the *.env files
holding secrets), the Zitadel Postgres database (every user, policy, and
peer registration), and Caddy's certificate data. Stop the stack briefly,
snapshot the config directory and every Docker volume Compose created for
it, then bring it back up:
mkdir -p ~/netbird-backups
cd ~/netbird
docker compose stop
tar czf ~/netbird-backups/netbird-config-$(date +%F).tar.gz .
PROJECT=$(basename "$PWD")
for vol in $(docker volume ls -q --filter "label=com.docker.compose.project=${PROJECT}"); do
docker run --rm -v "${vol}:/data:ro" -v ~/netbird-backups:/backup alpine \
tar czf "/backup/${vol}-$(date +%F).tar.gz" -C /data .
done
docker compose start
Copy ~/netbird-backups off the box — S3-compatible storage or another
machine — on a schedule, and periodically restore into a throwaway stack to
confirm the backup is actually usable. Losing the Zitadel database without a
backup means every peer has to re-enroll and every user has to be
re-invited; it's recoverable, just painful.
Upgrades
cd ~/netbird
docker compose pull
docker compose up -d
This pulls newer images for whatever the compose file pins and recreates
only the containers that changed. Don't re-run the getting-started.sh
installer to upgrade — it's meant for a fresh install and can overwrite
the config and secrets you're already running on. Back up first (above),
then read the release notes on the NetBird GitHub releases
page before a major version
bump — this stack has more moving parts than most, and a breaking change in
Zitadel or the management API is worth knowing about ahead of time.
Troubleshooting
Dashboard shows a certificate warning, or curl reports the fallback
local CA. Caddy couldn't complete the ACME challenge — almost always
because the domain doesn't resolve yet, or because a cloud provider's
network-level firewall (Hetzner Cloud Firewall, DigitalOcean Cloud
Firewalls, AWS Security Groups) is blocking 80/443 at the edge even though
ufw allows them locally. Confirm dig +short <your-domain> returns the
server's IP, check the provider's firewall panel, then hit the domain again.
Peers show as connected but no traffic flows, or connections are slow.
Usually the pair fell back to the relay and the relay's UDP range is
blocked. Confirm 49152:65535/udp and 3478/udp are open in both ufw and
any cloud-level firewall — the same edge-firewall gotcha as the HTTPS case
above.
Lost the initial Zitadel admin password. It was only ever shown once in the installer's terminal output. If you didn't save it and didn't create a second admin account first, you'll need Zitadel's own password-reset flow (from its admin console) rather than anything NetBird-specific — Zitadel owns identity here, not NetBird.
A container won't start after docker compose up -d. Check
docker compose logs <service> for that container specifically — Zitadel
in particular needs its Postgres dependency healthy before it starts, so an
early boot failure there is usually a race the compose file's health checks
should already handle; a docker compose up -d retry after Postgres
settles is usually enough.
Verification + next steps
You're done when the dashboard loads over HTTPS on your domain with a real
Let's Encrypt certificate, you've logged in via Zitadel and rotated the
initial admin password, docker compose ps shows every service Up, and at
least one real client device shows Connected in the Peers list after
joining with a setup key.
From here, invite teammates through Zitadel, group your peers and write access policies before you connect anything sensitive, and add more devices with setup keys as you go. A Hetzner CX22 (2 vCPU / 4 GB) is a solid value pick for this stack; reach for Kamatera if you want to dial RAM precisely as your peer count grows. See Best VPS for self-hosting for the ranked picks.