Set Up External Signal Server
Updated
This guide is part of the Splitting Your Self-Hosted Deployment guide. It covers extracting the Signal server to a dedicated machine.
In most deployments, the embedded Signal server works well and does not need to be extracted. Consider running an external Signal server if you want to separate it from the Management server for organizational or infrastructure reasons.
Unlike relay servers, the Signal server cannot be replicated as it maintains in-memory connection state. If you need high-availability active-active mode for both Management and Signal, this is available through an enterprise commercial license.
Changing the Signal server URL requires all clients to restart. After updating the configuration, each client must run netbird down followed by netbird up to reconnect to the new Signal server. This limitation will be addressed in a future client release.
Server Requirements
- A Linux VM with at least 1 CPU and 1GB RAM
- Public IP address
- A domain name pointing to the server (e.g.,
signal.example.com) - Docker installed
- Firewall ports open: 80/tcp (Let's Encrypt HTTP challenge) and 443/tcp (gRPC/WebSocket client communication)
Create Signal Configuration
On your signal server, create a directory and configuration:
mkdir -p ~/netbird-signal
cd ~/netbird-signal
Like the relay, the signal server can automatically obtain TLS certificates via Let's Encrypt.
Replace signal.example.com with your signal server's domain.
Create signal.env with your signal settings:
NB_PORT=443
NB_LOG_LEVEL=info
# TLS via Let's Encrypt (automatic certificate provisioning)
NB_LETSENCRYPT_DOMAIN=signal.example.com
Create docker-compose.yml:
services:
signal:
image: netbirdio/signal:latest
container_name: netbird-signal
restart: unless-stopped
ports:
- '443:443'
- '80:80'
env_file:
- signal.env
volumes:
- signal_data:/var/lib/netbird
logging:
driver: "json-file"
options:
max-size: "500m"
max-file: "2"
volumes:
signal_data:
Let's Encrypt challenge listener
When NB_PORT is not 443, the signal server opens a separate listener for the Let's Encrypt challenges. Set its address with --letsencrypt-listen-address or NB_LETSENCRYPT_LISTEN_ADDRESS. The default is :443.
A process that does not run as root (for example, the UBI images) cannot bind port 443. In this case, choose one of these options:
-
Set the listener to a port that the process can bind, and forward public port 443 to it:
NB_LETSENCRYPT_LISTEN_ADDRESS=:8443 -
Forward public port 443 to
NB_PORT, and disable the separate listener with an empty value. The main TLS listener then answers the TLS-ALPN-01 challenges:NB_LETSENCRYPT_LISTEN_ADDRESS=
If the challenge listener cannot bind its address, the signal server logs an error and continues to run.
The Management server has the same --letsencrypt-listen-address flag, with the same default. It does not read an environment variable for it. If its challenge listener cannot bind, the Management server stops with an error. The Relay and the combined server do not use this flag.
Alternative: TLS with Existing Certificates
If you have existing TLS certificates, replace the Let's Encrypt variable in signal.env with:
# Replace the NB_LETSENCRYPT_DOMAIN line with:
NB_CERT_FILE=/certs/fullchain.pem
NB_CERT_KEY=/certs/privkey.pem
And add a certificate volume to docker-compose.yml:
volumes:
- /path/to/certs:/certs:ro
- signal_data:/var/lib/netbird
Start the Signal Server
docker compose up -d
Verify it's running:
docker compose logs -f
If you configured Let's Encrypt, trigger certificate provisioning with an HTTPS request:
curl -v https://signal.example.com/
Confirm the certificate was issued:
* Server certificate:
* subject: CN=signal.example.com
* issuer: C=US; O=Let's Encrypt; CN=E8
* SSL certificate verify ok.
Update Main Server Configuration
On your main server, add signalUri to config.yaml. This disables the embedded Signal server:
server:
# ... existing settings ...
# External signal server
signalUri: "https://signal.example.com:443"
Restart the main server:
docker compose down
docker compose up -d
Verify Signal Extraction
Check the main server logs to confirm the embedded Signal is disabled:
docker compose logs netbird-server
INFO combined/cmd/root.go: Management: true (log level: info)
INFO combined/cmd/root.go: Signal: false (log level: )
INFO combined/cmd/root.go: Relay: false (log level: )

