Mesh VPN
Self-hosted Netbird with DynIP
Netbird is an open-source, WireGuard-based mesh VPN that you can self-host on a single Docker host. The standard install drops a docker-compose stack with Netbird's signal/management/relay services and a bundled Traefik that obtains a Let's Encrypt certificate for the dashboard hostname. The catch: that hostname has to keep resolving to your server's public IP. On a residential ISP, a CGNATed cellular link, or any home connection where the IPv4 rotates, you need DDNS — this guide shows how to drop the dynip-updater container into Netbird's compose file so the dashboard stays reachable without manual intervention.
What you'll need
- A Linux host (anything Docker-supported — a small VPS, a homelab box, a Raspberry Pi 4 or newer) with Docker Engine and the Compose v2 plugin installed.
- A DynIP zone, e.g.
netbird.ddns.dynip.devor a name under your own BYOD namespace. Free tier is enough. - An email address Let's Encrypt can contact for cert expiry notices — you'll enter this during the installer.
- Public reachability on TCP 80 (HTTP-01 cert issuance), TCP 443 (the dashboard), and UDP 3478 (Netbird's TURN relay for clients that can't establish a direct WireGuard tunnel). Port 80 only carries the ACME challenge and an HTTPS redirect; if your ISP blocks it, jump to the DNS-01 alternative below.
Setup
1. Register the DynIP zone
In the DynIP dashboard, create a zone for the Netbird hostname — netbird.ddns.dynip.dev is a typical choice. Copy the generated TSIG-style key from the row; you'll paste it into the updater container's environment in a moment. Leave the dashboard open in a tab.
2. Run the Netbird getting-started script
On the host, in an empty directory you're happy to keep as Netbird's working root (e.g. ~/netbird):
$ mkdir ~/netbird && cd ~/netbird $ curl -fsSL https://github.com/netbirdio/netbird/releases/latest/download/getting-started.sh | bash
The script asks a short series of questions:
- Domain — enter the DynIP hostname you registered (e.g.
netbird.ddns.dynip.dev). This becomes the dashboard URL and the SAN on the Let's Encrypt cert. - Email — your Let's Encrypt contact address.
- Reverse proxy — press Enter for the default, Traefik with automatic TLS. Netbird's installer bundles a Traefik service that handles HTTP-01 issuance and renewal with no extra config.
- Netbird Proxy service — safe to leave disabled (
N) unless you already know you need it.
When the script finishes you'll have a docker-compose.yml, a config.yaml, a dashboard.env, and (if you enabled the proxy) a proxy.env in the working directory. The stack is started by the script as part of its run, but the dashboard won't come up cleanly until the hostname resolves to the host — which is what the updater container is for.
3. Add the dynip-updater container to the compose file
Open the generated docker-compose.yml and append a new service block alongside Netbird's existing services. Anywhere under the top-level services: key works — the updater is independent and doesn't need to start in any particular order:
services:
# ... Netbird's existing services (signal, management, relay, dashboard, traefik) ...
dynip-updater:
image: ghcr.io/33k-org/dynip-updater:latest
container_name: dynip-updater
restart: unless-stopped
environment:
- DYNIP_DOMAIN=netbird.ddns.dynip.dev
- DYNIP_KEY=YOUR_44_CHAR_BASE64_TSIG_SECRET
# Netbird's services don't depend on the updater —
# this can start in any order. No volumes, no ports.
Replace the two environment values with what you copied in step 1. The image is multi-arch (amd64 and arm64) so the same compose works on a Pi as on a VPS. By default the updater checks every 5 minutes; the full env reference is in the Docker container section of the docs.
4. Restart the stack
$ docker compose up -d $ docker compose logs -f dynip-updater # Expected on first run: # INFO DynIP updater v1.1.0 starting for domain=netbird.ddns.dynip.dev ... # INFO Updated A record for netbird.ddns.dynip.dev to 203.0.113.42 (changed)
Once that first Updated A record line appears, Traefik's ACME client can complete its HTTP-01 challenge. Within another 10–60 seconds the dashboard cert is issued and https://netbird.ddns.dynip.dev is live.
Verification
$ dig +short netbird.ddns.dynip.dev 203.0.113.42 $ curl -I https://netbird.ddns.dynip.dev HTTP/2 200 $ docker compose logs --tail=5 dynip-updater # On steady-state cycles, no INFO lines — only DEBUG. # A republish only fires when something actually changes.
Open https://netbird.ddns.dynip.dev in a browser. You'll land on Netbird's first-run onboarding page where you create the initial admin account, then on the dashboard itself. From there, follow Netbird's own docs to invite peers, define groups, and configure access policies — none of that involves DynIP further.
TLS and certificate handling
Out of the box, Netbird's bundled Traefik obtains and renews a Let's Encrypt cert via the HTTP-01 challenge. Let's Encrypt connects to http://netbird.ddns.dynip.dev/.well-known/acme-challenge/... over port 80, Traefik serves the response, and the cert is issued. Renewal runs automatically about 30 days before expiry. As long as port 80 is reachable from the public internet and DNS resolves to the host, you don't need to touch anything.
Optional — DNS-01 via DynIP TSIG (behind CGNAT or port-80 blocks)
If your ISP blocks inbound port 80, or you're behind CGNAT on IPv4 and only have a public IPv6 address, HTTP-01 won't work. The fix is the DNS-01 challenge: Traefik writes a TSIG-authenticated DNS UPDATE to DynIP, Let's Encrypt validates against DynIP's authoritative nameservers, and no inbound HTTP is needed.
Because Netbird's bundled Traefik runs in the same docker-compose stack you just edited, the change is roughly: add the four RFC2136_* environment variables to Traefik's service, switch the cert resolver from httpchallenge to dnschallenge, and point the DNS provider at rfc2136. The full set of flags is documented in the Traefik + DynIP guide — the only Netbird-specific detail is that you're editing the installer's generated Traefik service rather than authoring one from scratch.
Common issues
Dashboard returns "Bad Gateway" or a Traefik default page
Cert issuance failed and Traefik is serving its built-in fallback. Check docker compose logs traefik — the most common cause is port 80 not reachable from the public internet. Either open it on the router, or switch to the DNS-01 flow above.
Updater logs "Authentication rejected (HTTP 403)"
The TSIG key doesn't match the zone. Re-copy the key from the dashboard; remember each zone has its own key. The container exits with code 3 on auth failures rather than retrying, so a restart loop here is a clear signal.
IPv6 endpoint returned 4xx — no IPv6 connectivity from this host
Expected on hosts without working IPv6. The container logs a warning per cycle and continues publishing the A record. To silence it, set DYNIP_ENABLE_V6=false in the service's environment.
Peers can connect but traffic doesn't flow
That's almost always UDP 3478 blocked. Netbird tries to establish a direct WireGuard tunnel between peers first; when NAT traversal fails it falls back to the TURN relay on 3478. Open the port on the firewall or accept that some peer pairs will only work when one end is on a more open network.
Related guides
- Traefik + DynIP — the full DNS-01 configuration the section above points at, plus a worked example of a multi-service stack.
- Caddy + DynIP — alternative if you want to swap Netbird's bundled Traefik for Caddy as the public TLS terminator.
- Tailscale + DynIP — the closest commercial alternative to Netbird. Tailscale doesn't need self-hosting; the trade-off is your control plane lives on someone else's servers.