I’ve been working on a self-hosted NetBird docker deployment (signal + relay + management with Zitadel self-hosted docker deployed separately on the same server) and finally have everything working reliably except the Relay server when running behind an Nginx reverse proxy with TLS. Server is Ubuntu 22.04
Peers can connect successfully to the Signal service and management API, but relay connections keep failing with errors like:
I spent a few days alternately trying to terminate TLS at the nginx proxy and then terminating at the containers, but each time I'd end up breaking the working setup with no progress towards solving the relay issue.
upstream dashboard {
server 127.0.0.1:8081;
keepalive 10;
}
upstream management {
server 127.0.0.1:9444;
keepalive 10;
}
upstream signal {
server 127.0.0.1:10000;
keepalive 10;
}
upstream relay {
server 127.0.0.1:33080;
}
server {
listen 80;
server_name netbird.example.com;
# 301 redirect to HTTPS
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl http2;
server_name netbird.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
client_header_timeout 1d;
client_body_timeout 1d;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Scheme $scheme;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Host $host;
grpc_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Proxy dashboard
location / {
proxy_pass http://dashboard;
}
# Proxy Signal
location /signalexchange.SignalExchange/ {
grpc_pass grpc://signal;
grpc_read_timeout 1d;
grpc_send_timeout 1d;
grpc_socket_keepalive on;
}
# Proxy Management http endpoint
location /api/ {
proxy_pass http://management;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; }
# Proxy Management grpc endpoint
location /management.ManagementService/ {
grpc_pass grpc://management;
#grpc_ssl_verify off;
grpc_read_timeout 1d;
grpc_send_timeout 1d;
grpc_socket_keepalive on;
}
# Relay for headless peers
location /relay {
grpc_pass grpc://relay;
grpc_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
grpc_set_header X-Forwarded-Proto $scheme;
grpc_read_timeout 1d;
grpc_send_timeout 1d;
grpc_socket_keepalive on;
}
}
x-default: &default
restart: 'unless-stopped'
logging:
driver: 'json-file'
options:
max-size: '500m'
max-file: '2'
services:
# UI dashboard
dashboard:
<<: *default
image: netbirdio/dashboard:latest
ports:
- 8081:80
- 9443:443
environment:
# Endpoints
- NETBIRD_MGMT_API_ENDPOINT=https://netbird.example.com
- NETBIRD_MGMT_GRPC_API_ENDPOINT=https://netbird.example.com
# OIDC
- AUTH_AUDIENCE=1234
- AUTH_CLIENT_ID=1234
- AUTH_CLIENT_SECRET=example
- AUTH_AUTHORITY=https://zitadel.example.com
- USE_AUTH0=false
- AUTH_SUPPORTED_SCOPES=openid profile email offline_access api
- AUTH_REDIRECT_URI=/auth
- AUTH_SILENT_REDIRECT_URI=/silent-auth
- NETBIRD_TOKEN_SOURCE=accessToken
# SSL
- NGINX_SSL_PORT=443
# Letsencrypt
- LETSENCRYPT_DOMAIN=
- LETSENCRYPT_EMAIL=
volumes:
- netbird-letsencrypt:/etc/letsencrypt/
# Signal
signal:
<<: *default
image: netbirdio/signal:latest
volumes:
- netbird-signal:/var/lib/netbird
ports:
- 10000:80
# Relay
relay:
<<: *default
image: netbirdio/relay:latest
environment:
- NB_LOG_LEVEL=info
- NB_LISTEN_ADDRESS=:33080
- NB_EXPOSED_ADDRESS=rels://netbird.example.com:33080/relay
# todo: change to a secure secret
- NB_AUTH_SECRET=example
ports:
- 33080:33080
# Management
management:
<<: *default
image: netbirdio/management:latest
depends_on:
- dashboard
volumes:
- netbird-mgmt:/var/lib/netbird
- netbird-letsencrypt:/etc/letsencrypt:ro
- ./management.json:/etc/netbird/management.json
ports:
- 9444: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.example.comm",
"--dns-domain=netbird.selfhosted"
]
environment:
- NETBIRD_STORE_ENGINE_POSTGRES_DSN=
- NETBIRD_STORE_ENGINE_MYSQL_DSN=
# Coturn
coturn:
<<: *default
image: coturn/coturn:latest
volumes:
- ./turnserver.conf:/etc/turnserver.conf:ro
network_mode: host
environment:
- REALM=netbird.example.com
- LISTEN_PORT=3478
- TLS_LISTEN_PORT=5349
- CERT_FILE=/etc/letsencrypt/live/example.com/fullchain.pem
- KEY_FILE=/etc/letsencrypt/live/example.com/privkey.pem
command:
- -c /etc/turnserver.conf
volumes:
netbird-mgmt:
netbird-signal:
netbird-letsencrypt:
{
"Stuns": [
{
"Proto": "udp",
"URI": "stun:netbird.example.com:3478",
"Username": "",
"Password": ""
}
],
"TURNConfig": {
"TimeBasedCredentials": false,
"CredentialsTTL": "12h0m0s",
"Secret": "secret",
"Turns": [
{
"Proto": "udp",
"URI": "turn:netbird.example.com:3478",
"Username": "self",
"Password": "example"
}
]
},
"Relay": {
"Addresses": [
"rels://netbird.example.com:33080/relay"
],
"CredentialsTTL": "24h0m0s",
"Secret": "example"
},
"Signal": {
"Proto": "http",
"URI": "netbird.example.com:10000",
"Username": "",
"Password": ""
},
"Datadir": "/var/lib/netbird/",
"DataStoreEncryptionKey": "example=",
"HttpConfig": {
"LetsEncryptDomain": "",
"CertFile": "",
"CertKey": "",
"AuthAudience": "1234",
"AuthIssuer": "https://zitadel.example.com",
"AuthUserIDClaim": "",
"AuthKeysLocation": "https://zitadel.example.com/oauth/v2/keys",
"OIDCConfigEndpoint": "https://zitadel.example.com/.well-known/openid-configuration",
"IdpSignKeyRefreshEnabled": true,
"ExtraAuthAudience": ""
},
"IdpManagerConfig": {
"ManagerType": "zitadel",
"ClientConfig": {
"Issuer": "https://zitadel.example.com",
"TokenEndpoint": "https://zitadel.example.com/oauth/v2/token",
"ClientID": "netbird",
"ClientSecret": "example",
"GrantType": "client_credentials"
},
"ExtraConfig": {
"AdminEndpoint": "https://zitadel.example.com/management/v1",
"ManagementEndpoint": "https://zitadel.example.com/management/v1"
},
"Auth0ClientCredentials": null,
"AzureClientCredentials": null,
"KeycloakClientCredentials": null,
"ZitadelClientCredentials": null
},
"DeviceAuthorizationFlow": {
"Provider": "hosted",
"ProviderConfig": {
"ClientID": "1234",
"ClientSecret": "",
"Domain": "zitadel.example.com",
"Audience": "1234",
"TokenEndpoint": "https://zitadel.example.com/oauth/v2/token",
"DeviceAuthEndpoint": "https://zitadel.example.com/oauth/v2/device_authorization",
"AuthorizationEndpoint": "",
"Scope": "openid profile email offline_access",
"UseIDToken": false,
"RedirectURLs": null,
"DisablePromptLogin": false,
"LoginFlag": 0
}
},
"PKCEAuthorizationFlow": {
"ProviderConfig": {
"ClientID": "1234",
"ClientSecret": "example",
"Domain": "",
"Audience": "335978769598709762",
"TokenEndpoint": "https://zitadel.example.com/oauth/v2/token",
"DeviceAuthEndpoint": "",
"AuthorizationEndpoint": "https://zitadel.example.com/oauth/v2/authorize",
"Scope": "openid profile email offline_access api",
"UseIDToken": false,
"RedirectURLs": [
"http://localhost:53000"
],
"DisablePromptLogin": false,
"LoginFlag": 0
}
},
"StoreConfig": {
"Engine": "sqlite"
},
"ReverseProxy": {
"TrustedHTTPProxies": [],
"TrustedHTTPProxiesCount": 0,
"TrustedPeers": [
"0.0.0.0/0"
]
},
"DisableDefaultPolicy": true
}
Hi team,
I’ve been working on a self-hosted NetBird docker deployment (signal + relay + management with Zitadel self-hosted docker deployed separately on the same server) and finally have everything working reliably except the Relay server when running behind an Nginx reverse proxy with TLS. Server is Ubuntu 22.04
Peers can connect successfully to the Signal service and management API, but relay connections keep failing with errors like:
I spent a few days alternately trying to terminate TLS at the nginx proxy and then terminating at the containers, but each time I'd end up breaking the working setup with no progress towards solving the relay issue.
Current working set up:
Ports open in UFW:
-3478/udp and 49152:65535/udp (STUN/media)
Other services (signal, management, dashboard) are working fine through the same proxy.
What I’ve tried
Questions
/etc/nginx/sites-available/netbird.conf
docker-compose
management.json