diff --git a/src/components/NavigationDocs.jsx b/src/components/NavigationDocs.jsx
index 4d4acd84..3346167f 100644
--- a/src/components/NavigationDocs.jsx
+++ b/src/components/NavigationDocs.jsx
@@ -301,6 +301,15 @@ export const docsNavigation = [
isOpen: false,
links: [
{ title: 'Configuration Files', href: '/selfhosted/configuration-files' },
+ {
+ 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' },
diff --git a/src/pages/selfhosted/configuration-files.mdx b/src/pages/selfhosted/configuration-files.mdx
index 1aa3250f..b3e6a551 100644
--- a/src/pages/selfhosted/configuration-files.mdx
+++ b/src/pages/selfhosted/configuration-files.mdx
@@ -41,11 +41,15 @@ The Docker Compose file defines all NetBird services, their dependencies, networ
### Services Overview
-| Service | Image | Internal Port | Description |
-|---------|-------|---------------|-------------|
-| `traefik` | `traefik:v3.6` | 80, 443 | Handles TLS termination via Let's Encrypt and routes incoming HTTPS requests to the appropriate NetBird services. Only included when using the built-in Traefik option (option 0). |
-| `dashboard` | `netbirdio/dashboard` | 80 | The web-based management console where administrators configure networks, manage peers, create access policies, and view activity logs. Includes an embedded nginx server for serving the UI. |
-| `netbird-server` | `netbirdio/netbird-server` | 80, 3478/udp | Combined server that includes management, signal, relay, and embedded STUN in a single container. Configured via `config.yaml`. |
+| Service | Image | Internal Port | External (Exposed) | Description |
+|---------|-------|---------------|-------------------|-------------|
+| `traefik` | `traefik:v3.6` | 80, 443 | 80:80, 443:443 | Handles TLS termination via Let's Encrypt and routes incoming HTTPS requests to the appropriate NetBird services. Only included when using the built-in Traefik option (option 0). |
+| `dashboard` | `netbirdio/dashboard` | 80 | 8080:80 | The web-based management console where administrators configure networks, manage peers, create access policies, and view activity logs. Includes an embedded nginx server for serving the UI. |
+| `netbird-server` | `netbirdio/netbird-server` | 80, 3478/udp | 8081:80, 3478:3478/udp | Combined server that includes management, signal, relay, and embedded STUN in a single container. Configured via `config.yaml`. |
+
+
+**Internal vs External ports**: Internal ports are what services listen on inside their containers. External (Exposed) ports show the host-to-container mapping used when running without the built-in Traefik (e.g., with Nginx or other reverse proxies). When using the default Traefik deployment, only Traefik exposes ports externally.
+
The `netbird-server` container combines what were previously separate management, signal, relay, and STUN containers into a single service. This simplifies the deployment architecture while maintaining the same functionality.
@@ -91,8 +95,6 @@ The `readTimeout=0` setting on the websecure entrypoint disables read timeouts,
### Dashboard Service
-The dashboard provides the web interface for NetBird management.
-
**With built-in Traefik (default):**
```yaml
dashboard:
@@ -117,24 +119,6 @@ dashboard:
max-file: "2"
```
-**With external reverse proxy (exposed ports):**
-```yaml
-dashboard:
- image: netbirdio/dashboard:latest
- container_name: netbird-dashboard
- restart: unless-stopped
- networks: [netbird]
- ports:
- - '127.0.0.1:8080:80'
- env_file:
- - ./dashboard.env
- logging:
- driver: "json-file"
- options:
- max-size: "500m"
- max-file: "2"
-```
-
The dashboard service is configured via the `dashboard.env` file. See the [dashboard.env section](#dashboardenv) for the full list of environment variables. When using the built-in Traefik, no ports are exposed directly from the dashboard container; Traefik routes traffic to it internally via Docker labels.
@@ -256,7 +240,8 @@ server:
- "http://localhost:53000/"
store:
- engine: "sqlite"
+ engine: "sqlite" # sqlite, postgres, or mysql
+ dsn: "" # Connection string for postgres or mysql
encryptionKey: "your-encryption-key"
```
@@ -330,18 +315,32 @@ Configures the database backend for storing all NetBird management data includin
Database engine. Options: `sqlite`, `postgres`, `mysql`. Default: `sqlite`.
+
+ Connection string for postgres or mysql engines. For postgres: `host=localhost user=netbird password=secret dbname=netbird port=5432`. Alternatively, use the `NETBIRD_STORE_ENGINE_POSTGRES_DSN` or `NETBIRD_STORE_ENGINE_MYSQL_DSN` environment variables.
+
32-byte (256-bit) encryption key for sensitive data at rest. Used to encrypt setup keys, API tokens, and other secrets stored in the database. Auto-generated by the setup script.
+**What data is stored?**
+
+- **Accounts and users** - User accounts, roles, and permissions
+- **Peers** - Registered devices, their WireGuard keys, IP assignments, and metadata
+- **Groups** - Peer groupings used for access control
+- **Access policies** - Network access rules
+- **Routes** - Network routes for external subnets
+- **DNS configuration** - Custom DNS settings
+- **Setup keys** - Keys for automated peer enrollment
+- **Activity logs** - Audit trail
+
| Engine | Storage | Notes |
|--------|---------|-------|
| SQLite (default) | `/var/lib/netbird/` volume | File-based database stored in the `netbird_data` Docker volume. Zero configuration required, but does not support concurrent writes or running multiple management instances. Best for testing or small deployments. |
| PostgreSQL | External database server | Recommended for production deployments. Supports concurrent access, enabling multiple management instances for high availability. |
| MySQL | External database server | Alternative to PostgreSQL for organizations that have standardized on MySQL/MariaDB. Provides similar benefits including concurrent access. |
-For PostgreSQL or MySQL, set the connection string via environment variables on the `netbird-server` container. See [Using an External Database](#using-an-external-database) below.
+For PostgreSQL or MySQL, set the connection string via the `server.store.dsn` field in `config.yaml` or environment variables on the `netbird-server` container. See [Using an External Database](#using-an-external-database) below.
See [Management Postgres Store](/selfhosted/postgres-store) for detailed PostgreSQL setup.
@@ -430,13 +429,14 @@ To use PostgreSQL instead of SQLite:
server:
store:
engine: "postgres"
+ dsn: "host=db-server user=netbird password=secret dbname=netbird port=5432"
```
-2. Set the connection string as an environment variable on the `netbird-server` container in `docker-compose.yml`:
+Alternatively, you can use an environment variable instead of putting the DSN in the config file:
```yaml
netbird-server:
environment:
- - NETBIRD_STORE_ENGINE_POSTGRES_DSN=postgres://user:password@host:5432/netbird?sslmode=disable
+ - NETBIRD_STORE_ENGINE_POSTGRES_DSN=host=db-server user=netbird password=secret dbname=netbird port=5432
# Or for MySQL:
# - NETBIRD_STORE_ENGINE_MYSQL_DSN=user:password@tcp(host:3306)/netbird
```
@@ -454,6 +454,25 @@ server:
Options: `debug`, `info`, `warn`, `error`. Use `debug` for troubleshooting connection issues.
+### Custom STUN Ports
+
+To use multiple STUN ports, update `config.yaml`:
+
+```yaml
+server:
+ stunPorts:
+ - 3478
+ - 3479
+```
+
+Make sure to expose all ports in `docker-compose.yml`:
+```yaml
+netbird-server:
+ ports:
+ - '3478:3478/udp'
+ - '3479:3479/udp'
+```
+
### Behind a Reverse Proxy
When running behind your own reverse proxy (Nginx, Caddy, Nginx Proxy Manager, etc.) instead of the built-in Traefik:
@@ -466,6 +485,35 @@ When running behind your own reverse proxy (Nginx, Caddy, Nginx Proxy Manager, e
See [Reverse Proxy Configuration](/selfhosted/reverse-proxy) for detailed templates for Nginx, Caddy, and other proxies.
+### Using External Services (Advanced)
+
+
+The default NetBird deployment includes embedded relay, signal, and STUN services. External services are only needed for advanced use cases.
+
+
+To use external STUN, relay, or signal servers, add overrides to `config.yaml`:
+
+```yaml
+server:
+ # ... basic settings ...
+
+ # Optional: Use external STUN servers
+ stuns:
+ - uri: "stun:stun.example.com:3478"
+ proto: "udp"
+
+ # Optional: Use external relay servers
+ relays:
+ addresses:
+ - "rels://relay.example.com:443"
+ secret: "relay-auth-secret"
+ credentialsTTL: "24h"
+
+ # Optional: Use external signal server
+ signalUri: "https://signal.example.com:443"
+```
+See the [Scaling Your Self-Hosted Deployment](/selfhosted/maintenance/scaling/scaling-your-self-hosted-deployment) guide for more details on configuring external services.
+
---
## See Also
diff --git a/src/pages/selfhosted/maintenance/backup.mdx b/src/pages/selfhosted/maintenance/backup.mdx
index 537c9c1f..337deb0e 100644
--- a/src/pages/selfhosted/maintenance/backup.mdx
+++ b/src/pages/selfhosted/maintenance/backup.mdx
@@ -5,13 +5,28 @@ To back up your NetBird installation, you need to copy the configuration files a
The configuration files are located in the folder where you ran [the installation script](/selfhosted/selfhosted-quickstart#installation-script). To back up, copy the files to a backup location:
```bash
mkdir backup
-cp docker-compose.yml Caddyfile dashboard.env management.json relay.env backup/
+cp docker-compose.yml dashboard.env config.yaml backup/
```
For detailed information about each configuration file and its options, see the [Configuration Files Reference](/selfhosted/configuration-files).
-To save the Management service databases, stop the Management service and copy the files from the store directory:
+To save the server databases, stop the server and copy the files from the data directory:
+```bash
+docker compose stop netbird-server
+docker compose cp -a netbird-server:/var/lib/netbird/ backup/
+docker compose start netbird-server
+```
+
+## Older Setup (Separate Containers)
+
+If your deployment uses the older setup with separate containers (`management`, `signal`, `relay`, `coturn`), back up the configuration files:
+```bash
+mkdir backup
+cp docker-compose.yml Caddyfile dashboard.env management.json relay.env backup/
+```
+
+To save the Management service database:
```bash
docker compose stop management
docker compose cp -a management:/var/lib/netbird/ backup/
diff --git a/src/pages/selfhosted/maintenance/remove.mdx b/src/pages/selfhosted/maintenance/remove.mdx
index 66b1dacf..122fe66c 100644
--- a/src/pages/selfhosted/maintenance/remove.mdx
+++ b/src/pages/selfhosted/maintenance/remove.mdx
@@ -5,7 +5,7 @@ To remove the NetBird installation and all related data from your server, run th
# remove all NetBird-related containers and volumes (data)
docker compose down --volumes
# remove downloaded and generated config files
-rm -f docker-compose.yml Caddyfile dashboard.env management.json relay.env
+rm -f docker-compose.yml dashboard.env config.yaml nginx-netbird.conf caddyfile-netbird.txt npm-advanced-config.txt
```
## Get In Touch
diff --git a/src/pages/selfhosted/maintenance/scaling/migrate-sqlite-to-postgresql.mdx b/src/pages/selfhosted/maintenance/scaling/migrate-sqlite-to-postgresql.mdx
new file mode 100644
index 00000000..969995bc
--- /dev/null
+++ b/src/pages/selfhosted/maintenance/scaling/migrate-sqlite-to-postgresql.mdx
@@ -0,0 +1,124 @@
+# 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
+```
+
+
+Replace the default `password` with a strong, unique password for production deployments.
+
+
+## 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
+;
+```
+
+
+Update the SQLite path and PostgreSQL connection string to match your environment.
+
+
+## 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"
+ dsn: "host=postgres-server user=postgres password=password dbname=postgres port=5432"
+```
+
+Alternatively, you can pass the connection string as an environment variable instead of putting it in the config file:
+
+```yaml
+ netbird-server:
+ environment:
+ - NETBIRD_STORE_ENGINE_POSTGRES_DSN=host=postgres-server user=postgres password=password dbname=postgres port=5432
+```
+
+## 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
+```
\ No newline at end of file
diff --git a/src/pages/selfhosted/maintenance/scaling/scaling-your-self-hosted-deployment.mdx b/src/pages/selfhosted/maintenance/scaling/scaling-your-self-hosted-deployment.mdx
new file mode 100644
index 00000000..4d817cde
--- /dev/null
+++ b/src/pages/selfhosted/maintenance/scaling/scaling-your-self-hosted-deployment.mdx
@@ -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.
+
+
+ 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).
+
+
+## Architecture Overview
+
+The default single-server deployment runs all services on one machine: **Traefik** (reverse proxy), **Dashboard** (web UI),
+and a **combined netbird-server** container that includes Management, Signal, and Relay + STUN as components. Traefik 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 Traefik, 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
\ No newline at end of file
diff --git a/src/pages/selfhosted/maintenance/scaling/set-up-external-relays.mdx b/src/pages/selfhosted/maintenance/scaling/set-up-external-relays.mdx
new file mode 100644
index 00000000..7bb1e22f
--- /dev/null
+++ b/src/pages/selfhosted/maintenance/scaling/set-up-external-relays.mdx
@@ -0,0 +1,280 @@
+# 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
+```
+
+
+Replace `relay-us.example.com` with your relay server's domain and `your-shared-secret-here` with the secret you generated.
+
+
+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:
+ issuer: "https://netbird.example.com/oauth2"
+ # ... rest of auth config
+```
+
+
+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.
+
+
+### 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
+```
\ No newline at end of file
diff --git a/src/pages/selfhosted/maintenance/scaling/set-up-external-signal.mdx b/src/pages/selfhosted/maintenance/scaling/set-up-external-signal.mdx
new file mode 100644
index 00000000..125ed0bb
--- /dev/null
+++ b/src/pages/selfhosted/maintenance/scaling/set-up-external-signal.mdx
@@ -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).
+
+
+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:
+
+```bash
+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:
+
+```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: )
+```
\ No newline at end of file
diff --git a/src/pages/selfhosted/maintenance/upgrade.mdx b/src/pages/selfhosted/maintenance/upgrade.mdx
index 1e8d57f3..e64d405f 100644
--- a/src/pages/selfhosted/maintenance/upgrade.mdx
+++ b/src/pages/selfhosted/maintenance/upgrade.mdx
@@ -46,17 +46,29 @@ To upgrade NetBird to the latest version:
2. Review the release notes (see above) for any breaking changes.
3. Pull the latest NetBird docker images:
```bash
- docker compose pull management dashboard signal relay
+ docker compose pull netbird-server dashboard
```
4. Restart the NetBird containers with the new images:
```bash
- docker compose up -d --force-recreate management dashboard signal relay
+ docker compose up -d --force-recreate netbird-server dashboard
```
For upgrades from older versions (pre-v0.26.0), see the [Legacy upgrade notes](#legacy-self-hosting-with-zitadel-idp).
+### Legacy Setup (Separate Containers)
+
+If your deployment uses the older setup with separate containers (`management`, `signal`, `relay`, `coturn`), pull and recreate those containers instead:
+
+```bash
+docker compose pull management dashboard signal relay
+```
+
+```bash
+docker compose up -d --force-recreate management dashboard signal relay
+```
+
## Get In Touch
Feel free to ping us on [Slack](/slack-url) if you have any questions.
diff --git a/src/pages/selfhosted/reverse-proxy.mdx b/src/pages/selfhosted/reverse-proxy.mdx
index d74745a0..abf0fd2b 100644
--- a/src/pages/selfhosted/reverse-proxy.mdx
+++ b/src/pages/selfhosted/reverse-proxy.mdx
@@ -1,6 +1,6 @@
# Reverse Proxy Configuration
-NetBird includes a built-in Caddy reverse proxy that handles TLS certificates automatically. However, if you already have an existing reverse proxy (Traefik, Nginx, etc.), you can configure NetBird to work with it instead.
+NetBird includes a built-in Traefik reverse proxy that handles TLS certificates automatically via Let's Encrypt. However, if you already have an existing reverse proxy (Nginx, Caddy, etc.), you can configure NetBird to work with it instead.
Not all reverse proxies are supported as NetBird uses *gRPC* for various components. Your reverse proxy must support HTTP/2 and gRPC proxying.
@@ -14,8 +14,8 @@ The `getting-started.sh` script supports multiple reverse proxy configurations.
```
Which reverse proxy will you use?
- [0] Built-in Caddy (recommended - automatic TLS)
- [1] Traefik (labels added to containers)
+ [0] Traefik (recommended - automatic TLS, included in Docker Compose)
+ [1] Existing Traefik (labels for external Traefik instance)
[2] Nginx (generates config template)
[3] Nginx Proxy Manager (generates config + instructions)
[4] External Caddy (generates Caddyfile snippet)
diff --git a/src/pages/selfhosted/selfhosted-guide.mdx b/src/pages/selfhosted/selfhosted-guide.mdx
index 05ab4664..8a5195f7 100644
--- a/src/pages/selfhosted/selfhosted-guide.mdx
+++ b/src/pages/selfhosted/selfhosted-guide.mdx
@@ -201,7 +201,7 @@ docker compose logs dashboard
## Advanced: Running NetBird behind an existing reverse-proxy
-If you already have a reverse proxy (Traefik, Nginx, Caddy, etc.), you can configure NetBird to work with it instead of using the built-in Caddy.
+If you already have a reverse proxy (Nginx, Caddy, etc.), you can configure NetBird to work with it instead of using the built-in Traefik.
Not all reverse proxies are supported as NetBird uses *gRPC* for various components. Your reverse proxy must support HTTP/2 and gRPC proxying.
diff --git a/src/pages/selfhosted/selfhosted-quickstart.mdx b/src/pages/selfhosted/selfhosted-quickstart.mdx
index a04b2b1d..d17e504b 100644
--- a/src/pages/selfhosted/selfhosted-quickstart.mdx
+++ b/src/pages/selfhosted/selfhosted-quickstart.mdx
@@ -84,12 +84,14 @@ Rendering initial files...
Starting NetBird services
-[+] Running 4/4
- ✔ Network netbird Created
- ✔ Container netbird-traefik Started
- ✔ Container netbird-dashboard Started
- ✔ Container netbird-server Started
-Waiting for NetBird server to become ready . . done
+[+] up 6/6
+ ✔ Network combined_netbird Created 0.1s
+ ✔ Volume combined_netbird_data Created 0.0s
+ ✔ Volume combined_netbird_traefik_letsencrypt Created 0.0s
+ ✔ Container netbird-server Created 0.1s
+ ✔ Container netbird-traefik Created 0.1s
+ ✔ Container netbird-dashboard Created 0.1s
+Waiting for NetBird server to become ready . . . done
Done!
@@ -157,6 +159,22 @@ Once your NetBird instance is running, refer to these guides for ongoing mainten
]}
/>
+
+
---
## Troubleshoot