# 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) ```bash 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): ```bash curl -fsSL https://pkgs.netbird.io/install.sh | sh netbird up --setup-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`: ```yaml 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.