If you use Cloudflare:
Make sure to enable: gRPC Allow gRPC connections to your origin server. (In the network category)
cd /home/myusername/docker
The following command will create a folder called netbird then it will cd into netbird/infrastructure_files
REPO="https://github.com/netbirdio/netbird/"; LATEST_TAG=$(basename $(curl -fs -o/dev/null -w %{redirect_url} ${REPO}releases/latest)); echo $LATEST_TAG; git clone --depth 1 --branch $LATEST_TAG $REPO && cd netbird/infrastructure_files
Name: Netbird
Authentication Flow: default-authentication-flow (Welcome to authentik!)
Authorization Flow: default-provider-authorization-explicit-consent (Authorize Application)
Protocol Settings:
Make sure to save the Client ID because you need to add it later in setup.env
Advanced protocol settings:
Name: Netbird
Slug: netbird
Provider: Netbird
Username: netbird
Create group: disable
Expiring: disable
Make sure to save the username and password because you need to add it later in setup.env
Verify if the endpoint returns a JSON response by calling it from your browser.
https://YOUR_AUTHENTIK_HOST_AND_PORT/application/o/netbird/.well-known/openid-configuration
nano setup.env
Add the following config to setup.env
# Dashboard domain.
NETBIRD_DOMAIN="netbird.DOMAIN.COM"
# TURN server domain. e.g. turn.mydomain.com
# if not specified it will assume NETBIRD_DOMAIN
NETBIRD_TURN_DOMAIN=""
# TURN server public IP address
# required for a connection involving peers in
# the same network as the server and external peers
# If you are confused just use the main server IP where netbird is hosted on
NETBIRD_TURN_EXTERNAL_IP="192.168.1.x"
NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT="https://authentik.DOMAIN.COM/application/o/netbird/.well-known/openid-configuration"
# Replace IMPORT_CLIENT_ID_HERE with the authentik client ID of netbird
NETBIRD_AUTH_AUDIENCE="IMPORT_CLIENT_ID_HERE"
NETBIRD_AUTH_CLIENT_ID="IMPORT_CLIENT_ID_HERE"
NETBIRD_AUTH_DEVICE_AUTH_CLIENT_ID="IMPORT_CLIENT_ID_HERE"
NETBIRD_AUTH_DEVICE_AUTH_AUDIENCE=$NETBIRD_AUTH_AUDIENCE
NETBIRD_AUTH_DEVICE_AUTH_SCOPE="openid"
NETBIRD_AUTH_DEVICE_AUTH_USE_ID_TOKEN=false
NETBIRD_AUTH_DEVICE_AUTH_PROVIDER="none"
NETBIRD_AUTH_PKCE_REDIRECT_URL_PORTS="53000"
# Image tags
# You can force specific tags for each component; will be set to latest if empty
NETBIRD_DASHBOARD_TAG=""
NETBIRD_SIGNAL_TAG=""
NETBIRD_MANAGEMENT_TAG=""
COTURN_TAG=""
NETBIRD_AUTH_SUPPORTED_SCOPES="openid profile email offline_access api"
NETBIRD_USE_AUTH0="false"
NETBIRD_MGMT_IDP="authentik"
NETBIRD_IDP_MGMT_CLIENT_ID=$NETBIRD_AUTH_CLIENT_ID
NETBIRD_IDP_MGMT_CLIENT_SECRET=""
NETBIRD_IDP_MGMT_EXTRA_USERNAME="netbird"
NETBIRD_IDP_MGMT_EXTRA_PASSWORD="IMPORT_SERVICE_ACCOUNT_PASSWORD_HERE"
NETBIRD_DISABLE_LETSENCRYPT=true
NETBIRD_LETSENCRYPT_EMAIL=""
NETBIRD_DISABLE_ANONYMOUS_METRICS=false
NETBIRD_MGMT_DNS_DOMAIN=netbird.selfhosted
NETBIRD_MGMT_API_PORT=443
NETBIRD_SIGNAL_PORT=443
cd /home/myusername/docker/traefik-crowdsec/traefik-data
nano fileConfig.yml
http:
routers:
##########################################################
###======================ROUTERS======================###
### NetBird - router ###
netbird:
entryPoints:
- https
- http
rule: "Host(`netbird.DOMAIN.COM`)"
service: netbird
# NetBird API - router
netbird-api:
rule: "Host(`netbird.DOMAIN.COM`) && PathPrefix(`/api`)"
service: netbird-api
# --- WebSocket Management Router ---
netbird-ws-mgmt:
rule: "Host(`netbird.DOMAIN.COM`) && PathPrefix(`/ws-proxy/management`)"
service: netbird-api
# --- WebSocket Signal Router ---
netbird-ws-signal:
rule: "Host(`netbird.DOMAIN.COM`) && PathPrefix(`/ws-proxy/signal`)"
service: netbird-signal-http
# NetBird Management - router (gRPC)
netbird-management:
rule: "Host(`netbird.DOMAIN.COM`) && PathPrefix(`/management.ManagementService/`)"
service: netbird-management
# NetBird Signal - Router (gRPC)
netbird-signal:
rule: "Host(`netbird.DOMAIN.COM`) && PathPrefix(`/signalexchange.SignalExchange/`)"
service: netbird-signal
# NetBird Relay - Router
netbird-relay:
rule: "Host(`netbird.DOMAIN.COM`) && PathPrefix(`/relay`)"
service: netbird-relay
##########################################################
###======================SERVICES======================###
services:
### NetBird - service ###
netbird:
loadBalancer:
servers:
- url: http://192.168.1.x:9180
# NetBird API - service
netbird-api:
loadBalancer:
servers:
- url: http://192.168.1.x:9184
# NetBird Management - service
netbird-management:
loadBalancer:
servers:
- url: h2c://192.168.1.x:9184
# NetBird Signal - service
netbird-signal:
loadBalancer:
servers:
- url: h2c://192.168.1.x:9182
# NetBird Signal - service (For WebSockets)
netbird-signal-http:
loadBalancer:
servers:
- url: http://192.168.1.x:9182
# NetBird Relay - service
netbird-relay:
loadBalancer:
servers:
- url: http://192.168.1.x:33080
./configure.sh
cd artifacts && docker compose up -d
The following docker-compose.yml file is just a example for using it with a existing reverse proxy you could replace it existing docker-compose.yml with this one in the artifacts folder but make sure you change the DOMAIN. COM to your domain and the PROVIDER_CLIENT_ID_HERE
services:
dashboard:
image: netbirdio/dashboard:latest
container_name: netbird-dashboard
restart: unless-stopped
ports:
- 9180:80
# - 443:443
environment:
# Endpoints
NETBIRD_MGMT_API_ENDPOINT: https://netbird.DOMAIN.COM:443
NETBIRD_MGMT_GRPC_API_ENDPOINT: https://netbird.DOMAIN.COM:443
# OIDC
AUTH_AUDIENCE: PROVIDER_CLIENT_ID_HERE
AUTH_CLIENT_ID: PROVIDER_CLIENT_ID_HERE
AUTH_CLIENT_SECRET:
AUTH_AUTHORITY: https://authentik.DOMAIN.COM/application/o/netbird/
USE_AUTH0: false
AUTH_SUPPORTED_SCOPES: openid profile email offline_access api
AUTH_REDIRECT_URI:
AUTH_SILENT_REDIRECT_URI:
NETBIRD_TOKEN_SOURCE: accessToken
# SSL
NGINX_SSL_PORT: 443
# Letsencrypt
# - LETSENCRYPT_DOMAIN=
# - LETSENCRYPT_EMAIL=
# volumes:
# - ./netbird_container/netbird-letsencrypt:/etc/letsencrypt/
logging:
driver: "json-file"
options:
max-size: "500m"
max-file: "2"
networks:
- proxy
signal:
image: netbirdio/signal:latest
container_name: netbird-signal
restart: unless-stopped
volumes:
- ./netbird_container/netbird-signal:/var/lib/netbird
ports:
- 9182:80
# # port and command for Let's Encrypt validation
# - 443:443
# command: ["--letsencrypt-domain", "", "--log-file", "console"]
logging:
driver: "json-file"
options:
max-size: "500m"
max-file: "2"
networks:
- proxy
management:
image: netbirdio/management:latest
container_name: netbird-management
restart: unless-stopped
depends_on:
- dashboard
volumes:
- ./netbird_container/netbird-mgmt:/var/lib/netbird
# - ./netbird_container/netbird-letsencrypt:/etc/letsencrypt:ro
- ./management.json:/etc/netbird/management.json
ports:
- 9184:443 #API port
# # command for Let's Encrypt validation without dashboard container
# command: ["--letsencrypt-domain", "", "--log-file", "console"]
command: [
"--port", "443",
"--log-file", "console",
"--log-level", "info",
"--disable-anonymous-metrics=false",
"--single-account-mode-domain=netbird.DOMAIN.COM",
"--dns-domain=netbird.selfhosted"
]
logging:
driver: "json-file"
options:
max-size: "500m"
max-file: "2"
networks:
- proxy
coturn:
image: coturn/coturn:latest
container_name: netbird-coturn
restart: unless-stopped
#domainname: netbird.DOMAIN.COM # only needed when TLS is enabled
volumes:
- ./turnserver.conf:/etc/turnserver.conf:ro
# - ./privkey.pem:/etc/coturn/private/privkey.pem:ro
# - ./cert.pem:/etc/coturn/certs/cert.pem:ro
network_mode: host
command:
- -c /etc/turnserver.conf
logging:
driver: "json-file"
options:
max-size: "500m"
max-file: "2"
relay:
image: netbirdio/relay:latest
container_name: netbird-relay
restart: unless-stopped
environment:
NB_LOG_LEVEL: info
NB_LISTEN_ADDRESS: :33080
NB_EXPOSED_ADDRESS: rels://netbird.DOMAIN.COM:443/relay
NB_AUTH_SECRET: XtsT4YH4g99nehxK39X8zcjlay2e1MMPWm6GKxguXJs # generate a new key with: openssl rand -base64 32 | sed 's/=//g'
ports:
- 33080:33080
logging:
driver: "json-file"
options:
max-size: "500m"
max-file: "2"
networks:
- proxy
networks:
proxy:
external: true
"Relay": {
"Addresses": ["rels://netbird.DOMAIN.COM:443/relay"],
"CredentialsTTL": "24h",
"Secret": "XtsT4YH4g99nehxK39X8zcjlay2e1MMPWm6GKxguXJs"
},
cd /home/myusername/docker
mkdir netbird_client && cd "$_"
When netbird-client network is set to MACVLAN (LAN) the server host ip will not be accessable
When netbird-client network is set to default docker network it will have full access
services:
netbird-client:
image: netbirdio/netbird:latest
container_name: netbird-client
hostname: gateway-prod-01
restart: unless-stopped
network_mode: host # Use host networking for Routing Peers for best performance
privileged: true
cap_add:
- NET_ADMIN
- SYS_ADMIN # for better BPF/Kernel support
- SYS_RESOURCE
environment:
NB_SETUP_KEY: IMPORT_SETUP_KEY_HERE
NB_MANAGEMENT_URL: https://netbird.DOMAIN.COM:443
# NB_ADMIN_URL: https://netbird.DOMAIN.COM:443
NB_ALLOW_SERVER_SSH: true # for emergency access to the gateway
volumes:
- ./data:/etc/netbird
Download the client here: https://pkgs.netbird.io/windows/x64
docker compose pull
docker compose up -d --force-recreate
verify netbird version
docker inspect IMAGE_ID | grep version
| Requirements |
|---|
| - Traefik |
| - Technitium |
| - Netbird client |
| - Netbird |
This infrastructure usecase utilizes NetBird operating on a Zero-Trust model.
Unlike traditional VPNs that grant access to the entire network upon connection, this architecture uses NetBird's "Networks" and "Access Policies" to grant least-privilege access.
Traffic routing is handled dynamically via Domain Resources (*.local.domain.com). This means the VPN intercepts traffic based on the URL the user requests, securely queries the internal DNS (Technitium), and tunnels the traffic to the Traefik reverse proxy.
192.168.1.100 = Main linux server with Docker, Technitium, traefik, netbird etc...
192.168.1.123 = Technitium DNS hosted from 192.168.1.100 with a macvlan
For VPN routing to work flawlessly, the Client's physical network (e.g., a hotel Wi-Fi) must not use the same IP subnet as the Office/Server network.
If the Server is on 192.168.1.0/24 and a remote developer connects from a home network that is ALSO 192.168.1.0/24, the operating system will drop the VPN packets because physical Ethernet connections always take priority over VPN tunnels.
✅ The Enterprise Solution (Recommended):
The Server/Office network should be migrated to an obscure subnet (e.g., 10.42.0.0/24 or 192.168.99.0/24). This guarantees that remote users will never experience IP collisions when traveling.
⚠️ The Temporary Workaround (Not Scalable!):
If a user is actively experiencing an overlapping subnet block and the server subnet cannot be changed, you can force their OS to obey the VPN by manually injecting a /32 route on their client machine:
sudo ip route add 192.168.1.100 dev wt0route add 192.168.1.100 mask 255.255.255.255 100.93.x.x (Replace with NetBird IP)The gateway-prod-01 NetBird client runs in Docker network_mode: host. The Technitium DNS runs in a macvlan network on the same host. Linux kernel security blocks a host from directly pinging its own Macvlan IPs.
Use a SystemD "Macvlan Shim" service on the Linux host to bypass this restriction so the VPN gateway can reach the DNS server.
Maintain a strict separation between User Groups and Resource Groups.
people-sysadmins, people-developers, people-users).routing-peers-prod (Contains the gateway-prod-01 peer)res-dns (For the DNS server)res-web-internal (For the Traefik host / Web apps)res-docker-mgmt (For backend Docker subnets)NetBird needs to know how to resolve the internal .local domains without hijacking the public Cloudflare domains.
Technitium Internal192.168.1.123 (Port 53)local.domain.com (Do NOT add domain.com here, or public sites will break).people-sysadmins, people-developers, people-users, AND routing-peers-prod.We use the modern "Networks" tab (not the legacy "Network Routes").
Network 1: Internal Web Apps (Domain Routing)
ARCADEPARTY-Web-Servicesrouting-peers-prod*.local.domain.com (Assign to group: res-web-internal)Network 2: Physical Infrastructure
ARCADEPARTY-Physical-LANrouting-peers-prod192.168.1.123/32 (Assign to group: res-dns)192.168.1.100/32 (Assign to group: res-web-internal / Linux Traefik Host)Network 3: Docker Backend (Sysadmin only)
ARCADEPARTY-Docker-Subnetsrouting-peers-prod10.0.0.0/16 (Assign to group: res-docker-mgmt)These policies map the Users to the Resources to establish Zero-Trust.
people-users, people-developers, people-sysadminsres-dnspeople-users, people-developers, people-sysadminsres-web-internalpeople-sysadminsres-docker-mgmtIf the Linux Host is ever rebuilt, the NetBird Gateway will lose the ability to query Technitium DNS unless this shim is recreated.
sudo nano /etc/systemd/system/macvlan-shim.service
Insert the following contents in macvlan-shim.service
[Unit]
Description=Macvlan Shim for Technitium DNS (NetBird to Docker Macvlan)
After=network.target network-online.target
Wants=network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStartPre=-/sbin/ip link del macvlan-shim
ExecStart=/sbin/ip link add macvlan-shim link REPLACE_ME_WITH_NETWORK_CARD_NAME type macvlan mode bridge
ExecStart=/sbin/ip addr add 192.168.1.250/32 dev macvlan-shim
ExecStart=/sbin/ip link set macvlan-shim up
ExecStart=/sbin/ip route add 192.168.1.123/32 dev macvlan-shim
ExecStop=-/sbin/ip link del macvlan-shim
[Install]
WantedBy=multi-user.target
Activation:
sudo systemctl daemon-reload
sudo systemctl enable --now macvlan-shim.service
Symptom:
The entire Linux host crashes or loses network connectivity. Checking the logs (journalctl or dmesg) reveals critical errors like systemd-networkd-wait-online: Could not create manager: Too many open files.
Root Cause:
Because the NetBird Coturn container runs in network_mode: host, if a listening IP is not explicitly defined, it defaults to listening on 0.0.0.0 (all network interfaces). On a host running many Docker containers, this causes Coturn to open TCP and UDP sockets for every single Docker bridge network (e.g., 172.x.x.x), rapidly exhausting the Linux kernel's file descriptor limit and crashing system services.
The Fix:
Force Coturn to only listen on the host machine's primary local IP rather than 0.0.0.0.
nano /home/myusername/docker/netbird/infrastructure_files/artifacts/turnserver.conf
listening-ip and relay-ip settings, uncomment them (remove the #), and set them to the server's primary LAN IP (e.g., 192.168.1.100 or 192.168.1.x):listening-ip=192.168.1.x
relay-ip=192.168.1.x
(Ensure there are no active lines saying 0.0.0.0)
docker restart netbird-coturn
sudo ss -tuln | grep 3478
Here is the Wiki-style article adapted for the New Combined Container Architecture (v0.65.0+).
Even though NetBird merged everything into two containers (dashboard and netbird-server), the underlying protocol for the Signal and Management streams is still gRPC. Therefore, the exact same rules regarding WAF buffering, Gzip, and Docker timeouts apply!
netbirdio/netbird-server v0.65.0+) behind a Traefik reverse proxy.crowdsec-bouncer@file or gzip@file immediately fixes the connection.rpc error: code = Internal desc = stream terminated by RST_STREAM or 504 Gateway Timeout.The new netbird-server container combines HTTP API, WebSockets, and gRPC onto a single port (usually 80 inside the container). Traefik uses a multiplexer to split this traffic.
The WAF Buffering Problem:
When you enable CrowdSec AppSec (Deep Packet Inspection) or Gzip globally in Traefik, they attempt to "buffer" the request body to scan it for malicious payloads or compress it.
Because gRPC (used by NetBird's Signal and Management services) is a continuous, never-ending HTTP/2 stream, the WAF waits forever for the stream to "finish." This traps the connection, causing the client to hang on "Connecting...".
You cannot apply crowdsec-bouncer or gzip globally to your HTTPS entrypoint. If you do, it will intercept and break the gRPC streams.
Incorrect (traefik.yml):
https:
address: :443
http:
middlewares:
- crowdsec-bouncer@file # ❌ REMOVE THIS
- gzip@file # ❌ REMOVE THIS
(Fix: Remove them from traefik.yml and instead apply them directly to standard websites in your fileConfig.yml, completely omitting them from the NetBird gRPC routers).
Just like the legacy setup, if you route Traefik traffic to your Host IP (e.g., 192.168.1.95), Docker's internal firewall will aggressively kill idle gRPC streams every 60 seconds, causing constant reconnects.
Ensure Traefik and NetBird share a Docker network (e.g., dmz) and use the internal container names.
Correct docker-compose.yml for NetBird:
services:
dashboard:
image: netbirdio/dashboard:latest
container_name: netbird-dashboard
restart: unless-stopped
networks:
- dmz
netbird-server:
image: netbirdio/netbird-server:latest
container_name: netbird-server
restart: unless-stopped
ports:
# We only need to expose STUN to the outside world!
# Traefik will talk to the HTTP/gRPC port entirely internally.
- "3478:3478/udp"
volumes:
- ./data:/var/lib/netbird
- ./config.yaml:/etc/netbird/config.yaml
command: ["--config", "/etc/netbird/config.yaml"]
networks:
- dmz
networks:
dmz:
external: true
Notice how we split the standard HTTP/WebSocket routes from the native gRPC routes. The gRPC router must point to a service using the h2c:// scheme, and neither router should use the CrowdSec middleware.
Correct fileConfig.yml:
http:
routers:
# 1. Dashboard Router
netbird-dashboard:
entryPoints:
- https
rule: "Host(`netbird.example.com`)"
service: netbird-dashboard-svc
middlewares:
- "securityHeadersPrivate"
# 2. HTTP & WebSocket Backend Router (API, Embedded IdP, Relay, WS)
netbird-backend:
entryPoints:
- https
rule: "Host(`netbird.example.com`) && (PathPrefix(`/relay`) || PathPrefix(`/ws-proxy/`) || PathPrefix(`/api`) || PathPrefix(`/oauth2`))"
service: netbird-server-svc
middlewares:
- "securityHeadersPrivate"
# 3. Native gRPC Router (Signal, Management, & ProxyService)
# 🚨 DO NOT ADD CROWDSEC OR GZIP MIDDLEWARES HERE 🚨
netbird-grpc:
entryPoints:
- https
rule: "Host(`netbird.example.com`) && (PathPrefix(`/signalexchange.SignalExchange/`) || PathPrefix(`/management.ManagementService/`) || PathPrefix(`/management.ProxyService/`))"
service: netbird-server-grpc-svc
services:
# Dashboard uses internal Docker port 80
netbird-dashboard-svc:
loadBalancer:
servers:
- url: "http://netbird-dashboard:80"
# Standard HTTP uses internal Docker port 80
netbird-server-svc:
loadBalancer:
servers:
- url: "http://netbird-server:80"
# gRPC uses internal Docker port 80, but REQUIRES the h2c:// scheme!
netbird-server-grpc-svc:
loadBalancer:
servers:
- url: "h2c://netbird-server:80"
If you use Cloudflare for your DNS, the record pointing to NetBird (e.g., netbird.example.com) must be set to "Grey Cloud" (DNS Only). Cloudflare's proxy buffers gRPC streams identically to CrowdSec, and will break the connection.
By bypassing the WAF for gRPC streams and using internal Docker networking to avoid NAT connection pruning, your new unified NetBird server will connect instantly and remain highly stable.