selfhostedops-stack/netbird
2026-08-28 12:26:31 +02:00
..
README.md Episode 5: NetBird mesh - control plane behind Caddy, Forgejo as IdP, services off the public internet 2026-08-28 12:26:31 +02:00

NetBird - the mesh around the stack (episode 5)

Two containers: the dashboard and a combined server that carries the management API, the signal service, a relay, STUN and a built-in identity provider. Everything except one UDP port rides through the same Caddy as the rest of the stack.

Install (on the box, next to the stack)

mkdir -p /opt/netbird && cd /opt/netbird
curl -fsSL https://raw.githubusercontent.com/netbirdio/netbird/main/infrastructure_files/getting-started.sh -o getting-started.sh

NETBIRD_DOMAIN=vpn.example.com \
NETBIRD_REVERSE_PROXY_TYPE=4 \
NETBIRD_EXTERNAL_PROXY_NETWORK=selfhostedops-stack_proxy \
NETBIRD_BIND_LOCALHOST_ONLY=true \
bash getting-started.sh

The script renders docker-compose.yml, config.yaml, dashboard.env and the exact Caddy block to paste (caddyfile-netbird.txt - the same block is already in this repo's Caddyfile). Reload Caddy, then open https://vpn.example.com: a one-time setup page asks for the first admin account. That account lives in the built-in provider and is your break-glass login - store the password in the vault from episode 4.

Forgejo as the network's identity provider

  1. Forgejo -> Site administration -> Applications -> new OAuth2 application. Redirect URI: https://vpn.example.com/oauth2/callback (confidential client).
  2. NetBird dashboard -> Settings -> Identity Providers -> Add Identity Provider. Type: OIDC (Generic). Issuer: your Forgejo base URL. Paste client ID + secret.
  3. The login page now shows "Continue with Forgejo". First-time users land in pending approval until an admin approves them in the Team list.

Machines join

Servers use setup keys (Settings -> Setup Keys -> Create Key):

curl -fsSL https://pkgs.netbird.io/install.sh | sh
netbird up --setup-key <KEY> --management-url https://vpn.example.com:443

People just run netbird up --management-url https://vpn.example.com:443; the CLI prints a device-code URL, and the browser side is the same "Continue with Forgejo" button.

Reaching the containers (and the Docker 28+ trap)

Dashboard -> Networks: add a network, add the compose subnet as a resource (e.g. 172.18.0.0/16), add the box itself as routing peer, add one access policy. Then note the one line already present on the proxy network in compose.yml:

networks:
  proxy:
    driver_opts:
      com.docker.network.bridge.trusted_host_interfaces: "wt0"

Docker 28+ drops any packet to a container IP that does not arrive on Docker's own bridge (a raw-table rule, so DOCKER-USER does not help). The option above makes Docker write the accept rules itself. Changing it needs a network recreate (docker compose down && docker compose up -d).

After that, delete a service's public block from the Caddyfile, reload Caddy, and the service is reachable over the mesh only.