Skip to content

Command Palette

Search for a command to run...

How to Deploy NetBird on a VPS

Updated Jul 2026

verified on Ubuntu 26.04 · Jul 2026

Self-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.

Before you start
  • 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:

  1. 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.
  2. 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.

Next steps

We use analytics cookies (Google Analytics, PostHog) to see which guides are useful. No ad networks, no cross-site tracking. See our privacy policy.