Skip to content

Problem getting NetBird Relay working behind Nginx with TLS on self-hosted setup #4472

Description

@teksrq

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:

failed to dial to Relay server 'wss://netbird.example.com:33080/relay':
failed to send handshake request:
Get "https://netbird.example.com:33080/relay":
http: server gave HTTP response to HTTPS client

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:

  • Nginx terminates TLS and forwards to the relay container.
    Ports open in UFW:
  • 10000/tcp (signal)
  • 33080/tcp (relay over TLS)
    -3478/udp and 49152:65535/udp (STUN/media)
  • 33080/udp allowed in from anywhere
    Other services (signal, management, dashboard) are working fine through the same proxy.

What I’ve tried

  • Confirmed that peers connect to the Signal service without issue.
  • Verified that nginx forwards /relay requests to the relay container.
  • Double-checked TLS certs (valid, working for management/dashboard).
  • Reviewed logs from peer and relay containers — relay errors are consistent.

Questions

  1. Is NetBird relay expected to work over pure TLS (wss://) behind an Nginx reverse proxy, or does it require QUIC/UDP to function properly?
  2. If TLS relay should work, are there specific Nginx config requirements for forwarding WebSockets and gRPC to the relay container?
  3. If QUIC is required, should I expose 33080/udp directly (bypassing nginx), or can nginx proxy QUIC as well?

/etc/nginx/sites-available/netbird.conf

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;
    }

}

docker-compose

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:

management.json

{
    "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
}

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions