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