Split combined server split migration guides

This commit is contained in:
braginini
2026-02-11 13:49:51 +01:00
parent fce254b265
commit f06ba8855f
6 changed files with 658 additions and 643 deletions

View File

@@ -291,7 +291,15 @@ export const docsNavigation = [
isOpen: false,
links: [
{ title: 'Configuration Files', href: '/selfhosted/configuration-files' },
{ title: 'Scaling Your Deployment', href: '/selfhosted/scaling-your-self-hosted-deployment' },
{
title: 'Scaling Your Deployment', href: '/selfhosted/maintenance/scaling/scaling-your-self-hosted-deployment',
isOpen: false,
links: [
{ title: 'Set Up External Relays', href: '/selfhosted/maintenance/scaling/set-up-external-relays' },
{ title: 'Migrate SQLite to PostgreSQL', href: '/selfhosted/maintenance/scaling/migrate-sqlite-to-postgresql' },
{ title: 'Set Up External Signal', href: '/selfhosted/maintenance/scaling/set-up-external-signal' },
]
},
{ title: 'Backup', href: '/selfhosted/maintenance/backup' },
{ title: 'Upgrade', href: '/selfhosted/maintenance/upgrade' },
{ title: 'Remove', href: '/selfhosted/maintenance/remove' },

View File

@@ -0,0 +1,123 @@
# Migrate from SQLite to PostgreSQL
import {Note, Warning} from "@/components/mdx";
This guide is part of the [Splitting Your Self-Hosted Deployment](/selfhosted/maintenance/scaling/scaling-your-self-hosted-deployment) guide. It covers migrating your Management server database from SQLite to PostgreSQL.
The default NetBird deployment uses SQLite, which stores all data in a single file. This works well for smaller setups, but you may want to migrate to PostgreSQL if:
- You want the database on a separate, dedicated machine
- You need better concurrency handling for larger deployments
- You prefer the operational tooling and backup options that PostgreSQL provides
For smaller teams, SQLite is perfectly capable and migration is not required.
## Set Up PostgreSQL
If you don't already have a PostgreSQL instance, you can run one in Docker:
```bash
docker run -d \
--name postgres-server \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=password \
-p 5432:5432 \
-v postgres_data:/var/lib/postgresql/data \
postgres:16
```
<Warning>
Replace the default `password` with a strong, unique password for production deployments.
</Warning>
## Back Up the SQLite Store
Before migrating, create a backup of your SQLite database:
```bash
mkdir backup
docker compose cp -a netbird-server:/var/lib/netbird/. backup/
```
## Install pgloader
The migration uses [pgloader](https://github.com/dimitri/pgloader) to transfer data from SQLite to PostgreSQL:
```bash
# Debian/Ubuntu
sudo apt-get install pgloader
# macOS
brew install pgloader
```
## Create the Migration File
Create a file called `sqlite.load` with the following content:
```
LOAD DATABASE
FROM sqlite:///root/combined/backup/store.db
INTO postgresql://postgres:password@localhost:5432/postgres
WITH include drop, create tables, create indexes, reset sequences
CAST
column accounts.is_domain_primary_account to boolean,
column accounts.settings_peer_login_expiration_enabled to boolean,
column accounts.settings_peer_inactivity_expiration_enabled to boolean,
column accounts.settings_regular_users_view_blocked to boolean,
column accounts.settings_groups_propagation_enabled to boolean,
column accounts.settings_jwt_groups_enabled to boolean,
column accounts.settings_routing_peer_dns_resolution_enabled to boolean,
column accounts.settings_extra_peer_approval_enabled to boolean,
column accounts.settings_extra_user_approval_required to boolean,
column accounts.settings_lazy_connection_enabled to boolean
;
```
<Note>
Update the SQLite path and PostgreSQL connection string to match your environment.
</Note>
## Run the Migration
```bash
pgloader sqlite.load
```
## Update config.yaml
On your main server, update the `store` section in `config.yaml` to use PostgreSQL:
```yaml
server:
# ... existing settings ...
store:
engine: "postgres"
```
Then pass the PostgreSQL connection string as an environment variable in your `docker-compose.yml`:
```yaml
netbird-server:
environment:
- NETBIRD_STORE_ENGINE_POSTGRES_DSN=postgres://postgres:password@postgres-server:5432/postgres
```
## Restart and Verify
```bash
docker compose up -d
```
Check the logs to confirm PostgreSQL is being used:
```bash
docker compose logs netbird-server | grep store
```
You should see:
```
using Postgres store engine
```

View File

@@ -0,0 +1,96 @@
# Splitting Your Self-Hosted Deployment
import {Note, Warning} from "@/components/mdx";
This guide explains how to split your NetBird self-hosted deployment from a single-server setup into a distributed architecture for better reliability and performance.
The most common approach is extracting the relay service (with its embedded STUN server) to separate servers and moving the PostgreSQL database to a dedicated machine.
In most cases, you won't need to extract the Signal server, but for completeness, this guide covers that as well.
NetBird clients can tolerate a Management server outage as long as connections are already established through relays or peer-to-peer.
This makes a stable relay infrastructure especially important.
This guide assumes you have already [deployed a single-server NetBird](/selfhosted/selfhosted-quickstart) and have a working configuration.
<Note>
If you are looking for a high-availability setup for the Management and Signal services, this is available through an enterprise
commercial license [here](https://netbird.io/pricing#on-prem).
</Note>
## Architecture Overview
The default single-server deployment runs all services on one machine: **Caddy** (reverse proxy), **Dashboard** (web UI),
and a **combined netbird-server** container that includes Management, Signal, and Relay + STUN as components. Caddy handles TLS termination on ports 80/443, while STUN listens on UDP port 3478. The Management server uses a **SQLite** database by default.
After splitting, the **main server** keeps Caddy, Dashboard, Management, and optionally Signal.
The **relay servers** run independently on different machines, each handling relay (port 443) and STUN (port 3478) traffic. Peers receive relay addresses from the Management server and connect to them directly. Optionally, the SQLite database can be migrated to **PostgreSQL** on a dedicated server, and Signal can also be extracted to its own machine.
## Guides
- [Set Up External Relay Servers](/selfhosted/maintenance/scaling/set-up-external-relays) — Deploy relay and STUN servers on separate machines and configure the main server to use them
- [Migrate from SQLite to PostgreSQL](/selfhosted/maintenance/scaling/migrate-sqlite-to-postgresql) — Move the Management database to a dedicated PostgreSQL instance (optional)
- [Set Up External Signal Server](/selfhosted/maintenance/scaling/set-up-external-signal) — Extract the Signal server to its own machine (optional)
## Configuration Reference
### Relay Server Environment Variables
| Variable | Required | Description |
|----------|----------|-------------|
| `NB_LISTEN_ADDRESS` | Yes | Address to listen on (e.g., `:443`) |
| `NB_EXPOSED_ADDRESS` | Yes | Public relay URL (`rels://` for TLS, `rel://` for plain) |
| `NB_AUTH_SECRET` | Yes | Shared authentication secret |
| `NB_ENABLE_STUN` | No | Enable embedded STUN server (`true`/`false`) |
| `NB_STUN_PORTS` | No | STUN UDP port(s), default `3478` |
| `NB_LETSENCRYPT_DOMAINS` | No | Domain(s) for automatic Let's Encrypt certificates |
| `NB_LETSENCRYPT_EMAIL` | No | Email for Let's Encrypt notifications |
| `NB_TLS_CERT_FILE` | No | Path to TLS certificate (alternative to Let's Encrypt) |
| `NB_TLS_KEY_FILE` | No | Path to TLS private key |
| `NB_LOG_LEVEL` | No | Log level: `debug`, `info`, `warn`, `error` |
### Main Server config.yaml - External Services
```yaml
server:
# External STUN servers
stuns:
- uri: "stun:hostname:port"
proto: "udp" # or "tcp"
# External relay servers
relays:
addresses:
- "rels://hostname:port" # TLS
- "rel://hostname:port" # Plain (not recommended)
secret: "shared-secret"
credentialsTTL: "24h" # How long relay credentials are valid
# External signal server (optional, usually keep embedded)
# signalUri: "https://signal.example.com:443"
```
## Troubleshooting
### Peers Can't Connect via Relay
1. **Check secrets match**: The `authSecret`/`NB_AUTH_SECRET` must be identical everywhere
2. **Check firewall**: Ensure port 443/tcp is open on relay servers
3. **Check TLS**: If using `rels://`, ensure TLS is properly configured
4. **Check logs**: `docker compose logs relay` on the relay server
### STUN Not Working
1. **Check UDP port**: Ensure port 3478/udp is open and not blocked by firewall
2. **Check NAT**: Some carrier-grade NATs block STUN; try a different network
3. **Verify STUN is enabled**: `NB_ENABLE_STUN=true` on relay servers
### Relay Shows as Unavailable
1. **DNS resolution**: Ensure the relay domain resolves correctly
2. **Port reachability**: Test with `nc -zv relay-us.example.com 443`
3. **Certificate issues**: Check Let's Encrypt logs or certificate validity
## See Also
- [Configuration Files Reference](/selfhosted/configuration-files) - Full config.yaml documentation
- [Self-hosting Quickstart](/selfhosted/selfhosted-quickstart) - Initial deployment guide

View File

@@ -0,0 +1,281 @@
# Set Up External Relay Servers
import {Note, Warning} from "@/components/mdx";
This guide is part of the [Splitting Your Self-Hosted Deployment](/selfhosted/maintenance/scaling/scaling-your-self-hosted-deployment) guide. It covers deploying external relay and STUN servers and configuring your main server to use them.
For each relay server you want to deploy:
## 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., `relay-us.example.com`)
- Docker installed
- Firewall ports open: **80/tcp** (Let's Encrypt HTTP challenge), **443/tcp** (relay), and **3478/udp** (STUN). If you configure multiple STUN ports, open all of them
## Generate Authentication Secret
All relay servers must share the same authentication secret with your main server. You can generate one with:
```bash
# Generate a secure random secret
openssl rand -base64 32
```
Save this secret - you'll need it for both the relay servers and your main server's config.
## Create Relay Configuration
On your relay server, create a directory and configuration:
```bash
mkdir -p ~/netbird-relay
cd ~/netbird-relay
```
Create `relay.env` with your relay settings. The relay server can automatically obtain and renew TLS certificates via Let's Encrypt:
```bash
NB_LOG_LEVEL=info
NB_LISTEN_ADDRESS=:443
NB_EXPOSED_ADDRESS=rels://relay-us.example.com:443
NB_AUTH_SECRET=your-shared-secret-here
# TLS via Let's Encrypt (automatic certificate provisioning)
NB_LETSENCRYPT_DOMAINS=relay-us.example.com
NB_LETSENCRYPT_EMAIL=admin@example.com
NB_LETSENCRYPT_DATA_DIR=/data/letsencrypt
# Embedded STUN (comma-separated for multiple ports, e.g., 3478,3479)
NB_ENABLE_STUN=true
NB_STUN_PORTS=3478
```
<Note>
Replace `relay-us.example.com` with your relay server's domain and `your-shared-secret-here` with the secret you generated.
</Note>
Create `docker-compose.yml`:
```yaml
services:
relay:
image: netbirdio/relay:latest
container_name: netbird-relay
restart: unless-stopped
ports:
- '443:443'
# Expose all ports listed in NB_STUN_PORTS
- '3478:3478/udp'
env_file:
- relay.env
volumes:
- relay_data:/data
logging:
driver: "json-file"
options:
max-size: "500m"
max-file: "2"
volumes:
relay_data:
```
## Alternative: TLS with Existing Certificates
If you have existing TLS certificates (e.g., from your own CA or a wildcard cert), replace the Let's Encrypt variables in `relay.env` with:
```bash
# Replace the NB_LETSENCRYPT_* lines with:
NB_TLS_CERT_FILE=/certs/fullchain.pem
NB_TLS_KEY_FILE=/certs/privkey.pem
```
And add a certificate volume to `docker-compose.yml`:
```yaml
volumes:
- /path/to/certs:/certs:ro
- relay_data:/data
```
## Start the Relay Server
```bash
docker compose up -d
```
Verify it's running:
```bash
docker compose logs -f
```
You should see:
```
level=info msg="Starting relay server on :443"
level=info msg="Starting STUN server on port 3478"
```
If you configured Let's Encrypt, the relay generates TLS certificates lazily on the first incoming request. Trigger certificate provisioning and verify it by running:
```bash
curl -v https://relay-us.example.com/
```
A `404 page not found` response is expected — what matters is that the TLS handshake succeeds. Look for a valid Let's Encrypt certificate in the output:
```
* Server certificate:
* subject: CN=relay-us.example.com
* issuer: C=US; O=Let's Encrypt; CN=E8
* SSL certificate verify ok.
```
## Repeat for Additional Relay Servers
If deploying multiple relays (e.g., for different regions), repeat the steps above on each server. Use the **same `NB_AUTH_SECRET`** but update the domain name for each.
## Update Main Server Configuration
Now update your main NetBird server to use the external relays instead of the embedded one.
### Edit config.yaml
On your main server, edit the `config.yaml` file:
```bash
cd ~/netbird # or wherever your deployment is
nano config.yaml
```
Remove the `authSecret` from the `server` section and add `relays` and `stuns` sections pointing to your external servers. The presence of the `relays` section disables both the embedded relay and the embedded STUN server, so the `stuns` section is required to provide external STUN addresses:
```yaml
server:
listenAddress: ":80"
exposedAddress: "https://netbird.example.com:443"
# Remove authSecret to disable the embedded relay
# authSecret: ...
# Remove or comment out stunPorts since we're using external STUN
# stunPorts:
# - 3478
metricsPort: 9090
healthcheckAddress: ":9000"
logLevel: "info"
logFile: "console"
dataDir: "/var/lib/netbird"
# External STUN servers (your relay servers)
stuns:
- uri: "stun:relay-us.example.com:3478"
proto: "udp"
- uri: "stun:relay-eu.example.com:3478"
proto: "udp"
# External relay servers
relays:
addresses:
- "rels://relay-us.example.com:443"
- "rels://relay-eu.example.com:443"
secret: "your-shared-secret-here"
credentialsTTL: "24h"
auth:
enabled: true
issuer: "https://netbird.example.com/oauth2"
# ... rest of auth config
```
<Warning>
The `secret` under `relays` and the `NB_AUTH_SECRET` on all relay servers **must be identical**. Mismatched secrets will cause relay connections to fail silently.
</Warning>
### Update docker-compose.yml (Optional)
If your main server was exposing STUN port 3478, you can remove it since STUN is now handled by external relays:
```yaml
netbird-server:
image: netbirdio/netbird-server:latest
container_name: netbird-server
restart: unless-stopped
networks: [netbird]
# Remove the STUN port - no longer needed
# ports:
# - '3478:3478/udp'
volumes:
- netbird_data:/var/lib/netbird
- ./config.yaml:/etc/netbird/config.yaml
command: ["--config", "/etc/netbird/config.yaml"]
```
### Restart the Main Server
```bash
docker compose down
docker compose up -d
```
## Verify the Configuration
### Check Main Server Logs
```bash
docker compose logs netbird-server
```
Verify that the embedded relay is disabled and your external relay addresses are listed:
```
INFO combined/cmd/root.go: Management: true (log level: info)
INFO combined/cmd/root.go: Signal: true (log level: info)
INFO combined/cmd/root.go: Relay: false (log level: )
```
```
Relay addresses: [rels://relay-us.example.com:443 rels://relay-eu.example.com:443]
```
### Check Peer Status
Connect a NetBird client and verify that both STUN and relay services are available:
```bash
netbird status -d
```
The output should list your external STUN and relay servers. All configured STUN servers will appear, but only one randomly chosen relay is used per client:
```
Relays:
[stun:relay-us.example.com:3478] is Available
[stun:relay-eu.example.com:3478] is Available
[rels://relay-eu.example.com:443] is Available
```
You can also test failover by stopping one of the relay servers and checking the status again. The client will detect the unavailable server and use the remaining one:
```
Relays:
[stun:relay-us.example.com:3478] is Available
[stun:relay-eu.example.com:3478] is Unavailable, reason: stun request: context deadline exceeded
[rels://relay-us.example.com:443] is Available
```
### Test Relay Connectivity
You can force all peer connections through relay to verify it works end-to-end. On a client, run:
```bash
sudo netbird service reconfigure --service-env NB_FORCE_RELAY=true
```
Then test connectivity to another peer (e.g., with `ping`).
Once confirmed, switch back to normal mode. The client will attempt peer-to-peer connections first and fall back to relay only when direct connectivity isn't possible:
```bash
sudo netbird service reconfigure --service-env NB_FORCE_RELAY=false
```

View File

@@ -0,0 +1,149 @@
# Set Up External Signal Server
import {Note, Warning} from "@/components/mdx";
This guide is part of the [Splitting Your Self-Hosted Deployment](/selfhosted/maintenance/scaling/scaling-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](https://netbird.io/pricing#on-prem).
<Warning>
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.
</Warning>
## 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:
```bash
mkdir -p ~/netbird-signal
cd ~/netbird-signal
```
Like the relay, the signal server can automatically obtain TLS certificates via Let's Encrypt.
<Note>
Replace `signal.example.com` with your signal server's domain.
</Note>
Create `signal.env` with your signal settings:
```bash
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`:
```yaml
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:
```
## Alternative: TLS with Existing Certificates
If you have existing TLS certificates, replace the Let's Encrypt variable in `signal.env` with:
```bash
# 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`:
```yaml
volumes:
- /path/to/certs:/certs:ro
- signal_data:/var/lib/netbird
```
## Start the Signal Server
```bash
docker compose up -d
```
Verify it's running:
```bash
docker compose logs -f
```
If you configured Let's Encrypt, trigger certificate provisioning with an HTTPS request:
```bash
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:
```yaml
server:
# ... existing settings ...
# External signal server
signalUri: "https://signal.example.com:443"
```
Restart the main server:
```bash
docker compose down
docker compose up -d
```
## Verify Signal Extraction
Check the main server logs to confirm the embedded Signal is disabled:
```bash
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: )
```

View File

@@ -1,642 +0,0 @@
# Splitting Your Self-Hosted Deployment
import {Note, Warning} from "@/components/mdx";
This guide explains how to split your NetBird self-hosted deployment from a single-server setup into a distributed architecture for better reliability and performance.
The most common approach is extracting the relay service (with its embedded STUN server) to separate servers and moving the PostgreSQL database to a dedicated machine.
In most cases, you won't need to extract the Signal server, but for completeness, this guide covers that as well.
NetBird clients can tolerate a Management server outage as long as connections are already established through relays or peer-to-peer.
This makes a stable relay infrastructure especially important.
This guide assumes you have already [deployed a single-server NetBird](/selfhosted/selfhosted-quickstart) and have a working configuration.
<Note>
If you are looking for a high-availability setup for the Management and Signal services, this is available through an enterprise
commercial license [here](https://netbird.io/pricing#on-prem).
</Note>
## Architecture Overview
The default single-server deployment runs all services on one machine: **Caddy** (reverse proxy), **Dashboard** (web UI),
and a **combined netbird-server** container that includes Management, Signal, and Relay + STUN as components. Caddy handles TLS termination on ports 80/443, while STUN listens on UDP port 3478. The Management server uses a **SQLite** database by default.
After splitting, the **main server** keeps Caddy, Dashboard, Management, and optionally Signal.
The **relay servers** run independently on different machines, each handling relay (port 443) and STUN (port 3478) traffic. Peers receive relay addresses from the Management server and connect to them directly. Optionally, the SQLite database can be migrated to **PostgreSQL** on a dedicated server, and Signal can also be extracted to its own machine.
## Step 1: Set Up External Relay Servers
For each relay server you want to deploy:
### 1.1 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., `relay-us.example.com`)
- Docker installed
- Firewall ports open: **80/tcp** (Let's Encrypt HTTP challenge), **443/tcp** (relay), and **3478/udp** (STUN). If you configure multiple STUN ports, open all of them
### 1.2 Generate Authentication Secret
All relay servers must share the same authentication secret with your main server. You can generate one with:
```bash
# Generate a secure random secret
openssl rand -base64 32
```
Save this secret - you'll need it for both the relay servers and your main server's config.
### 1.3 Create Relay Configuration
On your relay server, create a directory and configuration:
```bash
mkdir -p ~/netbird-relay
cd ~/netbird-relay
```
Create `relay.env` with your relay settings. The relay server can automatically obtain and renew TLS certificates via Let's Encrypt:
```bash
NB_LOG_LEVEL=info
NB_LISTEN_ADDRESS=:443
NB_EXPOSED_ADDRESS=rels://relay-us.example.com:443
NB_AUTH_SECRET=your-shared-secret-here
# TLS via Let's Encrypt (automatic certificate provisioning)
NB_LETSENCRYPT_DOMAINS=relay-us.example.com
NB_LETSENCRYPT_EMAIL=admin@example.com
NB_LETSENCRYPT_DATA_DIR=/data/letsencrypt
# Embedded STUN (comma-separated for multiple ports, e.g., 3478,3479)
NB_ENABLE_STUN=true
NB_STUN_PORTS=3478
```
<Note>
Replace `relay-us.example.com` with your relay server's domain and `your-shared-secret-here` with the secret you generated.
</Note>
Create `docker-compose.yml`:
```yaml
services:
relay:
image: netbirdio/relay:latest
container_name: netbird-relay
restart: unless-stopped
ports:
- '443:443'
# Expose all ports listed in NB_STUN_PORTS
- '3478:3478/udp'
env_file:
- relay.env
volumes:
- relay_data:/data
logging:
driver: "json-file"
options:
max-size: "500m"
max-file: "2"
volumes:
relay_data:
```
### 1.4 Alternative: TLS with Existing Certificates
If you have existing TLS certificates (e.g., from your own CA or a wildcard cert), replace the Let's Encrypt variables in `relay.env` with:
```bash
# Replace the NB_LETSENCRYPT_* lines with:
NB_TLS_CERT_FILE=/certs/fullchain.pem
NB_TLS_KEY_FILE=/certs/privkey.pem
```
And add a certificate volume to `docker-compose.yml`:
```yaml
volumes:
- /path/to/certs:/certs:ro
- relay_data:/data
```
### 1.5 Start the Relay Server
```bash
docker compose up -d
```
Verify it's running:
```bash
docker compose logs -f
```
You should see:
```
level=info msg="Starting relay server on :443"
level=info msg="Starting STUN server on port 3478"
```
If you configured Let's Encrypt, the relay generates TLS certificates lazily on the first incoming request. Trigger certificate provisioning and verify it by running:
```bash
curl -v https://relay-us.example.com/
```
A `404 page not found` response is expected — what matters is that the TLS handshake succeeds. Look for a valid Let's Encrypt certificate in the output:
```
* Server certificate:
* subject: CN=relay-us.example.com
* issuer: C=US; O=Let's Encrypt; CN=E8
* SSL certificate verify ok.
```
### 1.6 Repeat for Additional Relay Servers
If deploying multiple relays (e.g., for different regions), repeat steps 1.1-1.5 on each server. Use the **same `NB_AUTH_SECRET`** but update the domain name for each.
## Step 2: Update Main Server Configuration
Now update your main NetBird server to use the external relays instead of the embedded one.
### 2.1 Edit config.yaml
On your main server, edit the `config.yaml` file:
```bash
cd ~/netbird # or wherever your deployment is
nano config.yaml
```
Remove the `authSecret` from the `server` section and add `relays` and `stuns` sections pointing to your external servers. The presence of the `relays` section disables both the embedded relay and the embedded STUN server, so the `stuns` section is required to provide external STUN addresses:
```yaml
server:
listenAddress: ":80"
exposedAddress: "https://netbird.example.com:443"
# Remove authSecret to disable the embedded relay
# authSecret: ...
# Remove or comment out stunPorts since we're using external STUN
# stunPorts:
# - 3478
metricsPort: 9090
healthcheckAddress: ":9000"
logLevel: "info"
logFile: "console"
dataDir: "/var/lib/netbird"
# External STUN servers (your relay servers)
stuns:
- uri: "stun:relay-us.example.com:3478"
proto: "udp"
- uri: "stun:relay-eu.example.com:3478"
proto: "udp"
# External relay servers
relays:
addresses:
- "rels://relay-us.example.com:443"
- "rels://relay-eu.example.com:443"
secret: "your-shared-secret-here"
credentialsTTL: "24h"
auth:
enabled: true
issuer: "https://netbird.example.com/oauth2"
# ... rest of auth config
```
<Warning>
The `secret` under `relays` and the `NB_AUTH_SECRET` on all relay servers **must be identical**. Mismatched secrets will cause relay connections to fail silently.
</Warning>
### 2.2 Update docker-compose.yml (Optional)
If your main server was exposing STUN port 3478, you can remove it since STUN is now handled by external relays:
```yaml
netbird-server:
image: netbirdio/netbird-server:latest
container_name: netbird-server
restart: unless-stopped
networks: [netbird]
# Remove the STUN port - no longer needed
# ports:
# - '3478:3478/udp'
volumes:
- netbird_data:/var/lib/netbird
- ./config.yaml:/etc/netbird/config.yaml
command: ["--config", "/etc/netbird/config.yaml"]
```
### 2.3 Restart the Main Server
```bash
docker compose down
docker compose up -d
```
## Step 3: Verify the Configuration
### 3.1 Check Main Server Logs
```bash
docker compose logs netbird-server
```
Verify that the embedded relay is disabled and your external relay addresses are listed:
```
INFO combined/cmd/root.go: Management: true (log level: info)
INFO combined/cmd/root.go: Signal: true (log level: info)
INFO combined/cmd/root.go: Relay: false (log level: )
```
```
Relay addresses: [rels://relay-us.example.com:443 rels://relay-eu.example.com:443]
```
### 3.2 Check Peer Status
Connect a NetBird client and verify that both STUN and relay services are available:
```bash
netbird status -d
```
The output should list your external STUN and relay servers. All configured STUN servers will appear, but only one randomly chosen relay is used per client:
```
Relays:
[stun:relay-us.example.com:3478] is Available
[stun:relay-eu.example.com:3478] is Available
[rels://relay-eu.example.com:443] is Available
```
You can also test failover by stopping one of the relay servers and checking the status again. The client will detect the unavailable server and use the remaining one:
```
Relays:
[stun:relay-us.example.com:3478] is Available
[stun:relay-eu.example.com:3478] is Unavailable, reason: stun request: context deadline exceeded
[rels://relay-us.example.com:443] is Available
```
### 3.3 Test Relay Connectivity
You can force all peer connections through relay to verify it works end-to-end. On a client, run:
```bash
sudo netbird service reconfigure --service-env NB_FORCE_RELAY=true
```
Then test connectivity to another peer (e.g., with `ping`).
Once confirmed, switch back to normal mode. The client will attempt peer-to-peer connections first and fall back to relay only when direct connectivity isn't possible:
```bash
sudo netbird service reconfigure --service-env NB_FORCE_RELAY=false
```
## Step 4: Migrate from SQLite to PostgreSQL (Optional)
The default NetBird deployment uses SQLite, which stores all data in a single file. This works well for smaller setups, but you may want to migrate to PostgreSQL if:
- You want the database on a separate, dedicated machine
- You need better concurrency handling for larger deployments
- You prefer the operational tooling and backup options that PostgreSQL provides
For smaller teams, SQLite is perfectly capable and migration is not required.
### 4.1 Set Up PostgreSQL
If you don't already have a PostgreSQL instance, you can run one in Docker:
```bash
docker run -d \
--name postgres-server \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=password \
-p 5432:5432 \
-v postgres_data:/var/lib/postgresql/data \
postgres:16
```
<Warning>
Replace the default `password` with a strong, unique password for production deployments.
</Warning>
### 4.2 Back Up the SQLite Store
Before migrating, create a backup of your SQLite database:
```bash
mkdir backup
docker compose cp -a netbird-server:/var/lib/netbird/. backup/
```
### 4.3 Install pgloader
The migration uses [pgloader](https://github.com/dimitri/pgloader) to transfer data from SQLite to PostgreSQL:
```bash
# Debian/Ubuntu
sudo apt-get install pgloader
# macOS
brew install pgloader
```
### 4.4 Create the Migration File
Create a file called `sqlite.load` with the following content:
```
LOAD DATABASE
FROM sqlite:///root/combined/backup/store.db
INTO postgresql://postgres:password@localhost:5432/postgres
WITH include drop, create tables, create indexes, reset sequences
CAST
column accounts.is_domain_primary_account to boolean,
column accounts.settings_peer_login_expiration_enabled to boolean,
column accounts.settings_peer_inactivity_expiration_enabled to boolean,
column accounts.settings_regular_users_view_blocked to boolean,
column accounts.settings_groups_propagation_enabled to boolean,
column accounts.settings_jwt_groups_enabled to boolean,
column accounts.settings_routing_peer_dns_resolution_enabled to boolean,
column accounts.settings_extra_peer_approval_enabled to boolean,
column accounts.settings_extra_user_approval_required to boolean,
column accounts.settings_lazy_connection_enabled to boolean
;
```
<Note>
Update the SQLite path and PostgreSQL connection string to match your environment.
</Note>
### 4.5 Run the Migration
```bash
pgloader sqlite.load
```
### 4.6 Update config.yaml
On your main server, update the `store` section in `config.yaml` to use PostgreSQL:
```yaml
server:
# ... existing settings ...
store:
engine: "postgres"
```
Then pass the PostgreSQL connection string as an environment variable in your `docker-compose.yml`:
```yaml
netbird-server:
environment:
- NETBIRD_STORE_ENGINE_POSTGRES_DSN=postgres://postgres:password@postgres-server:5432/postgres
```
### 4.7 Restart and Verify
```bash
docker compose up -d
```
Check the logs to confirm PostgreSQL is being used:
```bash
docker compose logs netbird-server | grep store
```
You should see:
```
using Postgres store engine
```
## Step 5: Extract the Signal Server (Optional)
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](https://netbird.io/pricing#on-prem).
<Warning>
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.
</Warning>
### 5.1 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)
### 5.2 Create Signal Configuration
On your signal server, create a directory and configuration:
```bash
mkdir -p ~/netbird-signal
cd ~/netbird-signal
```
Like the relay, the signal server can automatically obtain TLS certificates via Let's Encrypt.
<Note>
Replace `signal.example.com` with your signal server's domain.
</Note>
Create `signal.env` with your signal settings:
```bash
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`:
```yaml
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:
```
### 5.3 Alternative: TLS with Existing Certificates
If you have existing TLS certificates, replace the Let's Encrypt variable in `signal.env` with:
```bash
# 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`:
```yaml
volumes:
- /path/to/certs:/certs:ro
- signal_data:/var/lib/netbird
```
### 5.4 Start the Signal Server
```bash
docker compose up -d
```
Verify it's running:
```bash
docker compose logs -f
```
If you configured Let's Encrypt, trigger certificate provisioning with an HTTPS request:
```bash
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.
```
### 5.5 Update Main Server Configuration
On your main server, add `signalUri` to `config.yaml`. This disables the embedded Signal server:
```yaml
server:
# ... existing settings ...
# External signal server
signalUri: "https://signal.example.com:443"
```
Restart the main server:
```bash
docker compose down
docker compose up -d
```
### 5.6 Verify Signal Extraction
Check the main server logs to confirm the embedded Signal is disabled:
```bash
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: )
```
## Configuration Reference
### Relay Server Environment Variables
| Variable | Required | Description |
|----------|----------|-------------|
| `NB_LISTEN_ADDRESS` | Yes | Address to listen on (e.g., `:443`) |
| `NB_EXPOSED_ADDRESS` | Yes | Public relay URL (`rels://` for TLS, `rel://` for plain) |
| `NB_AUTH_SECRET` | Yes | Shared authentication secret |
| `NB_ENABLE_STUN` | No | Enable embedded STUN server (`true`/`false`) |
| `NB_STUN_PORTS` | No | STUN UDP port(s), default `3478` |
| `NB_LETSENCRYPT_DOMAINS` | No | Domain(s) for automatic Let's Encrypt certificates |
| `NB_LETSENCRYPT_EMAIL` | No | Email for Let's Encrypt notifications |
| `NB_TLS_CERT_FILE` | No | Path to TLS certificate (alternative to Let's Encrypt) |
| `NB_TLS_KEY_FILE` | No | Path to TLS private key |
| `NB_LOG_LEVEL` | No | Log level: `debug`, `info`, `warn`, `error` |
### Main Server config.yaml - External Services
```yaml
server:
# External STUN servers
stuns:
- uri: "stun:hostname:port"
proto: "udp" # or "tcp"
# External relay servers
relays:
addresses:
- "rels://hostname:port" # TLS
- "rel://hostname:port" # Plain (not recommended)
secret: "shared-secret"
credentialsTTL: "24h" # How long relay credentials are valid
# External signal server (optional, usually keep embedded)
# signalUri: "https://signal.example.com:443"
```
## Troubleshooting
### Peers Can't Connect via Relay
1. **Check secrets match**: The `authSecret`/`NB_AUTH_SECRET` must be identical everywhere
2. **Check firewall**: Ensure port 443/tcp is open on relay servers
3. **Check TLS**: If using `rels://`, ensure TLS is properly configured
4. **Check logs**: `docker compose logs relay` on the relay server
### STUN Not Working
1. **Check UDP port**: Ensure port 3478/udp is open and not blocked by firewall
2. **Check NAT**: Some carrier-grade NATs block STUN; try a different network
3. **Verify STUN is enabled**: `NB_ENABLE_STUN=true` on relay servers
### Relay Shows as Unavailable
1. **DNS resolution**: Ensure the relay domain resolves correctly
2. **Port reachability**: Test with `nc -zv relay-us.example.com 443`
3. **Certificate issues**: Check Let's Encrypt logs or certificate validity
## Next Steps
- Add monitoring with Prometheus metrics (`NB_METRICS_PORT`)
- Set up health checks for container orchestration
- Consider geographic DNS for automatic relay selection
- Review [Reverse Proxy Configuration](/selfhosted/reverse-proxy) if placing relays behind a proxy
## See Also
- [Configuration Files Reference](/selfhosted/configuration-files) - Full config.yaml documentation
- [Self-hosting Quickstart](/selfhosted/selfhosted-quickstart) - Initial deployment guide
- [Troubleshooting](/selfhosted/troubleshooting) - Common issues and solutions