diff --git a/src/components/NavigationDocs.jsx b/src/components/NavigationDocs.jsx
index c696ef41c..f8933a317 100644
--- a/src/components/NavigationDocs.jsx
+++ b/src/components/NavigationDocs.jsx
@@ -539,6 +539,45 @@ export const docsNavigation = [
title: 'SELF-HOST NETBIRD',
links: [
{ title: 'Quickstart Guide', href: '/selfhosted/selfhosted-quickstart' },
+ {
+ title: 'Manual Setup',
+ href: '/selfhosted/manual-setup',
+ isOpen: false,
+ links: [
+ {
+ title: 'Plan Your Deployment',
+ href: '/selfhosted/manual-setup/planning',
+ },
+ { title: 'Combined Container', href: '/selfhosted/manual-setup' },
+ {
+ title: 'Multi-Container',
+ href: '/selfhosted/manual-setup/multi-container',
+ },
+ {
+ title: 'Reverse Proxies',
+ href: '/selfhosted/external-reverse-proxy',
+ isOpen: false,
+ links: [
+ {
+ title: 'Traefik',
+ href: '/selfhosted/manual-setup/reverse-proxies/traefik',
+ },
+ {
+ title: 'Nginx',
+ href: '/selfhosted/manual-setup/reverse-proxies/nginx',
+ },
+ {
+ title: 'Caddy',
+ href: '/selfhosted/manual-setup/reverse-proxies/caddy',
+ },
+ {
+ title: 'Nginx Proxy Manager',
+ href: '/selfhosted/manual-setup/reverse-proxies/nginx-proxy-manager',
+ },
+ ],
+ },
+ ],
+ },
{ title: 'Automated Setup', href: '/selfhosted/automated-setup' },
{
title: 'Infrastructure as Code',
diff --git a/src/pages/selfhosted/external-reverse-proxy.mdx b/src/pages/selfhosted/external-reverse-proxy.mdx
index 915ce9af2..b9ec91acb 100644
--- a/src/pages/selfhosted/external-reverse-proxy.mdx
+++ b/src/pages/selfhosted/external-reverse-proxy.mdx
@@ -1,1478 +1,221 @@
-# External Reverse Proxy Setup for Self-Hosted NetBird
+import {Note} from "@/components/mdx"
-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.
+export const description =
+ "Choose and configure Traefik, Nginx, Caddy, or Nginx Proxy Manager for combined-container or multi-container self-hosted NetBird."
-
-This is not to be confused with the NetBird reverse proxy service that launched with v0.65.0. The NetBird reverse proxy feature is only compatible with Traefik because it requires TLS passthrough, which Traefik supports natively. To learn more about using NetBird as a reverse proxy, see the [NetBird Proxy documentation](/manage/reverse-proxy).
-
-
-Not all reverse proxies are supported as NetBird uses *gRPC* for various components. Your reverse proxy must support HTTP/2 and gRPC proxying.
-
-Starting with **v0.65.0**, new NetBird installations use a **combined container** (`netbirdio/netbird-server`) that merges management, signal, and relay into a single service. This simplifies reverse proxy configuration because all traffic routes to one backend. The templates on this page cover both setups:
-
-- **[Combined container (v0.65.0+)](#combined-container-setup-v0-65-0)** -- the default for new installations. All backend services run in a single container on one port.
-- **[Multi-container (legacy)](#multi-container-setup-legacy)** -- for installations that predate v0.65.0, where management, signal, and relay run as separate containers.
-
-## Quick Setup
-
-### New Deployments
-
-The `getting-started.sh` script supports multiple reverse proxy configurations. During initial deployment, you'll be prompted to select your reverse proxy:
-
-```
-Which reverse proxy will you use?
- [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)
- [5] Other/Manual (displays setup documentation)
-```
-
-The script will generate the appropriate configuration files and provide setup instructions for your chosen proxy.
-
-
-This option is only available during initial setup with `getting-started.sh`. For existing deployments, use the manual configuration templates below.
-
-
-### Using the script with an existing Traefik (Option 1)
-
-If you select option `[1]` (Existing Traefik), the script generates a Docker Compose file with Traefik labels for the dashboard and netbird-server containers. It does not run Traefik for you. Your existing Traefik instance handles TLS termination, certificate issuance, and routing.
-
-#### Before you run the script
-
-Make sure these are in place first:
-
-- Traefik is already running on the same host
-- Traefik joins a Docker network you can attach the NetBird containers to
-- Traefik has an HTTPS entrypoint configured (default: `websecure`)
-- Traefik has a working certificate resolver (Let's Encrypt or otherwise)
-- Your domain (e.g. `netbird.example.com`) resolves to the host's public IP
-- Ports 80 and 443 on the host are reachable from the internet
-
-If your cert resolver isn't working before you start, NetBird won't magically fix it. Test that Traefik can issue certs for any other service first.
-
-#### What the script will ask you
+# Reverse proxies for self-hosted NetBird
-When you choose option 1, the script prompts for three things:
+NetBird supports Traefik, Nginx, Caddy, and Nginx Proxy Manager in front of a manual self-hosted deployment. Each proxy must route Dashboard HTTP, REST/OIDC, WebSockets, and native gRPC on one public TLS hostname while leaving STUN on UDP 3478 directly reachable.
-| Prompt | What to enter |
-|--------|---------------|
-| External network | The Docker network name your Traefik is on (e.g. `proxy`) |
-| HTTPS entrypoint name | Your Traefik HTTPS entrypoint (default: `websecure`) |
-| Certificate resolver name | Your cert resolver name (e.g. `letsencrypt`). Leave empty if you handle TLS some other way |
+For most installations, the recommended manual path is the [combined server with its embedded identity provider and Traefik](/selfhosted/manual-setup). Traefik handles automatic certificate issuance and renewal, so you do not need to choose another proxy before NetBird can run.
-#### What the script does
+Use this page when a reverse proxy is an integration choice: you already operate another proxy, need a specific certificate workflow, or use the multi-container layout. For the broader architecture decision, start with [Plan a manual NetBird deployment](/selfhosted/manual-setup/planning).
-After you answer the prompts, the script writes a `docker-compose.yml` with the right Traefik labels, brings up the dashboard and netbird-server containers, and waits for the server to become reachable through your Traefik instance.
+## Choose the NetBird layout
-The readiness check probes `https://your-domain/oauth2/.well-known/openid-configuration` through the proxy. The script auto-detects your Traefik container (any container running a Traefik image with ports 80 and 443 published) so it can pull diagnostic logs if something goes wrong.
+| Layout | Services | Use it when |
+|---|---|---|
+| **Combined container** | `dashboard` and `netbird-server` | Recommended for most setups because it keeps the core services together. |
+| **Multi-container** | `dashboard`, `management`, `signal`, and `relay` | Separate service lifecycles, per-service resource controls, or infrastructure that benefits from distinct backends. |
-If the wait check hangs, see [Installation script issues](/selfhosted/troubleshooting/installation) for the common causes.
+- [Manual combined-container setup](/selfhosted/manual-setup)
+- [Manual multi-container setup](/selfhosted/manual-setup/multi-container)
+- [Migrate a multi-container deployment to the combined container](/selfhosted/migration/combined-container)
-### Existing Deployments
+The combined layout is recommended for most setups because it has fewer moving parts. The multi-container layout remains a valid option when separating Management, Signal, and Relay better fits your operational requirements.
-For existing NetBird installations, use the configuration templates in the sections below to manually configure your reverse proxy.
+## Choose the reverse proxy
----
+| Proxy | TLS | Best fit | Guide |
+|---|---|---|---|
+| **Traefik** | Automatic ACME | Docker-native routing and the standard NetBird quickstart model. | [Traefik](/selfhosted/manual-setup/reverse-proxies/traefik) |
+| **Nginx** | Certbot or an existing certificate | Host-based deployments and administrators who want explicit location routing. | [Nginx](/selfhosted/manual-setup/reverse-proxies/nginx) |
+| **Caddy** | Automatic HTTPS | Small, readable configurations with automatic certificate lifecycle. | [Caddy](/selfhosted/manual-setup/reverse-proxies/caddy) |
+| **Nginx Proxy Manager** | Managed in its UI | Administrators who already operate NPM and prefer a web interface. | [Nginx Proxy Manager](/selfhosted/manual-setup/reverse-proxies/nginx-proxy-manager) |
-## Combined Container Setup (v0.65.0+)
+Every guide includes:
-Starting with v0.65.0, new NetBird deployments use the `netbirdio/netbird-server` image, which combines management, signal, and relay into a single container. All backend traffic routes to one service on a single port, which simplifies reverse proxy configuration significantly.
+- Combined-container and multi-container routing.
+- Host or Docker placement where the proxy supports it.
+- Trusted proxy configuration.
+- TLS and long-lived connection settings.
+- Public verification commands.
+- Proxy-specific troubleshooting.
-
-UDP port **3478** (STUN) must be publicly accessible and **cannot** be proxied through your reverse proxy. HTTP reverse proxies only handle TCP traffic; STUN requires direct UDP access. Ensure your firewall allows inbound UDP on port 3478 and that this port is published directly from the container (e.g. `3478:3478/udp`), bypassing the reverse proxy entirely.
-
+## Public port requirements
-### Required Routing Endpoints (Combined)
+| Port | Protocol | Destination | Purpose |
+|---|---|---|---|
+| `80` | TCP | Reverse proxy | HTTP-to-HTTPS redirect and ACME HTTP validation, when used. |
+| `443` | TCP | Reverse proxy | Dashboard, API, embedded IdP, Management, Signal, and secure Relay. |
+| `3478` | UDP | `netbird-server` or multi-container `relay` | STUN for NAT discovery. It bypasses the HTTP reverse proxy. |
-All reverse proxy configurations for the combined container must route the following endpoints to the single `netbird-server` container:
-
-| Path | Protocol | Target | Notes |
-|------|----------|--------|-------|
-| `/relay*` | WebSocket | netbird-server:80 | WebSocket upgrade required |
-| `/ws-proxy/signal*` | WebSocket | netbird-server:80 | WebSocket upgrade required |
-| `/ws-proxy/management*` | WebSocket | netbird-server:80 | WebSocket upgrade required |
-| `/signalexchange.SignalExchange/*` | gRPC | netbird-server:80 | HTTP/2 (h2c) required |
-| `/management.ManagementService/*` | gRPC | netbird-server:80 | HTTP/2 (h2c) required |
-| `/management.ProxyService/*` | gRPC | netbird-server:80 | HTTP/2 (h2c) required. Only needed if using the [Reverse Proxy feature](/manage/reverse-proxy). |
-| `/api/*` | HTTP | netbird-server:80 | REST API |
-| `/oauth2/*` | HTTP | netbird-server:80 | Embedded IdP |
-| `/*` | HTTP | dashboard:80 | Catch-all for dashboard |
+Do not publish Dashboard, Management, Signal HTTP/gRPC, or Relay HTTP backend ports on all interfaces. A host-installed proxy should reach them through `127.0.0.1` bindings; a Docker proxy should reach container names on a private network.
-The combined container includes an embedded STUN server that still needs to be directly accessible on UDP port 3478.
+A CDN or ordinary HTTP proxy in front of the NetBird hostname can break gRPC, WebSockets, source-address handling, or long-lived streams. Configure the NetBird hostname as DNS-only unless the upstream product is explicitly configured and tested for all of those protocols.
-### Docker Compose for External Proxy (Combined)
-
-When using an external reverse proxy with the combined container, expose the following ports:
+## Combined-container route map
-```yaml
-services:
- dashboard:
- image: netbirdio/dashboard:latest
- ports:
- - '127.0.0.1:8080:80'
- # ... other config
+All backend routes go to `netbird-server:80`; only the Dashboard catch-all uses `dashboard:80`.
- netbird-server:
- image: netbirdio/netbird-server:latest
- ports:
- - '127.0.0.1:8081:80'
- - '3478:3478/udp'
- volumes:
- - netbird_data:/var/lib/netbird
- - ./config.yaml:/etc/netbird/config.yaml
- command: ["--config", "/etc/netbird/config.yaml"]
- # ... other config
-```
+| Public path | Protocol | Upstream |
+|---|---|---|
+| `/relay*` | WebSocket | `netbird-server:80` |
+| `/ws-proxy/signal*` | WebSocket | `netbird-server:80` |
+| `/ws-proxy/management*` | WebSocket | `netbird-server:80` |
+| `/signalexchange.SignalExchange/*` | Native gRPC over h2c | `netbird-server:80` |
+| `/management.ManagementService/*` | Native gRPC over h2c | `netbird-server:80` |
+| `/management.ProxyService/*` | Native gRPC over h2c | `netbird-server:80` |
+| `/api/*` | HTTP | `netbird-server:80` |
+| `/oauth2/*` | HTTP | `netbird-server:80` |
+| All remaining paths | HTTP | `dashboard:80` |
-
-Binding to `127.0.0.1` is recommended when your reverse proxy runs on the same host. This prevents direct access to the containers and ensures all traffic goes through the proxy.
-
+`/management.ProxyService/*` is used by the optional [NetBird Reverse Proxy feature](/manage/reverse-proxy). Routing it now avoids another proxy change if that feature is enabled later.
-#### Container Port Reference (Combined)
+## Multi-container route map
-| Service | Host Port | Container Port | Protocol |
-|---------|-----------|----------------|----------|
-| Dashboard | 8080 | 80 | HTTP |
-| NetBird Server | 8081 | 80 | HTTP/gRPC/WebSocket |
-| STUN (UDP) | 3478 | 3478 | UDP |
+| Public path | Protocol | Upstream |
+|---|---|---|
+| `/relay*` | WebSocket | `relay:80` |
+| `/ws-proxy/signal*` | WebSocket | `signal:80` |
+| `/signalexchange.SignalExchange/*` | Native gRPC over h2c | `signal:10000` |
+| `/api/*` | HTTP | `management:80` |
+| `/oauth2/*` | HTTP | `management:80` |
+| `/ws-proxy/management*` | WebSocket | `management:80` |
+| `/management.ManagementService/*` | Native gRPC over h2c | `management:80` |
+| `/management.ProxyService/*` | Native gRPC over h2c | `management:80` |
+| All remaining paths | HTTP | `dashboard:80` |
----
+## Trust only the real proxy
-### Configuration Templates (Combined)
+NetBird uses two trust settings:
-#### Traefik (Combined)
+- `trustedHTTPProxies` controls which sources may supply forwarded HTTP client information.
+- `trustedPeers` controls trusted peer/proxy sources used by proxied protocol handling.
-For Traefik with the combined container, you only need two routers: one for gRPC (which requires an `h2c` backend) and one for everything else (WebSocket, REST API, OAuth2). The dashboard uses a separate low-priority catch-all router.
-
-Replace `netbird.example.com` with your domain, `websecure` with your HTTPS entrypoint, and `letsencrypt` with your certificate resolver name.
+For the combined YAML configuration:
```yaml
-services:
- dashboard:
- image: netbirdio/dashboard:latest
- networks: [traefik-network]
- labels:
- - traefik.enable=true
- - traefik.http.routers.netbird-dashboard.rule=Host(`netbird.example.com`)
- - traefik.http.routers.netbird-dashboard.entrypoints=websecure
- - traefik.http.routers.netbird-dashboard.tls=true
- - traefik.http.routers.netbird-dashboard.tls.certresolver=letsencrypt
- - traefik.http.routers.netbird-dashboard.priority=1
- - traefik.http.services.netbird-dashboard.loadbalancer.server.port=80
-
- netbird-server:
- image: netbirdio/netbird-server:latest
- networks: [traefik-network]
- ports:
- - '3478:3478/udp'
- volumes:
- - netbird_data:/var/lib/netbird
- - ./config.yaml:/etc/netbird/config.yaml
- command: ["--config", "/etc/netbird/config.yaml"]
- labels:
- - traefik.enable=true
- # gRPC router (needs h2c backend for HTTP/2 cleartext)
- - traefik.http.routers.netbird-grpc.rule=Host(`netbird.example.com`) && (PathPrefix(`/signalexchange.SignalExchange/`) || PathPrefix(`/management.ManagementService/`) || PathPrefix(`/management.ProxyService/`))
- - traefik.http.routers.netbird-grpc.entrypoints=websecure
- - traefik.http.routers.netbird-grpc.tls=true
- - traefik.http.routers.netbird-grpc.tls.certresolver=letsencrypt
- - traefik.http.routers.netbird-grpc.service=netbird-server-h2c
- - traefik.http.routers.netbird-grpc.priority=100
- # Backend router (relay, WebSocket, API, OAuth2)
- - traefik.http.routers.netbird-backend.rule=Host(`netbird.example.com`) && (PathPrefix(`/relay`) || PathPrefix(`/ws-proxy/`) || PathPrefix(`/api`) || PathPrefix(`/oauth2`))
- - traefik.http.routers.netbird-backend.entrypoints=websecure
- - traefik.http.routers.netbird-backend.tls=true
- - traefik.http.routers.netbird-backend.tls.certresolver=letsencrypt
- - traefik.http.routers.netbird-backend.service=netbird-server
- - traefik.http.routers.netbird-backend.priority=100
- # Services
- - traefik.http.services.netbird-server.loadbalancer.server.port=80
- - traefik.http.services.netbird-server-h2c.loadbalancer.server.port=80
- - traefik.http.services.netbird-server-h2c.loadbalancer.server.scheme=h2c
-
-networks:
- traefik-network:
- external: true
-
-volumes:
- netbird_data:
-```
-
-
-The combined container requires two Traefik service definitions pointing to the same port: `netbird-server` for standard HTTP/WebSocket traffic and `netbird-server-h2c` with `scheme=h2c` for gRPC traffic.
-
-
----
-
-#### Nginx (Combined)
-
-##### Nginx running in Docker (Combined)
-
-If Nginx runs in Docker on the same network as the NetBird containers, use the container names directly:
-
-```nginx
-# NetBird Nginx Configuration (Combined Container - Docker network)
-
-upstream netbird_dashboard {
- server netbird-dashboard:80;
- keepalive 10;
-}
-upstream netbird_server {
- server netbird-server:80;
-}
-
-server {
- listen 80;
- server_name netbird.example.com;
- location / {
- return 301 https://$host$request_uri;
- }
-}
-
-server {
- listen 443 ssl http2;
- server_name netbird.example.com;
-
- ssl_certificate /path/to/fullchain.pem;
- ssl_certificate_key /path/to/privkey.pem;
-
- # Required for long-lived gRPC connections
- client_header_timeout 1d;
- client_body_timeout 1d;
-
- # Common proxy headers
- 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;
-
- # WebSocket connections (relay, signal, management)
- location ~ ^/(relay|ws-proxy/) {
- proxy_pass http://netbird_server;
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "Upgrade";
- proxy_set_header Host $host;
- proxy_read_timeout 1d;
- }
-
- # Native gRPC (signal + management)
- location ~ ^/(signalexchange\.SignalExchange|management\.(ManagementService|ProxyService))/ {
- grpc_pass grpc://netbird_server;
- grpc_read_timeout 1d;
- grpc_send_timeout 1d;
- grpc_socket_keepalive on;
- }
-
- # HTTP routes (API + OAuth2)
- location ~ ^/(api|oauth2)/ {
- proxy_pass http://netbird_server;
- proxy_set_header Host $host;
- }
-
- # Dashboard (catch-all)
- location / {
- proxy_pass http://netbird_dashboard;
- }
-}
-```
-
-##### Nginx running on host (Combined)
-
-If Nginx is installed directly on the host, use localhost addresses:
-
-```nginx
-# NetBird Nginx Configuration (Combined Container - Host)
-
-upstream netbird_dashboard {
- server 127.0.0.1:8080;
- keepalive 10;
-}
-upstream netbird_server {
- server 127.0.0.1:8081;
-}
-
-server {
- listen 80;
- server_name netbird.example.com;
- location / {
- return 301 https://$host$request_uri;
- }
-}
-
-server {
- listen 443 ssl http2;
- server_name netbird.example.com;
-
- ssl_certificate /path/to/fullchain.pem;
- ssl_certificate_key /path/to/privkey.pem;
-
- # Required for long-lived gRPC connections
- client_header_timeout 1d;
- client_body_timeout 1d;
-
- # Common proxy headers
- 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;
-
- # WebSocket connections (relay, signal, management)
- location ~ ^/(relay|ws-proxy/) {
- proxy_pass http://netbird_server;
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "Upgrade";
- proxy_set_header Host $host;
- proxy_read_timeout 1d;
- }
-
- # Native gRPC (signal + management)
- location ~ ^/(signalexchange\.SignalExchange|management\.(ManagementService|ProxyService))/ {
- grpc_pass grpc://netbird_server;
- grpc_read_timeout 1d;
- grpc_send_timeout 1d;
- grpc_socket_keepalive on;
- }
-
- # HTTP routes (API + OAuth2)
- location ~ ^/(api|oauth2)/ {
- proxy_pass http://netbird_server;
- proxy_set_header Host $host;
- }
-
- # Dashboard (catch-all)
- location / {
- proxy_pass http://netbird_dashboard;
- }
-}
-```
-
----
-
-#### Caddy (Combined)
-
-##### Caddy running in Docker (Combined)
-
-If Caddy runs in Docker on the same network as NetBird, use container names:
-
-```
-netbird.example.com {
- # Native gRPC (needs HTTP/2 cleartext to backend)
- @grpc header Content-Type application/grpc*
- reverse_proxy @grpc h2c://netbird-server:80
-
- # Combined server paths (relay, signal, management, OAuth2)
- @backend path /relay* /ws-proxy/* /api/* /oauth2/*
- reverse_proxy @backend netbird-server:80
-
- # Dashboard (everything else)
- reverse_proxy /* netbird-dashboard:80
-}
-```
-
-##### Caddy running on host (Combined)
-
-If Caddy is installed directly on the host, use localhost addresses:
-
-```
-netbird.example.com {
- # Native gRPC (needs HTTP/2 cleartext to backend)
- @grpc header Content-Type application/grpc*
- reverse_proxy @grpc h2c://127.0.0.1:8081
-
- # Combined server paths (relay, signal, management, OAuth2)
- @backend path /relay* /ws-proxy/* /api/* /oauth2/*
- reverse_proxy @backend 127.0.0.1:8081
-
- # Dashboard (everything else)
- reverse_proxy /* 127.0.0.1:8080
-}
+server:
+ reverseProxy:
+ trustedHTTPProxies:
+ - "172.30.0.10/32"
+ trustedPeers:
+ - "172.30.0.10/32"
```
----
-
-#### Nginx Proxy Manager (Combined)
-
-With the combined container, NPM configuration is simpler because all backend traffic routes to the same service.
-
-
-NPM requires backend services to be running before you can create proxy hosts. Start the NetBird containers first, then configure NPM.
-
-
-##### NPM running in Docker (Combined)
-
-**1. Create a Proxy Host in NPM:**
-- Domain: `netbird.example.com`
-- Forward Hostname/IP: `netbird-dashboard`
-- Forward Port: `80`
-- Block Common Exploits: enabled
-
-**2. SSL tab:**
-- Request or select existing certificate
-- **Enable "HTTP/2 Support"** (required for gRPC)
+For multi-container `management.json`:
-**3. Advanced tab - paste this configuration:**
-
-```nginx
-# Required for long-lived connections (gRPC and WebSocket)
-client_header_timeout 1d;
-client_body_timeout 1d;
-
-# WebSocket connections (relay, signal, management)
-location ~ ^/(relay|ws-proxy/) {
- proxy_pass http://netbird-server:80;
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "upgrade";
- 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_read_timeout 1d;
-}
-
-# Native gRPC (signal + management)
-location ~ ^/(signalexchange\.SignalExchange|management\.(ManagementService|ProxyService))/ {
- grpc_pass grpc://netbird-server:80;
- grpc_read_timeout 1d;
- grpc_send_timeout 1d;
- grpc_socket_keepalive on;
-}
-
-# HTTP routes (API + OAuth2)
-location ~ ^/(api|oauth2)/ {
- proxy_pass http://netbird-server:80;
- 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;
+```json
+{
+ "ReverseProxy": {
+ "TrustedHTTPProxies": ["172.30.0.10/32"],
+ "TrustedHTTPProxiesCount": 0,
+ "TrustedPeers": ["172.30.0.10/32"]
+ }
}
```
-##### NPM running on host (Combined)
+The address depends on placement:
-**1. Create a Proxy Host in NPM:**
-- Domain: `netbird.example.com`
-- Forward Hostname/IP: `127.0.0.1`
-- Forward Port: `8080`
-- Block Common Exploits: enabled
+| Placement in these manual guides | Address to trust |
+|---|---|
+| Proxy container with fixed address | `172.30.0.10/32` |
+| Proxy installed on the Docker host and using published backend ports | `172.30.0.1/32` |
-**2. SSL tab:**
-- Request or select existing certificate
-- **Enable "HTTP/2 Support"** (required for gRPC)
+If you use a different network, inspect the real source address instead of copying these values. Never use `0.0.0.0/0` merely to make forwarding work.
-**3. Advanced tab - paste this configuration:**
+## Required proxy behavior
-```nginx
-# Required for long-lived connections (gRPC and WebSocket)
-client_header_timeout 1d;
-client_body_timeout 1d;
+A valid configuration must preserve all of these properties:
-# WebSocket connections (relay, signal, management)
-location ~ ^/(relay|ws-proxy/) {
- proxy_pass http://127.0.0.1:8081;
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "upgrade";
- 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_read_timeout 1d;
-}
+1. **Trusted public TLS:** the certificate chain and hostname must validate normally.
+2. **HTTP/2 from clients:** native gRPC requires HTTP/2 on public TCP 443.
+3. **h2c to native gRPC backends:** TLS ends at the proxy; the internal gRPC hop is cleartext HTTP/2.
+4. **WebSocket upgrade:** Relay and fallback Signal/Management paths require HTTP/1.1 upgrade semantics.
+5. **Long timeouts:** Signal and Relay connections are intentionally long-lived.
+6. **Forwarding headers:** send the original host, scheme, and client chain only to a NetBird service configured to trust that proxy.
+7. **Dashboard last:** the catch-all route must have the lowest priority.
+8. **Direct UDP 3478:** do not attempt to send STUN through the HTTP reverse proxy.
-# Native gRPC (signal + management)
-location ~ ^/(signalexchange\.SignalExchange|management\.(ManagementService|ProxyService))/ {
- grpc_pass grpc://127.0.0.1:8081;
- grpc_read_timeout 1d;
- grpc_send_timeout 1d;
- grpc_socket_keepalive on;
-}
-
-# HTTP routes (API + OAuth2)
-location ~ ^/(api|oauth2)/ {
- proxy_pass http://127.0.0.1:8081;
- 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;
-}
-```
-
----
-
-## Multi-Container Setup (Legacy)
-
-The following sections apply to NetBird installations from before v0.65.0 that use separate containers for management, signal, and relay. If you are running a new installation, use the [combined container templates above](#combined-container-setup-v0650).
-
-
-UDP port **3478** (STUN/TURN via coturn) must be publicly accessible and **cannot** be proxied through your reverse proxy. HTTP reverse proxies only handle TCP traffic; STUN/TURN requires direct UDP access. Ensure your firewall allows inbound UDP on port 3478 and that the coturn container publishes this port directly, bypassing the reverse proxy entirely.
-
-
-### Required Routing Endpoints (Multi-Container)
-
-All reverse proxy configurations must route the following endpoints:
-
-| Path | Protocol | Target | Notes |
-|------|----------|--------|-------|
-| `/relay*` | WebSocket | relay:80 | WebSocket upgrade required |
-| `/ws-proxy/signal*` | WebSocket | signal:80 | WebSocket upgrade required |
-| `/signalexchange.SignalExchange/*` | gRPC | signal:10000 | HTTP/2 (h2c) required |
-| `/api/*` | HTTP | management:80 | REST API |
-| `/ws-proxy/management*` | WebSocket | management:80 | WebSocket upgrade required |
-| `/management.ManagementService/*` | gRPC | management:80 | HTTP/2 (h2c) required |
-| `/management.ProxyService/*` | gRPC | management:80 | HTTP/2 (h2c) required. Only needed if using the [Reverse Proxy feature](/manage/reverse-proxy). |
-| `/oauth2/*` | HTTP | management:80 | Embedded IdP |
-| `/*` | HTTP | dashboard:80 | Catch-all for dashboard |
-
-
-The `coturn` service still needs to be directly accessible on UDP port 3478 as it handles STUN/TURN traffic.
-
-
-### Docker Compose for External Proxy (Multi-Container)
-
-When using an external reverse proxy, expose the NetBird container ports to the host:
-
-```yaml
-services:
- dashboard:
- image: netbirdio/dashboard:latest
- ports:
- - '127.0.0.1:8080:80'
- # ... other config
-
- signal:
- image: netbirdio/signal:latest
- ports:
- - '127.0.0.1:8083:80'
- - '127.0.0.1:10000:10000'
- # ... other config
-
- relay:
- image: netbirdio/relay:latest
- ports:
- - '127.0.0.1:8084:80'
- # ... other config
-
- management:
- image: netbirdio/management:latest
- ports:
- - '127.0.0.1:8081:80'
- # ... other config
-```
-
-
-Binding to `127.0.0.1` is recommended when your reverse proxy runs on the same host. This prevents direct access to the containers and ensures all traffic goes through the proxy.
-
-
-#### Container Port Reference (Multi-Container)
-
-| Service | Host Port | Container Port | Protocol |
-|---------|-----------|----------------|----------|
-| Dashboard | 8080 | 80 | HTTP |
-| Signal (HTTP) | 8083 | 80 | HTTP/WebSocket |
-| Signal (gRPC) | 10000 | 10000 | gRPC (h2c) |
-| Management | 8081 | 80 | HTTP/gRPC |
-| Relay | 8084 | 80 | WebSocket |
-
----
-
-### Configuration Templates (Multi-Container)
-
-#### Traefik (Multi-Container)
-
-For Traefik, NetBird containers are configured with Docker labels for automatic service discovery. The `getting-started.sh` script will prompt for your Traefik configuration:
-
-- **External network**: The Docker network your Traefik container uses
-- **HTTPS entrypoint**: Your Traefik entrypoint for HTTPS traffic (commonly `websecure`)
-- **Certificate resolver**: Your Traefik certresolver for automatic TLS (e.g., `letsencrypt`)
-
-##### Complete Docker Compose with Traefik Labels
-
-Add the following labels to your NetBird services in `docker-compose.yml`. Replace:
-- `netbird.example.com` with your domain
-- `websecure` with your HTTPS entrypoint name
-- `letsencrypt` with your certificate resolver name (or remove the certresolver lines if handling TLS externally)
-
-```yaml
-services:
- dashboard:
- image: netbirdio/dashboard:latest
- networks: [traefik-network]
- labels:
- - traefik.enable=true
- - traefik.http.routers.netbird-dashboard.rule=Host(`netbird.example.com`)
- - traefik.http.routers.netbird-dashboard.entrypoints=websecure
- - traefik.http.routers.netbird-dashboard.tls=true
- - traefik.http.routers.netbird-dashboard.tls.certresolver=letsencrypt
- - traefik.http.routers.netbird-dashboard.priority=1
- - traefik.http.services.netbird-dashboard.loadbalancer.server.port=80
-
- signal:
- image: netbirdio/signal:latest
- networks: [traefik-network]
- labels:
- - traefik.enable=true
- # WebSocket router
- - traefik.http.routers.netbird-signal-ws.rule=Host(`netbird.example.com`) && PathPrefix(`/ws-proxy/signal`)
- - traefik.http.routers.netbird-signal-ws.entrypoints=websecure
- - traefik.http.routers.netbird-signal-ws.tls=true
- - traefik.http.routers.netbird-signal-ws.tls.certresolver=letsencrypt
- - traefik.http.routers.netbird-signal-ws.service=netbird-signal-ws
- - traefik.http.services.netbird-signal-ws.loadbalancer.server.port=80
- # gRPC router
- - traefik.http.routers.netbird-signal-grpc.rule=Host(`netbird.example.com`) && PathPrefix(`/signalexchange.SignalExchange/`)
- - traefik.http.routers.netbird-signal-grpc.entrypoints=websecure
- - traefik.http.routers.netbird-signal-grpc.tls=true
- - traefik.http.routers.netbird-signal-grpc.tls.certresolver=letsencrypt
- - traefik.http.routers.netbird-signal-grpc.service=netbird-signal-grpc
- - traefik.http.services.netbird-signal-grpc.loadbalancer.server.port=10000
- - traefik.http.services.netbird-signal-grpc.loadbalancer.server.scheme=h2c
-
- relay:
- image: netbirdio/relay:latest
- networks: [traefik-network]
- labels:
- - traefik.enable=true
- - traefik.http.routers.netbird-relay.rule=Host(`netbird.example.com`) && PathPrefix(`/relay`)
- - traefik.http.routers.netbird-relay.entrypoints=websecure
- - traefik.http.routers.netbird-relay.tls=true
- - traefik.http.routers.netbird-relay.tls.certresolver=letsencrypt
- - traefik.http.services.netbird-relay.loadbalancer.server.port=80
-
- management:
- image: netbirdio/management:latest
- networks: [traefik-network]
- labels:
- - traefik.enable=true
- # API router
- - traefik.http.routers.netbird-api.rule=Host(`netbird.example.com`) && PathPrefix(`/api`)
- - traefik.http.routers.netbird-api.entrypoints=websecure
- - traefik.http.routers.netbird-api.tls=true
- - traefik.http.routers.netbird-api.tls.certresolver=letsencrypt
- - traefik.http.routers.netbird-api.service=netbird-api
- - traefik.http.services.netbird-api.loadbalancer.server.port=80
- # Management WebSocket router
- - traefik.http.routers.netbird-mgmt-ws.rule=Host(`netbird.example.com`) && PathPrefix(`/ws-proxy/management`)
- - traefik.http.routers.netbird-mgmt-ws.entrypoints=websecure
- - traefik.http.routers.netbird-mgmt-ws.tls=true
- - traefik.http.routers.netbird-mgmt-ws.tls.certresolver=letsencrypt
- - traefik.http.routers.netbird-mgmt-ws.service=netbird-mgmt-ws
- - traefik.http.services.netbird-mgmt-ws.loadbalancer.server.port=80
- # Management gRPC router
- - traefik.http.routers.netbird-mgmt-grpc.rule=Host(`netbird.example.com`) && (PathPrefix(`/management.ManagementService/`) || PathPrefix(`/management.ProxyService/`))
- - traefik.http.routers.netbird-mgmt-grpc.entrypoints=websecure
- - traefik.http.routers.netbird-mgmt-grpc.tls=true
- - traefik.http.routers.netbird-mgmt-grpc.tls.certresolver=letsencrypt
- - traefik.http.routers.netbird-mgmt-grpc.service=netbird-mgmt-grpc
- - traefik.http.services.netbird-mgmt-grpc.loadbalancer.server.port=80
- - traefik.http.services.netbird-mgmt-grpc.loadbalancer.server.scheme=h2c
- # OAuth2 router (embedded IdP)
- - traefik.http.routers.netbird-oauth2.rule=Host(`netbird.example.com`) && PathPrefix(`/oauth2`)
- - traefik.http.routers.netbird-oauth2.entrypoints=websecure
- - traefik.http.routers.netbird-oauth2.tls=true
- - traefik.http.routers.netbird-oauth2.tls.certresolver=letsencrypt
- - traefik.http.routers.netbird-oauth2.service=netbird-oauth2
- - traefik.http.services.netbird-oauth2.loadbalancer.server.port=80
-
-networks:
- traefik-network:
- external: true
-```
-
-##### Traefik Configuration Requirements
-
-Your Traefik instance must have the following configured:
-
-**1. HTTPS Entrypoint** (listening on port 443):
-```yaml
-# In traefik.yml or as command args
-entryPoints:
- websecure:
- address: ":443"
-```
-
-**2. Certificate Resolver** (for automatic Let's Encrypt certificates):
-```yaml
-certificatesResolvers:
- letsencrypt:
- acme:
- email: your-email@example.com
- storage: /letsencrypt/acme.json
- httpChallenge:
- entryPoint: web
-```
-
-**3. Docker Provider** with exposedByDefault disabled:
-```yaml
-providers:
- docker:
- exposedByDefault: false
-```
-
-**4. HTTP to HTTPS Redirect** (recommended):
-```yaml
-entryPoints:
- web:
- address: ":80"
- http:
- redirections:
- entryPoint:
- to: websecure
- scheme: https
-```
-
-##### Example Traefik Docker Compose
-
-If you don't have Traefik set up yet, here's a complete example:
-
-```yaml
-services:
- traefik:
- image: traefik:v3.2
- command:
- - "--api.insecure=true"
- - "--providers.docker=true"
- - "--providers.docker.exposedByDefault=false"
- - "--entrypoints.web.address=:80"
- - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- - "--entrypoints.web.http.redirections.entrypoint.scheme=https"
- - "--entrypoints.websecure.address=:443"
- - "--certificatesresolvers.letsencrypt.acme.email=your-email@example.com"
- - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
- - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
- ports:
- - "80:80"
- - "443:443"
- - "8080:8080" # Traefik dashboard (optional)
- volumes:
- - /var/run/docker.sock:/var/run/docker.sock:ro
- - ./letsencrypt:/letsencrypt
- networks:
- - traefik-network
-
-networks:
- traefik-network:
- name: traefik-network
-```
-
-
-When using `getting-started.sh` with Traefik, the script will ask for your network name, entrypoint, and certresolver. It will then generate a docker-compose.yml with all the correct labels pre-configured.
-
+## Verify before creating the first owner
----
+Run tests from a system outside the server network:
-#### Nginx (Multi-Container)
-
-##### Nginx running in Docker (Multi-Container)
-
-If Nginx is running in Docker, the easiest approach is to have NetBird join the same Docker network. The `getting-started.sh` script will ask for your Nginx network name and configure everything automatically.
-
-```nginx
-# NetBird Nginx Configuration (Docker network)
-# Uses container names for upstream servers
-
-upstream netbird_dashboard {
- server netbird-dashboard:80;
- keepalive 10;
-}
-upstream netbird_signal {
- server netbird-signal:10000;
-}
-upstream netbird_signal_ws {
- server netbird-signal:80;
-}
-upstream netbird_management {
- server netbird-management:80;
-}
-upstream netbird_relay {
- server netbird-relay:80;
-}
-
-server {
- listen 80;
- server_name netbird.example.com;
- location / {
- return 301 https://$host$request_uri;
- }
-}
-
-server {
- listen 443 ssl http2;
- server_name netbird.example.com;
-
- ssl_certificate /path/to/fullchain.pem;
- ssl_certificate_key /path/to/privkey.pem;
-
- # Required for long-lived gRPC connections
- client_header_timeout 1d;
- client_body_timeout 1d;
-
- # Common proxy headers
- 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;
-
- # Relay (WebSocket)
- location /relay {
- proxy_pass http://netbird_relay;
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "Upgrade";
- proxy_set_header Host $host;
- proxy_read_timeout 1d;
- }
-
- # Signal WebSocket
- location /ws-proxy/signal {
- proxy_pass http://netbird_signal_ws;
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "Upgrade";
- proxy_set_header Host $host;
- proxy_read_timeout 1d;
- }
-
- # Signal gRPC
- location /signalexchange.SignalExchange/ {
- grpc_pass grpc://netbird_signal;
- grpc_read_timeout 1d;
- grpc_send_timeout 1d;
- grpc_socket_keepalive on;
- }
-
- # Management API
- location /api/ {
- proxy_pass http://netbird_management;
- proxy_set_header Host $host;
- }
-
- # Management WebSocket
- location /ws-proxy/management {
- proxy_pass http://netbird_management;
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "Upgrade";
- proxy_set_header Host $host;
- proxy_read_timeout 1d;
- }
-
- # Management gRPC
- location /management.ManagementService/ {
- grpc_pass grpc://netbird_management;
- grpc_read_timeout 1d;
- grpc_send_timeout 1d;
- grpc_socket_keepalive on;
- }
-
- # Proxy gRPC (only needed if using the Reverse Proxy feature)
- location /management.ProxyService/ {
- grpc_pass grpc://netbird_management;
- grpc_read_timeout 1d;
- grpc_send_timeout 1d;
- grpc_socket_keepalive on;
- }
-
- # Embedded IdP OAuth2
- location /oauth2/ {
- proxy_pass http://netbird_management;
- proxy_set_header Host $host;
- }
-
- # Dashboard (catch-all)
- location / {
- proxy_pass http://netbird_dashboard;
- }
-}
-```
-
-##### Nginx running on host (Multi-Container)
-
-If Nginx is installed directly on the host, use localhost addresses:
-
-```nginx
-# NetBird Nginx Configuration
-# Replace netbird.example.com with your domain
-# Update SSL certificate paths
-
-upstream netbird_dashboard {
- server 127.0.0.1:8080;
- keepalive 10;
-}
-upstream netbird_signal {
- server 127.0.0.1:10000;
-}
-upstream netbird_signal_ws {
- server 127.0.0.1:8083;
-}
-upstream netbird_management {
- server 127.0.0.1:8081;
-}
-upstream netbird_relay {
- server 127.0.0.1:8084;
-}
-
-server {
- listen 80;
- listen [::]:80;
- server_name netbird.example.com;
- location / {
- return 301 https://$host$request_uri;
- }
-}
-
-server {
- listen 443 ssl http2;
- listen [::]:443 ssl http2;
- server_name netbird.example.com;
-
- ssl_certificate /path/to/fullchain.pem;
- ssl_certificate_key /path/to/privkey.pem;
-
- # Required for long-lived gRPC connections
- client_header_timeout 1d;
- client_body_timeout 1d;
-
- # Common proxy headers
- 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;
-
- # Relay (WebSocket)
- location /relay {
- proxy_pass http://netbird_relay;
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "Upgrade";
- proxy_set_header Host $host;
- proxy_read_timeout 1d;
- }
-
- # Signal WebSocket
- location /ws-proxy/signal {
- proxy_pass http://netbird_signal_ws;
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "Upgrade";
- proxy_set_header Host $host;
- proxy_read_timeout 1d;
- }
-
- # Signal gRPC
- location /signalexchange.SignalExchange/ {
- grpc_pass grpc://netbird_signal;
- grpc_read_timeout 1d;
- grpc_send_timeout 1d;
- grpc_socket_keepalive on;
- }
-
- # Management API
- location /api/ {
- proxy_pass http://netbird_management;
- proxy_set_header Host $host;
- }
-
- # Management WebSocket
- location /ws-proxy/management {
- proxy_pass http://netbird_management;
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "Upgrade";
- proxy_set_header Host $host;
- proxy_read_timeout 1d;
- }
-
- # Management gRPC
- location /management.ManagementService/ {
- grpc_pass grpc://netbird_management;
- grpc_read_timeout 1d;
- grpc_send_timeout 1d;
- grpc_socket_keepalive on;
- }
-
- # Proxy gRPC (only needed if using the Reverse Proxy feature)
- location /management.ProxyService/ {
- grpc_pass grpc://netbird_management;
- grpc_read_timeout 1d;
- grpc_send_timeout 1d;
- grpc_socket_keepalive on;
- }
-
- # Embedded IdP OAuth2
- location /oauth2/ {
- proxy_pass http://netbird_management;
- proxy_set_header Host $host;
- }
-
- # Dashboard (catch-all)
- location / {
- proxy_pass http://netbird_dashboard;
- }
-}
-```
-
-**Installation:**
-
-Debian/Ubuntu:
```bash
-sudo ln -s /path/to/netbird.conf /etc/nginx/sites-available/netbird
-sudo ln -s /etc/nginx/sites-available/netbird /etc/nginx/sites-enabled/
-sudo nginx -t && sudo systemctl reload nginx
-```
+NETBIRD_DOMAIN="netbird.example.com"
-RHEL/CentOS:
-```bash
-sudo cp /path/to/netbird.conf /etc/nginx/conf.d/netbird.conf
-sudo nginx -t && sudo systemctl reload nginx
+curl -fsSI "http://${NETBIRD_DOMAIN}/"
+curl -fsSI "https://${NETBIRD_DOMAIN}/setup"
+curl -fsS "https://${NETBIRD_DOMAIN}/oauth2/.well-known/openid-configuration"
```
-##### TLS Certificate Setup for Nginx
-
-
-**Important difference:** Unlike Caddy which obtains and renews TLS certificates automatically, Nginx requires manual certificate setup and configuration.
-
-
-You'll need to obtain SSL/TLS certificates and update the certificate paths in your Nginx configuration. Here are the most common options:
-
-**Option 1: Let's Encrypt with Certbot (Recommended)**
-
-Let's Encrypt provides free, automated TLS certificates with automatic renewal.
-
-1. Install certbot:
-```bash
-# Debian/Ubuntu
-sudo apt install certbot python3-certbot-nginx
-
-# RHEL/CentOS
-sudo yum install certbot python3-certbot-nginx
-```
+Check the certificate chain and hostname:
-2. Obtain certificate:
```bash
-# If Nginx is already running
-sudo certbot certonly --nginx -d netbird.example.com
-
-# If Nginx isn't running yet (standalone mode)
-sudo certbot certonly --standalone -d netbird.example.com
+openssl s_client \
+ -connect "${NETBIRD_DOMAIN}:443" \
+ -servername "$NETBIRD_DOMAIN" \
+ -verify_return_error
-**Requirements for Let's Encrypt:**
-- Your domain must point to your server's public IP
-- Port 80 must be accessible from the internet for certificate validation
-- Certificates are valid for 90 days and renewed automatically by certbot
-
+The response must have `content-type: application/grpc`. The request body is deliberately incomplete, so an application-level gRPC error is acceptable; an HTML response is not.
-**Option 2: Let's Encrypt with acme.sh**
-
-[acme.sh](https://github.com/acmesh-official/acme.sh) is a lightweight alternative to certbot:
+Probe Relay's WebSocket upgrade:
```bash
-# Install acme.sh
-curl https://get.acme.sh | sh
-
-# Issue certificate
-~/.acme.sh/acme.sh --issue -d netbird.example.com --nginx
-
-# Update Nginx config with paths:
-# ssl_certificate /root/.acme.sh/netbird.example.com/fullchain.cer;
-# ssl_certificate_key /root/.acme.sh/netbird.example.com/netbird.example.com.key;
+curl -sS --http1.1 --max-time 3 -D - -o /dev/null \
+ "https://${NETBIRD_DOMAIN}/relay" \
+ -H 'Connection: Upgrade' \
+ -H 'Upgrade: websocket' \
+ -H 'Sec-WebSocket-Version: 13' \
+ -H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ=='
```
-**Option 3: Custom/Commercial Certificates**
+A correct route returns `101 Switching Protocols`; `curl` then times out because the upgraded connection stays open.
-If you have certificates from another provider:
+After creating the first owner, enroll two clients from outside the server. Confirm:
-1. Place your certificate files on the server:
```bash
-sudo cp fullchain.pem /etc/ssl/certs/netbird.crt
-sudo cp privkey.pem /etc/ssl/private/netbird.key
-sudo chmod 600 /etc/ssl/private/netbird.key
-```
-
-2. Update your Nginx config:
-```nginx
-ssl_certificate /etc/ssl/certs/netbird.crt;
-ssl_certificate_key /etc/ssl/private/netbird.key;
-```
-
-**TLS Best Practices**
-
-The generated Nginx configurations include recommended TLS settings:
-
-```nginx
-# Use modern TLS protocols only
-ssl_protocols TLSv1.2 TLSv1.3;
-
-# Let clients choose the best cipher
-ssl_prefer_server_ciphers off;
-
-# Strong cipher suites
-ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
-```
-
-These settings ensure:
-- Only TLS 1.2 and 1.3 are allowed (older protocols like TLS 1.0/1.1 are disabled)
-- Modern cipher suites that support forward secrecy
-- Compatibility with all modern browsers and NetBird clients
-
----
-
-#### Caddy (Multi-Container)
-
-##### Caddy running in Docker (Multi-Container)
-
-If Caddy is running in Docker, the easiest approach is to have NetBird join the same Docker network. The `getting-started.sh` script will ask for your Caddy network name and configure everything automatically.
-
-```
-netbird.example.com {
- # Relay (WebSocket)
- reverse_proxy /relay* netbird-relay:80
-
- # Signal WebSocket
- reverse_proxy /ws-proxy/signal* netbird-signal:80
-
- # Signal gRPC (h2c for plaintext HTTP/2)
- reverse_proxy /signalexchange.SignalExchange/* h2c://netbird-signal:10000
-
- # Management API
- reverse_proxy /api/* netbird-management:80
-
- # Management WebSocket
- reverse_proxy /ws-proxy/management* netbird-management:80
-
- # Management gRPC
- reverse_proxy /management.ManagementService/* h2c://netbird-management:80
-
- # Proxy gRPC (only needed if using the Reverse Proxy feature)
- reverse_proxy /management.ProxyService/* h2c://netbird-management:80
-
- # Embedded IdP OAuth2
- reverse_proxy /oauth2/* netbird-management:80
-
- # Dashboard (catch-all)
- reverse_proxy /* netbird-dashboard:80
-}
-```
-
-##### Caddy running on host (Multi-Container)
-
-If Caddy is installed directly on the host, use localhost addresses:
-
-```
-netbird.example.com {
- # Relay (WebSocket)
- reverse_proxy /relay* 127.0.0.1:8084
-
- # Signal WebSocket
- reverse_proxy /ws-proxy/signal* 127.0.0.1:8083
-
- # Signal gRPC (h2c for plaintext HTTP/2)
- reverse_proxy /signalexchange.SignalExchange/* h2c://127.0.0.1:10000
-
- # Management API
- reverse_proxy /api/* 127.0.0.1:8081
-
- # Management WebSocket
- reverse_proxy /ws-proxy/management* 127.0.0.1:8081
-
- # Management gRPC
- reverse_proxy /management.ManagementService/* h2c://127.0.0.1:8081
-
- # Proxy gRPC (only needed if using the Reverse Proxy feature)
- reverse_proxy /management.ProxyService/* h2c://127.0.0.1:8081
-
- # Embedded IdP OAuth2
- reverse_proxy /oauth2/* 127.0.0.1:8081
-
- # Dashboard (catch-all)
- reverse_proxy /* 127.0.0.1:8080
-}
-```
-
-After adding the configuration, reload Caddy:
-```bash
-sudo systemctl reload caddy
-```
-
----
-
-#### Nginx Proxy Manager (Multi-Container)
-
-Nginx Proxy Manager requires all NetBird routing to be configured via the "Advanced" tab. The Custom Locations feature doesn't properly handle path prefixes.
-
-
-NPM requires backend services to be running before you can create proxy hosts. The `getting-started.sh` script will start NetBird containers first, then display the configuration instructions.
-
-
-##### NPM running in Docker (Multi-Container)
-
-If NPM is running in Docker, the easiest approach is to have NetBird join the same Docker network. The script will ask for your NPM network name and configure everything automatically.
-
-**1. Create a Proxy Host in NPM:**
-- Domain: `netbird.example.com`
-- Forward Hostname/IP: `netbird-dashboard`
-- Forward Port: `80`
-- Block Common Exploits: enabled
-
-**2. SSL tab:**
-- Request or select existing certificate
-- **Enable "HTTP/2 Support"** (required for gRPC)
-
-**3. Advanced tab - paste this configuration:**
-
-```nginx
-# Required for long-lived connections (gRPC and WebSocket)
-client_header_timeout 1d;
-client_body_timeout 1d;
-
-# Relay WebSocket
-location /relay {
- proxy_pass http://netbird-relay:80;
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "upgrade";
- 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_read_timeout 1d;
-}
-
-# Signal WebSocket
-location /ws-proxy/signal {
- proxy_pass http://netbird-signal:80;
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "upgrade";
- 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_read_timeout 1d;
-}
-
-# Management WebSocket
-location /ws-proxy/management {
- proxy_pass http://netbird-management:80;
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "upgrade";
- 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_read_timeout 1d;
-}
-
-# API routes
-location /api/ {
- proxy_pass http://netbird-management:80;
- 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;
-}
-
-# OAuth2/IdP routes
-location /oauth2/ {
- proxy_pass http://netbird-management:80;
- 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;
-}
-
-# gRPC for Signal service
-location /signalexchange.SignalExchange/ {
- grpc_pass grpc://netbird-signal:10000;
- grpc_read_timeout 1d;
- grpc_send_timeout 1d;
- grpc_socket_keepalive on;
-}
-
-# gRPC for Management service
-location /management.ManagementService/ {
- grpc_pass grpc://netbird-management:80;
- grpc_read_timeout 1d;
- grpc_send_timeout 1d;
- grpc_socket_keepalive on;
-}
-
-# gRPC for Proxy service (only needed if using the Reverse Proxy feature)
-location /management.ProxyService/ {
- grpc_pass grpc://netbird-management:80;
- grpc_read_timeout 1d;
- grpc_send_timeout 1d;
- grpc_socket_keepalive on;
-}
-
-```
-
-##### NPM running on host (Multi-Container)
-
-If NPM is installed directly on the host, use localhost addresses:
-
-**1. Create a Proxy Host in NPM:**
-- Domain: `netbird.example.com`
-- Forward Hostname/IP: `127.0.0.1`
-- Forward Port: `8080`
-- Block Common Exploits: enabled
-
-**2. SSL tab:**
-- Request or select existing certificate
-- **Enable "HTTP/2 Support"** (required for gRPC)
-
-**3. Advanced tab - paste this configuration:**
-
-```nginx
-# Required for long-lived connections (gRPC and WebSocket)
-client_header_timeout 1d;
-client_body_timeout 1d;
-
-# Relay WebSocket
-location /relay {
- proxy_pass http://127.0.0.1:8084;
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "upgrade";
- 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_read_timeout 1d;
-}
-
-# Signal WebSocket
-location /ws-proxy/signal {
- proxy_pass http://127.0.0.1:8083;
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "upgrade";
- 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_read_timeout 1d;
-}
-
-# Management WebSocket
-location /ws-proxy/management {
- proxy_pass http://127.0.0.1:8081;
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "upgrade";
- 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_read_timeout 1d;
-}
-
-# API routes
-location /api/ {
- proxy_pass http://127.0.0.1:8081;
- 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;
-}
-
-# OAuth2/IdP routes
-location /oauth2/ {
- proxy_pass http://127.0.0.1:8081;
- 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;
-}
-
-# gRPC for Signal service
-location /signalexchange.SignalExchange/ {
- grpc_pass grpc://127.0.0.1:10000;
- grpc_read_timeout 1d;
- grpc_send_timeout 1d;
- grpc_socket_keepalive on;
-}
-
-# gRPC for Management service
-location /management.ManagementService/ {
- grpc_pass grpc://127.0.0.1:8081;
- grpc_read_timeout 1d;
- grpc_send_timeout 1d;
- grpc_socket_keepalive on;
-}
-
-# gRPC for Proxy service (only needed if using the Reverse Proxy feature)
-location /management.ProxyService/ {
- grpc_pass grpc://127.0.0.1:8081;
- grpc_read_timeout 1d;
- grpc_send_timeout 1d;
- grpc_socket_keepalive on;
-}
-
+netbird status --detail
```
----
+- Management is connected.
+- Signal is connected.
+- STUN and secure Relay are available.
+- The two peers can exchange traffic.
-## Troubleshooting
+## Common failure patterns
-### gRPC connections failing
+### Dashboard works but clients cannot connect
-- Ensure your reverse proxy supports HTTP/2 and gRPC
-- **Nginx Proxy Manager**: Enable "HTTP/2 Support" in the SSL tab
-- Check that `h2c` (plaintext HTTP/2) is correctly configured for gRPC upstreams
-- Verify timeout settings are long enough (gRPC connections can be long-lived)
+The Dashboard catch-all is hiding a missing or lower-priority gRPC route. Run the gRPC probe and inspect the proxy access log. Native gRPC must reach NetBird Server or the correct multi-container Signal or Management service over h2c.
-### WebSocket connections failing
+### `400 Bad Request` on Relay or fallback paths
-- Ensure WebSocket upgrade headers are passed through
-- Check that `Connection: Upgrade` and `Upgrade: websocket` headers are preserved
+Confirm the proxy preserves WebSocket `Upgrade` and `Connection` headers. Use a valid 16-byte base64 WebSocket key when testing; a malformed test handshake itself produces `400`.
-### SSL/TLS certificate issues
+### Connections drop after a fixed interval
-- Verify your certificates are valid and not expired
-- Check certificate paths in your configuration
-- For Let's Encrypt, ensure ports 80 and 443 are accessible for validation
+Increase proxy and upstream load-balancer idle/read timeouts. A correct route can still fail when another load balancer in the path has a shorter timeout.
-### Dashboard loads but API calls fail
+### NetBird ignores forwarded client addresses
-- Check that `/api/*` routes are correctly configured
-- Verify the management container is running: `docker compose ps management`
-- Check management logs: `docker compose logs management`
+Inspect the source address seen by the container and configure that exact `/32` in both trust lists. Restart NetBird Server or the multi-container Management service after changing the configuration.
-### Signal or Relay connections failing
+### STUN is unavailable
-- Verify gRPC routes are using `h2c://` (plaintext HTTP/2)
-- Check that WebSocket routes have proper upgrade handling
-- Ensure coturn (STUN/TURN) is accessible on UDP port 3478
+The HTTP proxy configuration does not control STUN. Confirm UDP 3478 is published by the correct NetBird service and allowed by every provider, network, and host firewall.
-For more help, see the [Troubleshooting guide](/selfhosted/troubleshooting) or reach out on [Slack](/slack-url).
+For additional diagnostics, see [Self-hosted troubleshooting](/selfhosted/troubleshooting).
diff --git a/src/pages/selfhosted/maintenance/configuration-files.mdx b/src/pages/selfhosted/maintenance/configuration-files.mdx
index 32c9ad142..5f17bdd1a 100644
--- a/src/pages/selfhosted/maintenance/configuration-files.mdx
+++ b/src/pages/selfhosted/maintenance/configuration-files.mdx
@@ -217,7 +217,7 @@ The STUN port (3478/udp) must always be exposed publicly, regardless of reverse
## config.yaml
-The unified configuration file controls the combined NetBird server. It replaces the separate `management.json` and `relay.env` files from older deployments.
+The unified configuration file controls the combined NetBird server. Multi-container deployments use separate `management.json` and `relay.env` files instead.
See an example `config.yaml` file in the [NetBird GitHub repository](https://github.com/netbirdio/netbird/blob/main/combined/config.yaml.example).
### Complete Structure
@@ -547,6 +547,6 @@ See the [Scaling Your Self-Hosted Deployment](/selfhosted/maintenance/scaling/sc
## See Also
- [Self-hosting Quickstart Guide](/selfhosted/selfhosted-quickstart) - Get started quickly with default settings
-- [External Reverse Proxy Configuration](/selfhosted/external-reverse-proxy) - Nginx, Caddy, NPM, HAProxy setup
+- [External Reverse Proxy Configuration](/selfhosted/external-reverse-proxy) - Traefik, Nginx, Caddy, and Nginx Proxy Manager setup
- [Management SQLite Store](/selfhosted/maintenance/sqlite-store) - SQLite database details
- [Management Postgres Store](/selfhosted/maintenance/postgres-store) - PostgreSQL setup
diff --git a/src/pages/selfhosted/manual-setup.mdx b/src/pages/selfhosted/manual-setup.mdx
new file mode 100644
index 000000000..2bfc46afb
--- /dev/null
+++ b/src/pages/selfhosted/manual-setup.mdx
@@ -0,0 +1,397 @@
+import {Warning} from "@/components/mdx"
+
+export const description =
+ "Manually deploy self-hosted NetBird with Docker Compose, the combined server, embedded identity provider, and Traefik TLS."
+
+# Manually install NetBird with Docker Compose
+
+This guide deploys self-hosted NetBird without running `getting-started.sh`. You create and review each configuration file before starting the Dashboard, the combined NetBird server, and a Traefik reverse proxy on one Linux host.
+
+The resulting deployment uses the embedded identity provider for local users, SQLite for storage, an embedded Signal service, Relay, and STUN. It does not enable the optional [NetBird Proxy](/manage/reverse-proxy), CrowdSec, an external database, or high availability.
+
+This is the recommended manual starting point: the combined server and embedded identity provider keep the NetBird core together, while Traefik obtains and renews the public TLS certificate. If you are still choosing an architecture, read [Plan a manual NetBird deployment](/selfhosted/manual-setup/planning) first. External identity providers, databases, other reverse proxies, and scaling services can be integrated later without replacing the combined server.
+
+This is the current **combined-container** flow and includes a complete Traefik deployment. To use another proxy, prepare `config.yaml` and `dashboard.env` here, then follow the matching combined-layout guide:
+
+- [Nginx](/selfhosted/manual-setup/reverse-proxies/nginx#combined-container-layout)
+- [Caddy](/selfhosted/manual-setup/reverse-proxies/caddy#combined-container-layout)
+- [Nginx Proxy Manager](/selfhosted/manual-setup/reverse-proxies/nginx-proxy-manager#combined-container-layout)
+- [Traefik with an existing instance](/selfhosted/manual-setup/reverse-proxies/traefik#combined-container-layout)
+
+If you maintain separate Management, Signal, Relay, and Dashboard containers, use [Manual multi-container setup](/selfhosted/manual-setup/multi-container) instead.
+
+## Prerequisites
+
+- A public Linux server with at least 1 CPU. A small deployment can run with 1 GB of memory when swap is available; 2 GB or more is recommended for image pulls, upgrades, and diagnostics.
+- A public domain such as `netbird.example.com` whose A record resolves to the server's public IPv4 address. If the name has an AAAA record, it must also reach this server over IPv6.
+- Inbound TCP ports 80 and 443 and inbound UDP port 3478 allowed by every provider, network, and host firewall in front of the server.
+- [Docker Engine with Docker Compose version 2 or later](https://docs.docker.com/engine/install/).
+- `curl` and `openssl` installed on the server.
+- Shell access to an account that can run Docker commands.
+
+If your DNS provider can proxy HTTP traffic, configure this hostname as DNS-only. All NetBird clients must be able to reach the hostname directly.
+
+| Port | Protocol | Purpose |
+|---|---|---|
+| `80` | TCP | Redirects HTTP to HTTPS. |
+| `443` | TCP | Serves the Dashboard, REST API, embedded identity provider, Management gRPC, Signal, and Relay. Traefik also uses this port for the TLS-ALPN certificate challenge. |
+| `3478` | UDP | Serves the embedded STUN service for NAT discovery. |
+
+
+**Security warning:** Docker-published ports can bypass rules managed only through `ufw` or `firewalld`. Permit the required ports in your hosting provider or network firewall as well, and do not publish the Dashboard or NetBird server's internal HTTP ports directly. See [Docker's firewall limitations](https://docs.docker.com/engine/install/ubuntu/#firewall-limitations).
+
+
+The examples use `netbird.example.com`. Replace it with your actual hostname before running any command. Do not use the example hostname in a real deployment.
+
+## 1. Verify DNS and Docker
+
+Run these commands on the server. Set `NETBIRD_DOMAIN` to your actual hostname for the DNS check.
+
+```bash
+NETBIRD_DOMAIN="netbird.example.com"
+
+getent ahostsv4 "$NETBIRD_DOMAIN"
+docker --version
+docker compose version
+```
+
+The DNS result must contain this server's public IPv4 address. If you publish an AAAA record, also verify it:
+
+```bash
+getent ahostsv6 "$NETBIRD_DOMAIN"
+```
+
+Do not continue until DNS points to the server and the required ports are allowed. Certificate issuance will fail if the hostname resolves elsewhere or TCP 443 is blocked.
+
+For example, a host using `ufw` can add the required host rules with:
+
+```bash
+sudo ufw allow 80/tcp
+sudo ufw allow 443/tcp
+sudo ufw allow 3478/udp
+sudo ufw status
+```
+
+These rules do not replace your hosting provider or network firewall configuration.
+
+## 2. Create the deployment directory
+
+Create a private directory for the Compose and NetBird configuration files:
+
+```bash
+install -d -m 700 "$HOME/netbird"
+cd "$HOME/netbird"
+umask 077
+```
+
+All remaining commands in this guide run from this directory.
+
+Create `.env` with your actual NetBird hostname and an email address for Let's Encrypt certificate notices:
+
+```bash
+cat > .env <<'EOF'
+NETBIRD_DOMAIN=netbird.example.com
+LETSENCRYPT_EMAIL=admin@example.com
+EOF
+
+chmod 600 .env
+```
+
+Edit both values before continuing. Docker Compose automatically reads this file and substitutes the values into `docker-compose.yml`.
+
+## 3. Create the NetBird server configuration
+
+The combined `netbird-server` container runs Management, Signal, Relay, STUN, and the embedded identity provider. The following command generates separate random values for Relay authentication and encryption of sensitive values in the SQLite store, then writes `config.yaml`.
+
+```bash
+set -a
+. ./.env
+set +a
+
+NETBIRD_RELAY_AUTH_SECRET="$(openssl rand -base64 32 | tr -d '=')"
+NETBIRD_STORE_ENCRYPTION_KEY="$(openssl rand -base64 32)"
+
+cat > config.yaml <
+**Data-loss warning:** Keep `config.yaml` and `server.store.encryptionKey` secure and backed up. NetBird uses the encryption key to protect setup keys, API tokens, and other sensitive stored values. Losing it requires those encrypted credentials to be regenerated.
+
+
+## 4. Create the Dashboard configuration
+
+Create `dashboard.env` with endpoints that match the issuer and redirect URIs in `config.yaml`:
+
+```bash
+set -a
+. ./.env
+set +a
+
+cat > dashboard.env < docker-compose.yml <<'YAML'
+services:
+ traefik:
+ image: traefik:v3.6
+ container_name: netbird-traefik
+ restart: unless-stopped
+ networks:
+ netbird:
+ ipv4_address: 172.30.0.10
+ command:
+ - "--log.level=INFO"
+ - "--accesslog=true"
+ - "--providers.docker=true"
+ - "--providers.docker.exposedbydefault=false"
+ - "--providers.docker.network=netbird"
+ - "--entrypoints.web.address=:80"
+ - "--entrypoints.websecure.address=:443"
+ - "--entrypoints.websecure.allowACMEByPass=true"
+ - "--entrypoints.websecure.transport.respondingTimeouts.readTimeout=0"
+ - "--entrypoints.websecure.transport.respondingTimeouts.writeTimeout=0"
+ - "--entrypoints.websecure.transport.respondingTimeouts.idleTimeout=0"
+ - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
+ - "--entrypoints.web.http.redirections.entrypoint.scheme=https"
+ - "--certificatesresolvers.letsencrypt.acme.email=${LETSENCRYPT_EMAIL}"
+ - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
+ - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
+ - "--serverstransport.forwardingtimeouts.responseheadertimeout=0s"
+ - "--serverstransport.forwardingtimeouts.idleconntimeout=0s"
+ ports:
+ - "80:80"
+ - "443:443"
+ volumes:
+ - /var/run/docker.sock:/var/run/docker.sock:ro
+ - netbird_traefik_letsencrypt:/letsencrypt
+ logging:
+ driver: json-file
+ options:
+ max-size: 500m
+ max-file: "2"
+
+ dashboard:
+ image: netbirdio/dashboard:latest
+ container_name: netbird-dashboard
+ restart: unless-stopped
+ networks: [netbird]
+ env_file:
+ - ./dashboard.env
+ labels:
+ - "traefik.enable=true"
+ - "traefik.http.routers.netbird-dashboard.rule=Host(`${NETBIRD_DOMAIN}`)"
+ - "traefik.http.routers.netbird-dashboard.entrypoints=websecure"
+ - "traefik.http.routers.netbird-dashboard.tls=true"
+ - "traefik.http.routers.netbird-dashboard.tls.certresolver=letsencrypt"
+ - "traefik.http.routers.netbird-dashboard.service=dashboard"
+ - "traefik.http.routers.netbird-dashboard.priority=1"
+ - "traefik.http.services.dashboard.loadbalancer.server.port=80"
+ logging:
+ driver: json-file
+ options:
+ max-size: 500m
+ max-file: "2"
+
+ netbird-server:
+ image: netbirdio/netbird-server:latest
+ container_name: netbird-server
+ restart: unless-stopped
+ networks: [netbird]
+ ports:
+ - "3478:3478/udp"
+ volumes:
+ - netbird_data:/var/lib/netbird
+ - ./config.yaml:/etc/netbird/config.yaml:ro
+ command: ["--config", "/etc/netbird/config.yaml"]
+ labels:
+ - "traefik.enable=true"
+ - "traefik.http.routers.netbird-grpc.rule=Host(`${NETBIRD_DOMAIN}`) && (PathPrefix(`/signalexchange.SignalExchange/`) || PathPrefix(`/management.ManagementService/`) || PathPrefix(`/management.ProxyService/`))"
+ - "traefik.http.routers.netbird-grpc.entrypoints=websecure"
+ - "traefik.http.routers.netbird-grpc.tls=true"
+ - "traefik.http.routers.netbird-grpc.tls.certresolver=letsencrypt"
+ - "traefik.http.routers.netbird-grpc.service=netbird-server-h2c"
+ - "traefik.http.routers.netbird-grpc.priority=100"
+ - "traefik.http.routers.netbird-backend.rule=Host(`${NETBIRD_DOMAIN}`) && (PathPrefix(`/relay`) || PathPrefix(`/ws-proxy/`) || PathPrefix(`/api`) || PathPrefix(`/oauth2`))"
+ - "traefik.http.routers.netbird-backend.entrypoints=websecure"
+ - "traefik.http.routers.netbird-backend.tls=true"
+ - "traefik.http.routers.netbird-backend.tls.certresolver=letsencrypt"
+ - "traefik.http.routers.netbird-backend.service=netbird-server"
+ - "traefik.http.routers.netbird-backend.priority=100"
+ - "traefik.http.services.netbird-server.loadbalancer.server.port=80"
+ - "traefik.http.services.netbird-server-h2c.loadbalancer.server.port=80"
+ - "traefik.http.services.netbird-server-h2c.loadbalancer.server.scheme=h2c"
+ logging:
+ driver: json-file
+ options:
+ max-size: 500m
+ max-file: "2"
+
+volumes:
+ netbird_data:
+ netbird_traefik_letsencrypt:
+
+networks:
+ netbird:
+ name: netbird
+ driver: bridge
+ ipam:
+ config:
+ - subnet: 172.30.0.0/24
+ gateway: 172.30.0.1
+YAML
+
+chmod 600 docker-compose.yml
+```
+
+The only public container ports are TCP 80 and 443 on Traefik and UDP 3478 on `netbird-server`. The Dashboard and the server's internal HTTP listener remain reachable only through the private `netbird` Docker network.
+
+Traefik sends gRPC paths to the server over h2c and sends the REST API, embedded identity provider, Relay, and WebSocket paths over HTTP. The Dashboard router has the lowest priority and handles all remaining paths.
+
+## 6. Validate and start NetBird
+
+Ask Docker Compose to parse and interpolate the configuration before pulling images:
+
+```bash
+docker compose config --quiet
+```
+
+The command exits without output when the Compose file is valid. If it reports that `NETBIRD_DOMAIN` or `LETSENCRYPT_EMAIL` is missing, check `.env` and run the command again.
+
+Pull the current images, create the private network and persistent volumes, and start the deployment:
+
+```bash
+docker compose pull
+docker compose up -d
+```
+
+Check the container state:
+
+```bash
+docker compose ps
+```
+
+The `traefik`, `dashboard`, and `netbird-server` services must all show `Up`. If a service is restarting or has exited, inspect only that service first:
+
+```bash
+docker compose logs --tail=100 traefik
+docker compose logs --tail=100 dashboard
+docker compose logs --tail=100 netbird-server
+```
+
+Logs can contain hostnames, addresses, user details, and tokens. Review and redact them before sharing.
+
+## 7. Verify HTTPS and the NetBird endpoints
+
+Load the domain from `.env`, then test the public HTTPS path from the server:
+
+```bash
+set -a
+. ./.env
+set +a
+
+curl -fsS "https://${NETBIRD_DOMAIN}/oauth2/.well-known/openid-configuration" > /dev/null
+curl -fsSI "https://${NETBIRD_DOMAIN}/setup"
+```
+
+The first command exits successfully when Traefik can route to the embedded identity provider. The second command should return an HTTP `200` response before the first user is created.
+
+Also open `https://netbird.example.com/setup` from a different network and replace the hostname with your actual domain. This external test confirms that public DNS, the network firewall, Traefik, and the Dashboard path work together; a successful test from the server alone does not prove outside clients can connect.
+
+If HTTPS presents a default or untrusted certificate, do not bypass the warning. Confirm that the A and optional AAAA records point to this server, TCP 443 is publicly reachable, and Traefik's recent logs show successful ACME certificate issuance.
+
+## 8. Create the first owner user
+
+The first visit to `/setup` displays **Create the first admin account to get started**.
+
+1. Open `https://netbird.example.com/setup` with your actual hostname.
+2. Enter the owner's name and email address.
+3. Enter and confirm a password of at least eight characters. Use a long, unique password.
+4. Click **Create Admin Account**.
+
+The setup page is available only while the deployment has no users. After NetBird creates the first owner, `/setup` redirects to the regular login page.
+
+For non-interactive provisioning, use [Automated setup with a Personal Access Token](/selfhosted/automated-setup) instead of exposing credentials in shell history or deployment files.
+
+## Stop or resume the deployment
+
+Stop and remove the containers and private network while preserving the data and certificate volumes:
+
+```bash
+docker compose down
+```
+
+Start the same deployment again with:
+
+```bash
+docker compose up -d
+```
+
+Do not add `--volumes` unless you intend to permanently delete the NetBird database and Traefik certificates. Before upgrades or configuration changes, follow the [backup guide](/selfhosted/maintenance/backup), then review the [upgrade procedure](/selfhosted/maintenance/upgrade).
+
+## Next steps
+
+- Compare all [supported reverse proxy configurations](/selfhosted/external-reverse-proxy), including combined-container and multi-container layouts.
+- Use [Manual multi-container setup](/selfhosted/manual-setup/multi-container) when separate Management, Signal, and Relay services better fit your operational requirements.
+- Review the [configuration files reference](/selfhosted/maintenance/configuration-files) before changing server, authentication, storage, or reverse proxy settings.
+- Connect an external identity provider through [Authentication and IdPs](/selfhosted/identity-providers).
+- Add the optional [NetBird Proxy](/selfhosted/migration/enable-reverse-proxy) when you need to publish selected NetBird resources.
+- Use [self-hosted installation troubleshooting](/selfhosted/troubleshooting/installation) if the containers, certificate, or setup page do not reach the expected state.
diff --git a/src/pages/selfhosted/manual-setup/multi-container.mdx b/src/pages/selfhosted/manual-setup/multi-container.mdx
new file mode 100644
index 000000000..05860b911
--- /dev/null
+++ b/src/pages/selfhosted/manual-setup/multi-container.mdx
@@ -0,0 +1,429 @@
+import {Note, Warning} from "@/components/mdx"
+
+export const description =
+ "Manually deploy NetBird with separate Management, Signal, Relay, and Dashboard containers using Docker Compose and an external reverse proxy."
+
+# Manual multi-container setup
+
+This guide manually deploys NetBird with separate `management`, `signal`, `relay`, and `dashboard` containers. It uses the embedded identity provider, SQLite, and the current NetBird images.
+
+The [combined `netbird-server` manual setup](/selfhosted/manual-setup) is recommended for most installations because it reduces the number of services and configuration files to manage. Choose this multi-container layout when separate service lifecycles, per-service resource controls, or your infrastructure topology make distinct Management, Signal, and Relay services a better fit.
+
+The guide prepares the NetBird services and loopback backend ports. Finish the deployment with one of the tested reverse proxy guides:
+
+- [Traefik](/selfhosted/manual-setup/reverse-proxies/traefik)
+- [Nginx](/selfhosted/manual-setup/reverse-proxies/nginx)
+- [Caddy](/selfhosted/manual-setup/reverse-proxies/caddy)
+- [Nginx Proxy Manager](/selfhosted/manual-setup/reverse-proxies/nginx-proxy-manager)
+
+## Architecture and ports
+
+One public hostname serves every HTTP, WebSocket, and gRPC endpoint. The reverse proxy selects a backend by path.
+
+| Public path | Protocol | Backend |
+|---|---|---|
+| `/relay` | WebSocket | `relay:80` |
+| `/ws-proxy/signal` | WebSocket | `signal:80` |
+| `/signalexchange.SignalExchange/*` | gRPC over h2c | `signal:10000` |
+| `/api/*` | HTTP | `management:80` |
+| `/oauth2/*` | HTTP | `management:80` |
+| `/ws-proxy/management` | WebSocket | `management:80` |
+| `/management.ManagementService/*` | gRPC over h2c | `management:80` |
+| `/management.ProxyService/*` | gRPC over h2c | `management:80` |
+| Every other path | HTTP | `dashboard:80` |
+
+The public listener requirements are:
+
+| Port | Protocol | Owner | Purpose |
+|---|---|---|---|
+| `80` | TCP | Reverse proxy | HTTP redirect and ACME HTTP challenge, when used. |
+| `443` | TCP | Reverse proxy | Dashboard, API, embedded IdP, Management, Signal, and secure Relay. |
+| `3478` | UDP | `relay` | STUN for NAT discovery. This bypasses the HTTP reverse proxy. |
+
+The Compose file also binds backend ports to loopback for a proxy installed on the host:
+
+| Host address | Service |
+|---|---|
+| `127.0.0.1:8080` | Dashboard HTTP |
+| `127.0.0.1:8081` | Management HTTP, WebSocket, and h2c gRPC |
+| `127.0.0.1:8083` | Signal WebSocket |
+| `127.0.0.1:10000` | Signal h2c gRPC |
+| `127.0.0.1:8084` | Relay WebSocket |
+
+
+A Docker-based Traefik or Nginx Proxy Manager deployment reaches the services by container name over the private `netbird-proxy` network. Keeping the loopback bindings does not publish them to the Internet.
+
+
+## Prerequisites
+
+- A public Linux server with Docker Engine and Docker Compose v2.
+- A hostname such as `netbird.example.com` whose A record points directly to the server. If an AAAA record exists, IPv6 must reach the same server.
+- Inbound TCP 80 and 443 and inbound UDP 3478 allowed by provider, network, and host firewalls.
+- `curl`, `jq`, and `openssl`.
+- No CDN or HTTP proxy in front of the NetBird hostname.
+
+A small deployment can run with 1 CPU and 1 GB of memory when swap is available. At least 2 GB is recommended to give image pulls, upgrades, and diagnostics more headroom.
+
+
+Docker-published ports can bypass rules managed only through `ufw` or `firewalld`. Enforce the public port policy in your hosting provider or upstream network firewall too. Never publish ports 8080, 8081, 8083, 8084, or 10000 on all interfaces.
+
+
+## 1. Verify DNS and Docker
+
+Set the real hostname and confirm that DNS and Docker are ready:
+
+```bash
+NETBIRD_DOMAIN="netbird.example.com"
+
+getent ahostsv4 "$NETBIRD_DOMAIN"
+docker --version
+docker compose version
+```
+
+If the hostname has an AAAA record, verify it too:
+
+```bash
+getent ahostsv6 "$NETBIRD_DOMAIN"
+```
+
+For a host using `ufw`, the required rules are:
+
+```bash
+sudo ufw allow 80/tcp
+sudo ufw allow 443/tcp
+sudo ufw allow 3478/udp
+sudo ufw status
+```
+
+## 2. Create the deployment directory
+
+```bash
+install -d -m 700 "$HOME/netbird"
+cd "$HOME/netbird"
+umask 077
+```
+
+Create `.env`:
+
+```bash
+cat > .env <<'EOF'
+NETBIRD_DOMAIN=netbird.example.com
+EOF
+
+chmod 600 .env
+```
+
+Replace the example hostname before continuing.
+
+## 3. Choose the trusted proxy address
+
+NetBird accepts forwarded client addresses only from explicitly trusted proxies. Choose one address based on where the proxy runs:
+
+| Proxy placement | Trusted address |
+|---|---|
+| Nginx or Caddy installed on this host | `172.30.0.1/32` (the private Docker bridge gateway) |
+| Traefik or Nginx Proxy Manager in Docker | `172.30.0.10/32` (the fixed proxy address used by these guides) |
+
+Add the choice to `.env`. This example is for a host-installed proxy:
+
+```bash
+cat >> .env <<'EOF'
+NETBIRD_TRUSTED_PROXY=172.30.0.1/32
+EOF
+```
+
+Do not use `0.0.0.0/0`. Trusting every source allows clients to forge forwarding headers.
+
+## 4. Create `management.json`
+
+Generate independent Relay and data-store encryption secrets, then render the Management configuration:
+
+```bash
+set -a
+. ./.env
+set +a
+
+NETBIRD_RELAY_AUTH_SECRET="$(openssl rand -base64 32 | tr -d '=')"
+NETBIRD_STORE_ENCRYPTION_KEY="$(openssl rand -base64 32)"
+
+cat > management.json <
+Back up `management.json`, especially `DataStoreEncryptionKey`, and the Management data volume. Losing the key makes stored setup keys, API tokens, and other encrypted values unusable.
+
+
+## 5. Create Relay and Dashboard environments
+
+Use the same Relay secret in Management and Relay:
+
+```bash
+set -a
+. ./.env
+NETBIRD_RELAY_AUTH_SECRET="$(jq -r '.Relay.Secret' management.json)"
+set +a
+
+cat > relay.env < dashboard.env < docker-compose.yml <<'YAML'
+services:
+ dashboard:
+ image: netbirdio/dashboard:latest
+ container_name: netbird-dashboard
+ restart: unless-stopped
+ networks: [netbird-proxy]
+ ports:
+ - "127.0.0.1:8080:80"
+ env_file:
+ - ./dashboard.env
+ logging:
+ driver: json-file
+ options:
+ max-size: 100m
+ max-file: "2"
+
+ signal:
+ image: netbirdio/signal:latest
+ container_name: netbird-signal
+ restart: unless-stopped
+ networks: [netbird-proxy]
+ ports:
+ - "127.0.0.1:8083:80"
+ - "127.0.0.1:10000:10000"
+ logging:
+ driver: json-file
+ options:
+ max-size: 100m
+ max-file: "2"
+
+ relay:
+ image: netbirdio/relay:latest
+ container_name: netbird-relay
+ restart: unless-stopped
+ networks: [netbird-proxy]
+ ports:
+ - "127.0.0.1:8084:80"
+ - "3478:3478/udp"
+ env_file:
+ - ./relay.env
+ logging:
+ driver: json-file
+ options:
+ max-size: 100m
+ max-file: "2"
+
+ management:
+ image: netbirdio/management:latest
+ container_name: netbird-management
+ restart: unless-stopped
+ networks: [netbird-proxy]
+ ports:
+ - "127.0.0.1:8081:80"
+ volumes:
+ - netbird_management:/var/lib/netbird
+ - ./management.json:/etc/netbird/management.json:ro
+ command:
+ - --port
+ - "80"
+ - --log-file
+ - console
+ - --log-level
+ - info
+ - --disable-anonymous-metrics=false
+ - --single-account-mode-domain=netbird.selfhosted
+ - --dns-domain=netbird.selfhosted
+ - --idp-sign-key-refresh-enabled
+ logging:
+ driver: json-file
+ options:
+ max-size: 100m
+ max-file: "2"
+
+volumes:
+ netbird_management:
+
+networks:
+ netbird-proxy:
+ name: netbird-proxy
+ driver: bridge
+ ipam:
+ config:
+ - subnet: 172.30.0.0/24
+ gateway: 172.30.0.1
+YAML
+
+chmod 600 docker-compose.yml
+docker compose config --quiet
+```
+
+If `172.30.0.0/24` overlaps another host or Docker network, select an unused private subnet and change the gateway and trusted proxy addresses consistently.
+
+## 7. Start the NetBird backends
+
+```bash
+docker compose pull
+docker compose up -d
+docker compose ps
+```
+
+All four services must show `Up`. The public HTTPS endpoint will not work until the reverse proxy is configured.
+
+Inspect a failed service without dumping every log:
+
+```bash
+docker compose logs --tail=100 management
+docker compose logs --tail=100 signal
+docker compose logs --tail=100 relay
+docker compose logs --tail=100 dashboard
+```
+
+Logs can contain hostnames, addresses, user information, and tokens. Redact them before sharing.
+
+## 8. Configure the reverse proxy
+
+Choose the multi-container section in one guide:
+
+- [Traefik multi-container routing](/selfhosted/manual-setup/reverse-proxies/traefik#multi-container-layout)
+- [Nginx multi-container routing](/selfhosted/manual-setup/reverse-proxies/nginx#multi-container-layout)
+- [Caddy multi-container routing](/selfhosted/manual-setup/reverse-proxies/caddy#multi-container-layout)
+- [Nginx Proxy Manager multi-container routing](/selfhosted/manual-setup/reverse-proxies/nginx-proxy-manager#multi-container-layout)
+
+For Docker-based Traefik or Nginx Proxy Manager, confirm that the proxy joins `netbird-proxy` at `172.30.0.10`. For host-installed Nginx or Caddy, keep the loopback upstreams and trust `172.30.0.1/32`.
+
+## 9. Verify the complete deployment
+
+Run these tests from a machine outside the server's network:
+
+```bash
+NETBIRD_DOMAIN="netbird.example.com"
+
+curl -fsSI "http://${NETBIRD_DOMAIN}/"
+curl -fsSI "https://${NETBIRD_DOMAIN}/setup"
+curl -fsS "https://${NETBIRD_DOMAIN}/oauth2/.well-known/openid-configuration"
+```
+
+Expected results:
+
+- HTTP redirects to HTTPS.
+- HTTPS uses a certificate trusted for the exact hostname.
+- `/setup` returns `200` before the first owner exists.
+- OIDC discovery returns an issuer of `https://netbird.example.com/oauth2` with your real hostname.
+
+Verify gRPC reaches Management rather than the Dashboard:
+
+```bash
+curl -sS --http2 -D - -o /dev/null \
+ -X POST "https://${NETBIRD_DOMAIN}/management.ManagementService/GetServerKey" \
+ -H 'Content-Type: application/grpc' \
+ --data-binary $'\x00\x00\x00\x00\x00'
+```
+
+The response must use `content-type: application/grpc`. An application-level gRPC error is acceptable for this deliberately incomplete request; HTML is not.
+
+Verify the Relay WebSocket upgrade:
+
+```bash
+curl -sS --http1.1 --max-time 3 -D - -o /dev/null \
+ "https://${NETBIRD_DOMAIN}/relay" \
+ -H 'Connection: Upgrade' \
+ -H 'Upgrade: websocket' \
+ -H 'Sec-WebSocket-Version: 13' \
+ -H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ=='
+```
+
+A successful route returns `101 Switching Protocols`; `curl` then times out because the WebSocket remains open.
+
+Finally, create the first owner at `https://netbird.example.com/setup`, enroll two clients, and confirm both `netbird status --detail` and traffic between them. A working Dashboard alone does not verify Signal or Relay.
+
+## Stop, resume, and migrate
+
+Stop the containers while preserving data:
+
+```bash
+docker compose down
+```
+
+Resume them with:
+
+```bash
+docker compose up -d
+```
+
+Do not add `--volumes` unless you intend to delete the Management database permanently. Before changes, follow the [backup guide](/selfhosted/maintenance/backup). If you later want to consolidate the services, follow [Migrate to the combined container](/selfhosted/migration/combined-container).
diff --git a/src/pages/selfhosted/manual-setup/planning.mdx b/src/pages/selfhosted/manual-setup/planning.mdx
new file mode 100644
index 000000000..600268cf2
--- /dev/null
+++ b/src/pages/selfhosted/manual-setup/planning.mdx
@@ -0,0 +1,134 @@
+import { Note } from '@/components/mdx'
+
+export const description =
+ 'Plan a manual self-hosted NetBird deployment, compare both server layouts, and start with the combined server recommended for most setups.'
+
+# Plan a manual NetBird deployment
+
+A new self-hosted NetBird deployment does not need a separate identity provider, database server, or collection of control-plane containers. Start with the combined NetBird server, embedded identity provider, SQLite, and Traefik on one host. This gives you automatic TLS and the fewest systems to operate.
+
+Treat everything else—an external identity provider, PostgreSQL, another reverse proxy, external Signal or Relay services, and high availability—as an integration. Add one when it solves a requirement you actually have, not because it appears in the list of available components.
+
+## Recommended starting point
+
+For most new manual installations, use this layout:
+
+| Part | Recommended choice | Why |
+| ------------------- | --------------------------- | ---------------------------------------------------------------------------------- |
+| NetBird services | Combined `netbird-server` | Runs Management, Signal, Relay, STUN, and the embedded identity provider together. |
+| User authentication | Embedded identity provider | Provides local users without another service or client registration to maintain. |
+| Data store | SQLite | Keeps the initial deployment self-contained on one host. |
+| Public entry point | Traefik | Obtains and renews a trusted TLS certificate automatically. |
+| Dashboard | NetBird Dashboard container | Serves the administration interface behind Traefik. |
+| Host count | One Linux server | Keeps installation, backup, and troubleshooting straightforward. |
+
+The [manual combined-container guide](/selfhosted/manual-setup) builds this exact deployment. It is the recommended path for most installations.
+
+
+ The combined architecture is sometimes described as the “single-container”
+ setup because the NetBird server components share one `netbird-server`
+ container. The complete deployment still has separate Dashboard and reverse
+ proxy containers. “Combined” is the more precise name.
+
+
+## Combined and multi-container guides
+
+The manual documentation covers both server layouts:
+
+| Layout | What runs | When to use it |
+| ---------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
+| [**Combined server**](/selfhosted/manual-setup) | One `netbird-server` for Management, Signal, Relay, STUN, and the embedded IdP; separate Dashboard and Traefik containers. | Recommended for most setups because it reduces the number of services and configuration files to manage. |
+| [**Multi-container**](/selfhosted/manual-setup/multi-container) | Separate Management, Signal, Relay, and Dashboard containers. | Separate service lifecycles, per-service resource controls, or infrastructure that benefits from distinct backends. |
+
+The manual guides cover both layouts. If you use the multi-container layout and later want to reduce its operational overhead, follow [Migrate to the combined container](/selfhosted/migration/combined-container).
+
+## Keep the core small
+
+The recommended deployment has a clear boundary:
+
+- Traefik accepts public TCP 80 and 443, manages the certificate, and routes HTTP, WebSocket, and gRPC traffic.
+- `netbird-server` provides the NetBird control-plane services, secure Relay, STUN, and local authentication.
+- The Dashboard provides the web interface.
+- SQLite stores NetBird state on the host-backed server volume.
+- UDP 3478 reaches the STUN listener directly instead of passing through Traefik.
+
+Only Traefik's HTTP ports and the NetBird STUN port need public ingress. Internal Dashboard and server HTTP ports stay on the private Docker network.
+
+This layout is intentionally modest. It is suitable when one host is an acceptable availability boundary and you can restore the deployment from a tested backup.
+
+## Add integrations when a requirement calls for them
+
+### External identity provider
+
+Keep the embedded identity provider when local users meet your needs. Add an [external identity provider](/selfhosted/identity-providers) when you need an existing SSO system, centralized account lifecycle, provider-enforced MFA, or identity claims from systems such as Microsoft Entra ID, Google Workspace, Okta, Keycloak, or Authentik.
+
+An external provider integrates with the combined server; it does not require switching back to separate Management and Signal containers. Verify external login before disabling local authentication, and retain a tested recovery path for an IdP outage or configuration error.
+
+### PostgreSQL
+
+Stay with SQLite when the deployment is on one host and its backup and restore model fits your requirements. Integrate [PostgreSQL](/selfhosted/maintenance/postgres-store) when you need a separately managed database or are preparing for multiple Management instances and high availability.
+
+Switching stores is a data migration. Do not point a running installation at an empty PostgreSQL database and assume its SQLite state will appear there. Follow [Migrate SQLite to PostgreSQL](/selfhosted/maintenance/scaling/migrate-sqlite-to-postgresql) and verify the migrated data before retiring the original store.
+
+### Existing reverse proxy
+
+Traefik is the recommended default because the combined manual guide includes it and automatic certificate handling. Replace it only when another proxy is already part of your infrastructure or your operating requirements favor a different tool.
+
+The manual guides support:
+
+- [Traefik](/selfhosted/manual-setup/reverse-proxies/traefik) for Docker-native routing and automatic ACME certificates.
+- [Caddy](/selfhosted/manual-setup/reverse-proxies/caddy) for a compact configuration and automatic HTTPS.
+- [Nginx](/selfhosted/manual-setup/reverse-proxies/nginx) for explicit host- or container-based routing, with Certbot or an existing certificate.
+- [Nginx Proxy Manager](/selfhosted/manual-setup/reverse-proxies/nginx-proxy-manager) when an existing NPM installation and UI-managed certificate lifecycle are operational requirements.
+
+Each proxy can front either documented NetBird server layout. Changing the proxy does not require changing the server layout.
+
+### External Signal, Relay, and high availability
+
+A single combined server already includes Signal and Relay. Add [external Signal](/selfhosted/maintenance/scaling/set-up-external-signal) or [external Relay](/selfhosted/maintenance/scaling/set-up-external-relays) services only when your topology or scale requires them.
+
+For service redundancy, follow the [high-availability architecture](/selfhosted/maintenance/scaling/high-availability). Running a second copy of the one-host Compose project behind a load balancer is not a complete HA design; shared state and coordination services must be planned together.
+
+### Observability
+
+Metrics do not change the deployment architecture. Connect the combined server's private metrics and health endpoints to your monitoring system after the core deployment works. The [combined deployment metrics](/selfhosted/observability/combined) and [observability overview](/selfhosted/observability) document the endpoints and their exposure boundaries.
+
+Keep metrics private. They are operational interfaces, not public NetBird client endpoints.
+
+## Check the host and network before installation
+
+For the recommended deployment, prepare:
+
+- One public Linux server with Docker Engine and Docker Compose v2.
+- At least 1 CPU and 1 GB of memory with swap; 2 GB or more provides better headroom for upgrades and diagnostics.
+- A hostname whose A record points to the server. If you publish an AAAA record, IPv6 must reach the same deployment.
+- Public TCP 80 and 443 for Traefik.
+- Public UDP 3478 for STUN.
+- An off-host location for configuration and data backups.
+- A plan to create the first owner as soon as `/setup` becomes public.
+
+If another service already owns TCP 80 or 443, decide whether Traefik will replace it or whether NetBird will integrate with your existing reverse proxy before starting the Compose project.
+
+## Decide what failure you can tolerate
+
+The recommended one-host deployment prioritizes simplicity, not uninterrupted availability. Before using it for critical access, decide:
+
+- How long NetBird may be unavailable while you restore the host.
+- How much state you can afford to lose between backups.
+- Where the store encryption key and backups will be kept.
+- Who can recover access if the primary owner or external IdP is unavailable.
+- How you will monitor HTTPS, container health, disk space, and backup age.
+
+If a single-host outage is outside your acceptable recovery time, plan PostgreSQL and the full HA architecture before onboarding production users.
+
+## Follow the recommended path
+
+For the simplest manual installation with automatic TLS:
+
+1. Complete [Manual combined-container setup](/selfhosted/manual-setup).
+2. Create the first owner immediately after public endpoint verification.
+3. Enroll two clients and verify Management, Signal, Relay, STUN, and peer traffic.
+4. Follow the [backup procedure](/selfhosted/maintenance/backup) before changing the deployment.
+5. Add identity, storage, proxy, monitoring, or scaling integrations only after the base deployment is healthy.
+
+At that point you have a complete NetBird deployment, not a partial installation waiting for an external IdP or database. The integrations above are ways to adapt it to your environment as requirements grow.
diff --git a/src/pages/selfhosted/manual-setup/reverse-proxies/caddy.mdx b/src/pages/selfhosted/manual-setup/reverse-proxies/caddy.mdx
new file mode 100644
index 000000000..bc7be3e17
--- /dev/null
+++ b/src/pages/selfhosted/manual-setup/reverse-proxies/caddy.mdx
@@ -0,0 +1,331 @@
+export const description =
+ "Configure Caddy for manual NetBird combined-container and multi-container deployments with automatic TLS, h2c gRPC, WebSockets, and trusted proxies."
+
+# Use Caddy with a manual NetBird deployment
+
+Caddy provides automatic HTTPS and concise protocol-aware routing for NetBird. This guide covers Caddy installed on the host and Caddy in Docker for both NetBird layouts.
+
+Start with [Manual setup](/selfhosted/manual-setup) for the combined container or [Manual multi-container setup](/selfhosted/manual-setup/multi-container) for the multi-container layout.
+
+## How Caddy routes NetBird
+
+Caddy automatically obtains and renews a public certificate when the hostname resolves to the server and TCP 80/443 are reachable. It forwards WebSocket upgrades automatically. Native gRPC upstreams must use `h2c://` because NetBird's internal gRPC listeners use cleartext HTTP/2 behind the TLS-terminating proxy.
+
+UDP 3478 bypasses Caddy and remains published directly from `netbird-server` or `relay`.
+
+## 1. Configure trusted proxy addresses
+
+For Caddy installed on the host, use the Docker bridge gateway:
+
+Combined `config.yaml`:
+
+```yaml
+server:
+ reverseProxy:
+ trustedHTTPProxies:
+ - "172.30.0.1/32"
+ trustedPeers:
+ - "172.30.0.1/32"
+```
+
+Multi-container `management.json`:
+
+```json
+{
+ "ReverseProxy": {
+ "TrustedHTTPProxies": ["172.30.0.1/32"],
+ "TrustedHTTPProxiesCount": 0,
+ "TrustedPeers": ["172.30.0.1/32"]
+ }
+}
+```
+
+For Caddy running in Docker at `172.30.0.10`, trust `172.30.0.10/32` instead. Trust only the proxy, never `0.0.0.0/0`.
+
+## 2. Install Caddy on the host
+
+On Debian or Ubuntu, use Caddy's official package repository:
+
+```bash
+sudo apt update
+sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https gpg
+
+curl -1sLf https://dl.cloudsmith.io/public/caddy/stable/gpg.key \
+ | sudo gpg --dearmor --yes \
+ -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
+
+curl -1sLf https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt \
+ | sudo tee /etc/apt/sources.list.d/caddy-stable.list >/dev/null
+
+sudo chmod o+r \
+ /usr/share/keyrings/caddy-stable-archive-keyring.gpg \
+ /etc/apt/sources.list.d/caddy-stable.list
+
+sudo apt update
+sudo apt install caddy
+```
+
+The package creates and enables `caddy.service`. Do not start another service on TCP 80 or 443.
+
+## Combined container layout
+
+The NetBird Compose file must publish the private HTTP backends only on loopback and publish STUN publicly:
+
+```yaml
+services:
+ dashboard:
+ ports:
+ - "127.0.0.1:8080:80"
+
+ netbird-server:
+ ports:
+ - "127.0.0.1:8081:80"
+ - "3478:3478/udp"
+```
+
+Replace `/etc/caddy/Caddyfile` with:
+
+```text
+netbird.example.com {
+ # Native Signal, Management, and optional NetBird Proxy gRPC.
+ @grpc path \
+ /signalexchange.SignalExchange/* \
+ /management.ManagementService/* \
+ /management.ProxyService/*
+ reverse_proxy @grpc h2c://127.0.0.1:8081
+
+ # REST, OIDC, Relay, and gRPC-over-WebSocket fallback paths.
+ @backend path \
+ /relay* \
+ /ws-proxy/* \
+ /api/* \
+ /oauth2/*
+ reverse_proxy @backend 127.0.0.1:8081
+
+ # Dashboard catch-all.
+ reverse_proxy 127.0.0.1:8080
+}
+```
+
+Replace the hostname, validate, format, and reload:
+
+```bash
+sudo caddy validate --config /etc/caddy/Caddyfile
+sudo caddy fmt --overwrite /etc/caddy/Caddyfile
+sudo systemctl enable --now caddy
+sudo systemctl reload caddy
+```
+
+Caddy evaluates the named routes before the final catch-all. Do not move the Dashboard proxy above them.
+
+## Multi-container layout
+
+Use this Caddyfile with the loopback ports from [Manual multi-container setup](/selfhosted/manual-setup/multi-container):
+
+```text
+netbird.example.com {
+ # Signal native gRPC and WebSocket fallback.
+ @signal_grpc path /signalexchange.SignalExchange/*
+ reverse_proxy @signal_grpc h2c://127.0.0.1:10000
+
+ @signal_ws path /ws-proxy/signal*
+ reverse_proxy @signal_ws 127.0.0.1:8083
+
+ # Management native gRPC, REST, OIDC, and WebSocket fallback.
+ @management_grpc path \
+ /management.ManagementService/* \
+ /management.ProxyService/*
+ reverse_proxy @management_grpc h2c://127.0.0.1:8081
+
+ @management_http path \
+ /api/* \
+ /oauth2/* \
+ /ws-proxy/management*
+ reverse_proxy @management_http 127.0.0.1:8081
+
+ # Secure Relay WebSocket.
+ @relay path /relay*
+ reverse_proxy @relay 127.0.0.1:8084
+
+ # Dashboard catch-all.
+ reverse_proxy 127.0.0.1:8080
+}
+```
+
+Replace the hostname, then validate and reload:
+
+```bash
+sudo caddy validate --config /etc/caddy/Caddyfile
+sudo caddy fmt --overwrite /etc/caddy/Caddyfile
+sudo systemctl reload caddy
+```
+
+The `relay` service must still publish `3478:3478/udp` directly.
+
+## Caddy running in Docker
+
+Create or reuse the private network and assign Caddy the same fixed proxy address used in NetBird's trust settings:
+
+```bash
+docker network create \
+ --driver bridge \
+ --subnet 172.30.0.0/24 \
+ --gateway 172.30.0.1 \
+ netbird-proxy
+```
+
+Example Caddy Compose file:
+
+```yaml
+services:
+ caddy:
+ image: caddy:2
+ container_name: netbird-caddy
+ restart: unless-stopped
+ networks:
+ netbird-proxy:
+ ipv4_address: 172.30.0.10
+ ports:
+ - "80:80"
+ - "443:443"
+ - "443:443/udp"
+ volumes:
+ - ./Caddyfile:/etc/caddy/Caddyfile:ro
+ - caddy_data:/data
+ - caddy_config:/config
+
+volumes:
+ caddy_data:
+ caddy_config:
+
+networks:
+ netbird-proxy:
+ external: true
+```
+
+TCP 443 is required for NetBird. The optional UDP 443 mapping enables HTTP/3 for compatible web clients; it is unrelated to NetBird's required UDP 3478 STUN listener.
+
+### Combined Docker Caddyfile
+
+```text
+netbird.example.com {
+ @grpc path \
+ /signalexchange.SignalExchange/* \
+ /management.ManagementService/* \
+ /management.ProxyService/*
+ reverse_proxy @grpc h2c://netbird-server:80
+
+ @backend path \
+ /relay* \
+ /ws-proxy/* \
+ /api/* \
+ /oauth2/*
+ reverse_proxy @backend netbird-server:80
+
+ reverse_proxy netbird-dashboard:80
+}
+```
+
+### Multi-container Docker Caddyfile
+
+```text
+netbird.example.com {
+ @signal_grpc path /signalexchange.SignalExchange/*
+ reverse_proxy @signal_grpc h2c://netbird-signal:10000
+
+ @signal_ws path /ws-proxy/signal*
+ reverse_proxy @signal_ws netbird-signal:80
+
+ @management_grpc path \
+ /management.ManagementService/* \
+ /management.ProxyService/*
+ reverse_proxy @management_grpc h2c://netbird-management:80
+
+ @management_http path \
+ /api/* \
+ /oauth2/* \
+ /ws-proxy/management*
+ reverse_proxy @management_http netbird-management:80
+
+ @relay path /relay*
+ reverse_proxy @relay netbird-relay:80
+
+ reverse_proxy netbird-dashboard:80
+}
+```
+
+Join every referenced NetBird service to `netbird-proxy`, remove unnecessary HTTP port publishing, keep UDP 3478 published, and set both trusted proxy lists to `172.30.0.10/32`.
+
+Validate before starting:
+
+```bash
+docker run --rm \
+ -v "$PWD/Caddyfile:/etc/caddy/Caddyfile:ro" \
+ caddy:2 \
+ caddy validate --config /etc/caddy/Caddyfile
+
+docker compose config --quiet
+docker compose up -d
+```
+
+## Verify Caddy and NetBird
+
+Check Caddy's service and recent logs:
+
+```bash
+sudo systemctl --no-pager --full status caddy
+sudo journalctl -u caddy --since '10 minutes ago' --no-pager
+```
+
+For Docker:
+
+```bash
+docker compose ps
+docker compose logs --tail=100 caddy
+```
+
+From another network:
+
+```bash
+NETBIRD_DOMAIN="netbird.example.com"
+
+curl -fsSI "http://${NETBIRD_DOMAIN}/"
+curl -fsSI "https://${NETBIRD_DOMAIN}/setup"
+curl -fsS "https://${NETBIRD_DOMAIN}/oauth2/.well-known/openid-configuration"
+```
+
+Then run the gRPC and WebSocket checks from [Manual multi-container setup](/selfhosted/manual-setup/multi-container#9-verify-the-complete-deployment). Create the first owner and enroll two clients to verify Management, Signal, STUN, and Relay.
+
+## Troubleshooting Caddy
+
+### Automatic HTTPS does not issue a certificate
+
+Check that public DNS resolves directly to this host and TCP 80/443 are reachable. Caddy logs the ACME reason:
+
+```bash
+sudo journalctl -u caddy --since '30 minutes ago' --no-pager
+```
+
+Do not use `tls internal` for public NetBird clients unless every client explicitly trusts your private CA.
+
+### gRPC returns HTML or an HTTP status instead of gRPC
+
+The Dashboard catch-all handled the request. Keep the `@grpc` matcher above the final `reverse_proxy` and keep `h2c://` on each native gRPC upstream.
+
+### WebSockets disconnect
+
+Caddy handles upgrade headers automatically. If connections still close, inspect any upstream load balancer or CDN and ensure the hostname is DNS-only. Verify that the matched upstream is NetBird Server, Signal, Management, or Relay—not Dashboard.
+
+### Caddy cannot reach a Docker backend
+
+Confirm all containers share the same network:
+
+```bash
+docker network inspect netbird-proxy
+```
+
+Use container names only when Caddy is in Docker. A host-installed Caddy must use the loopback mappings instead.
+
+### Forwarded addresses are ignored
+
+Confirm whether Caddy uses `172.30.0.1` (host install) or `172.30.0.10` (Docker), and place that exact `/32` in both `trustedHTTPProxies` and `trustedPeers`. Restart the NetBird server or Management service after changing it.
diff --git a/src/pages/selfhosted/manual-setup/reverse-proxies/nginx-proxy-manager.mdx b/src/pages/selfhosted/manual-setup/reverse-proxies/nginx-proxy-manager.mdx
new file mode 100644
index 000000000..f8cbfe2f6
--- /dev/null
+++ b/src/pages/selfhosted/manual-setup/reverse-proxies/nginx-proxy-manager.mdx
@@ -0,0 +1,365 @@
+import {Note, Warning} from "@/components/mdx"
+
+export const description =
+ "Configure Nginx Proxy Manager for manual NetBird combined-container and multi-container deployments with TLS, gRPC, WebSockets, and a private admin UI."
+
+# Use Nginx Proxy Manager with a manual NetBird deployment
+
+Nginx Proxy Manager (NPM) can terminate TLS for NetBird, but its standard forwarding fields are not enough. NetBird uses path-specific HTTP, WebSocket, and native gRPC backends, so you must paste the complete routing block into the Proxy Host's **Advanced** tab.
+
+This guide covers both the combined-container and multi-container NetBird layouts. Start with [Manual setup](/selfhosted/manual-setup) or [Manual multi-container setup](/selfhosted/manual-setup/multi-container).
+
+
+Do not expose NPM's administration port 81 to the Internet. The Compose example binds it to loopback. Use an SSH tunnel or a private NetBird connection to administer it.
+
+
+## 1. Create or reuse the proxy network
+
+The examples place NPM at a fixed address so NetBird can trust exactly one proxy:
+
+```bash
+docker network create \
+ --driver bridge \
+ --subnet 172.30.0.0/24 \
+ --gateway 172.30.0.1 \
+ netbird-proxy
+```
+
+If the manual NetBird Compose project already created `netbird-proxy`, do not recreate it.
+
+Combined `config.yaml`:
+
+```yaml
+server:
+ reverseProxy:
+ trustedHTTPProxies:
+ - "172.30.0.10/32"
+ trustedPeers:
+ - "172.30.0.10/32"
+```
+
+Multi-container `management.json`:
+
+```json
+{
+ "ReverseProxy": {
+ "TrustedHTTPProxies": ["172.30.0.10/32"],
+ "TrustedHTTPProxiesCount": 0,
+ "TrustedPeers": ["172.30.0.10/32"]
+ }
+}
+```
+
+Trust only NPM's fixed `/32`, not the whole subnet.
+
+## 2. Deploy Nginx Proxy Manager
+
+Create a separate directory and Compose file:
+
+```bash
+install -d -m 700 "$HOME/nginx-proxy-manager"
+cd "$HOME/nginx-proxy-manager"
+```
+
+```yaml
+services:
+ npm:
+ image: jc21/nginx-proxy-manager:2.15.1
+ container_name: nginx-proxy-manager
+ restart: unless-stopped
+ networks:
+ netbird-proxy:
+ ipv4_address: 172.30.0.10
+ ports:
+ - "80:80"
+ - "443:443"
+ - "127.0.0.1:81:81"
+ environment:
+ DISABLE_IPV6: "true"
+ volumes:
+ - npm_data:/data
+ - npm_letsencrypt:/etc/letsencrypt
+
+volumes:
+ npm_data:
+ npm_letsencrypt:
+
+networks:
+ netbird-proxy:
+ external: true
+```
+
+If the host has working IPv6 and the container is configured for it, remove `DISABLE_IPV6`. If the NetBird hostname publishes an AAAA record, TCP 80/443 over IPv6 must reach NPM too.
+
+Validate and start:
+
+```bash
+docker compose config --quiet
+docker compose pull
+docker compose up -d
+docker compose logs --tail=100 npm
+```
+
+Wait until the log reports that the backend is listening on port 3000.
+
+## 3. Create the NPM owner
+
+NPM 2.15 uses a first-run setup screen; it does not rely on the historical `admin@example.com`/`changeme` credentials.
+
+From your workstation, open an SSH tunnel to the server:
+
+```bash
+ssh -L 8181:127.0.0.1:81 user@netbird.example.com
+```
+
+Open `http://127.0.0.1:8181`, create the NPM owner, and use a real email address. NPM supplies that address to Let's Encrypt when requesting certificates.
+
+
+Keep the SSH session open while configuring NPM. The public NetBird hostname should serve only NPM's ports 80 and 443, never its admin interface.
+
+
+## Combined container layout
+
+The Dashboard and `netbird-server` must share `netbird-proxy` with NPM. No HTTP backend ports need to be public:
+
+```yaml
+services:
+ dashboard:
+ networks: [netbird-proxy]
+
+ netbird-server:
+ networks: [netbird-proxy]
+ ports:
+ - "3478:3478/udp"
+
+networks:
+ netbird-proxy:
+ external: true
+```
+
+### Create the Proxy Host
+
+In **Hosts → Proxy Hosts → Add Proxy Host**, set:
+
+**Details**
+
+- Domain Names: `netbird.example.com`
+- Scheme: `http`
+- Forward Hostname / IP: `netbird-dashboard`
+- Forward Port: `80`
+- Block Common Exploits: enabled
+- Websockets Support: enabled
+
+**SSL**
+
+- Request a new SSL Certificate, or select an existing valid certificate.
+- Force SSL: enabled.
+- HTTP/2 Support: enabled. Native NetBird gRPC requires it.
+- Agree to the Let's Encrypt terms when requesting a certificate.
+
+**Advanced**
+
+Paste the complete block:
+
+```nginx
+client_header_timeout 1d;
+client_body_timeout 1d;
+
+# Relay and gRPC-over-WebSocket fallback paths.
+location ~ ^/(relay|ws-proxy/) {
+ proxy_pass http://netbird-server:80;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ 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_read_timeout 1d;
+}
+
+# Native Signal, Management, and optional NetBird Proxy gRPC.
+location ~ ^/(signalexchange\.SignalExchange|management\.(ManagementService|ProxyService))/ {
+ grpc_pass grpc://netbird-server:80;
+ grpc_read_timeout 1d;
+ grpc_send_timeout 1d;
+ grpc_socket_keepalive on;
+}
+
+# REST API and embedded identity provider.
+location ~ ^/(api|oauth2)/ {
+ proxy_pass http://netbird-server:80;
+ 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;
+}
+```
+
+The Proxy Host's normal destination remains the Dashboard catch-all. The Advanced locations override it for every backend path.
+
+## Multi-container layout
+
+Join `dashboard`, `management`, `signal`, and `relay` to `netbird-proxy`; keep UDP 3478 published from Relay.
+
+### Create the Proxy Host
+
+Use the same Details and SSL settings as the combined layout, with `netbird-dashboard:80` as the normal destination. In **Advanced**, paste:
+
+```nginx
+client_header_timeout 1d;
+client_body_timeout 1d;
+
+# Secure Relay WebSocket.
+location /relay {
+ proxy_pass http://netbird-relay:80;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ 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_read_timeout 1d;
+}
+
+# Signal WebSocket fallback.
+location /ws-proxy/signal {
+ proxy_pass http://netbird-signal:80;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ 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_read_timeout 1d;
+}
+
+# Signal native gRPC.
+location /signalexchange.SignalExchange/ {
+ grpc_pass grpc://netbird-signal:10000;
+ grpc_read_timeout 1d;
+ grpc_send_timeout 1d;
+ grpc_socket_keepalive on;
+}
+
+# Management REST API and embedded identity provider.
+location ~ ^/(api|oauth2)/ {
+ proxy_pass http://netbird-management:80;
+ 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;
+}
+
+# Management WebSocket fallback.
+location /ws-proxy/management {
+ proxy_pass http://netbird-management:80;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ 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_read_timeout 1d;
+}
+
+# Management and optional NetBird Proxy native gRPC.
+location ~ ^/management\.(ManagementService|ProxyService)/ {
+ grpc_pass grpc://netbird-management:80;
+ grpc_read_timeout 1d;
+ grpc_send_timeout 1d;
+ grpc_socket_keepalive on;
+}
+```
+
+Do not use NPM's Custom Locations screen for these routes. The Advanced block preserves the required prefix matching, WebSocket settings, and distinct gRPC upstreams.
+
+## Reaching backends through the host
+
+The shared Docker network is recommended. Inside the NPM container, `127.0.0.1` means the NPM container itself—not the Docker host.
+
+If NPM cannot join the NetBird network, add the Linux host gateway to its Compose service:
+
+```yaml
+services:
+ npm:
+ extra_hosts:
+ - "host.docker.internal:host-gateway"
+```
+
+Then replace the backend hostnames in the Advanced block with `host.docker.internal` and use the loopback-published port numbers only if Docker allows the container to reach those bindings. On many Linux systems a host service bound strictly to `127.0.0.1` is not reachable through `host.docker.internal`; bind it to the bridge address and firewall it, or use the shared network instead.
+
+
+Do not solve backend reachability by publishing Management, Signal, Relay HTTP, or Dashboard ports on `0.0.0.0`. That bypasses NPM and exposes unauthenticated internal listeners.
+
+
+## Verify NPM and NetBird
+
+Confirm that NPM generated a host configuration and has no reload errors:
+
+```bash
+docker logs --tail=100 nginx-proxy-manager
+docker exec nginx-proxy-manager nginx -t
+```
+
+From a machine outside the server network:
+
+```bash
+NETBIRD_DOMAIN="netbird.example.com"
+
+curl -fsSI "http://${NETBIRD_DOMAIN}/"
+curl -fsSI "https://${NETBIRD_DOMAIN}/setup"
+curl -fsS "https://${NETBIRD_DOMAIN}/oauth2/.well-known/openid-configuration"
+```
+
+Run the native gRPC and Relay WebSocket probes from [Manual multi-container setup](/selfhosted/manual-setup/multi-container#9-verify-the-complete-deployment). Then create the first NetBird owner and enroll two clients. Confirm Management and Signal are connected and that secure Relay is available in `netbird status --detail`.
+
+## Troubleshooting Nginx Proxy Manager
+
+### The Proxy Host is offline
+
+NPM resolves Docker names only on a shared network. Check membership and DNS:
+
+```bash
+docker network inspect netbird-proxy
+docker exec nginx-proxy-manager getent hosts netbird-dashboard
+```
+
+For combined-container deployments, also resolve `netbird-server`. For multi-container deployments, resolve `netbird-management`, `netbird-signal`, and `netbird-relay`.
+
+### Certificate request fails
+
+Confirm the domain points directly to this host, TCP 80 and 443 are reachable, and no other service owns those ports. Check NPM's logs and ensure the NPM owner's email is valid.
+
+### Dashboard loads but gRPC fails
+
+Enable **HTTP/2 Support** in the SSL tab. Confirm the Advanced block uses `grpc_pass` for native gRPC and that the request path is not falling through to `netbird-dashboard`.
+
+### WebSocket paths return `400` or disconnect
+
+Confirm **Websockets Support** is enabled and keep the explicit upgrade headers and one-day read timeout in every WebSocket location.
+
+### Saving Advanced configuration makes NPM fail to reload
+
+Use NPM's generated error message and container log to locate the syntax error:
+
+```bash
+docker logs --tail=100 nginx-proxy-manager
+docker exec nginx-proxy-manager nginx -t
+```
+
+Do not paste an entire `server {}` block into Advanced; paste only the directives and `location` blocks shown here.
+
+### Forwarded addresses are ignored
+
+Confirm NPM's fixed address:
+
+```bash
+docker inspect nginx-proxy-manager \
+ --format '{{(index .NetworkSettings.Networks "netbird-proxy").IPAddress}}'
+```
+
+Put that exact `/32` in both `trustedHTTPProxies` and `trustedPeers`, then restart NetBird Server or the multi-container Management service.
diff --git a/src/pages/selfhosted/manual-setup/reverse-proxies/nginx.mdx b/src/pages/selfhosted/manual-setup/reverse-proxies/nginx.mdx
new file mode 100644
index 000000000..5141b6e0f
--- /dev/null
+++ b/src/pages/selfhosted/manual-setup/reverse-proxies/nginx.mdx
@@ -0,0 +1,431 @@
+import {Note, Warning} from "@/components/mdx"
+
+export const description =
+ "Configure Nginx and Certbot for manual NetBird combined-container and multi-container deployments with gRPC, WebSockets, and trusted proxies."
+
+# Use Nginx with a manual NetBird deployment
+
+This guide installs Nginx on the NetBird host, obtains a Let's Encrypt certificate with Certbot, and routes either NetBird layout. It also lists the upstream changes for Nginx running in Docker.
+
+Start with [Manual setup](/selfhosted/manual-setup) for the combined layout or [Manual multi-container setup](/selfhosted/manual-setup/multi-container) for the multi-container layout.
+
+## How Nginx routes NetBird
+
+Nginx terminates TLS and accepts HTTP/2 from NetBird clients. It sends native gRPC to an h2c backend with `grpc_pass`, sends Relay and fallback paths with WebSocket upgrade headers, and sends the Dashboard to a catch-all HTTP upstream.
+
+UDP 3478 bypasses Nginx and must remain published directly from `netbird-server` or `relay`.
+
+
+Do not replace `grpc_pass` with `proxy_pass` for native gRPC paths. Do not send gRPC paths through the Dashboard catch-all. Either mistake can leave the Dashboard working while every client fails to connect.
+
+
+## 1. Configure trusted proxy addresses
+
+When Nginx is installed on the host and connects through Docker-published loopback ports, NetBird sees the source as the private bridge gateway used by these manual guides: `172.30.0.1`.
+
+Combined `config.yaml`:
+
+```yaml
+server:
+ reverseProxy:
+ trustedHTTPProxies:
+ - "172.30.0.1/32"
+ trustedPeers:
+ - "172.30.0.1/32"
+```
+
+Multi-container `management.json`:
+
+```json
+{
+ "ReverseProxy": {
+ "TrustedHTTPProxies": ["172.30.0.1/32"],
+ "TrustedHTTPProxiesCount": 0,
+ "TrustedPeers": ["172.30.0.1/32"]
+ }
+}
+```
+
+If Nginx runs in Docker at the fixed address `172.30.0.10`, trust `172.30.0.10/32` instead. Never trust `0.0.0.0/0`.
+
+## 2. Install Nginx and Certbot
+
+On Ubuntu or Debian:
+
+```bash
+sudo apt update
+sudo apt install nginx certbot python3-certbot-nginx
+sudo systemctl enable --now nginx
+```
+
+Ensure no other process owns TCP 80 or 443:
+
+```bash
+sudo ss -lntp | grep -E ':(80|443)\b' || true
+```
+
+## 3. Bootstrap the certificate
+
+Create a temporary HTTP-only server. Replace the hostname first:
+
+```bash
+NETBIRD_DOMAIN="netbird.example.com"
+
+sudo tee /etc/nginx/sites-available/netbird >/dev/null <
+The examples below use Nginx's current `http2 on;` syntax. On Nginx older than 1.25.1, put `http2` on each `listen 443 ssl http2;` directive and omit `http2 on;`.
+
+
+## Combined container layout
+
+The combined layout expects these loopback mappings in the NetBird Compose file:
+
+```yaml
+services:
+ dashboard:
+ ports:
+ - "127.0.0.1:8080:80"
+
+ netbird-server:
+ ports:
+ - "127.0.0.1:8081:80"
+ - "3478:3478/udp"
+```
+
+Replace `/etc/nginx/sites-available/netbird` with:
+
+```nginx
+upstream netbird_dashboard {
+ server 127.0.0.1:8080;
+ keepalive 10;
+}
+
+upstream netbird_server {
+ server 127.0.0.1:8081;
+}
+
+server {
+ listen 80;
+ listen [::]:80;
+ server_name netbird.example.com;
+ return 301 https://$host$request_uri;
+}
+
+server {
+ listen 443 ssl;
+ listen [::]:443 ssl;
+ http2 on;
+ server_name netbird.example.com;
+
+ ssl_certificate /etc/letsencrypt/live/netbird.example.com/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/netbird.example.com/privkey.pem;
+ ssl_protocols TLSv1.2 TLSv1.3;
+
+ 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-Forwarded-Proto https;
+ proxy_set_header X-Forwarded-Host $host;
+ grpc_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+
+ # Relay and gRPC-over-WebSocket fallback paths.
+ location ~ ^/(relay|ws-proxy/) {
+ proxy_pass http://netbird_server;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ 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 https;
+ proxy_set_header X-Forwarded-Host $host;
+ proxy_read_timeout 1d;
+ }
+
+ # Native Signal, Management, and optional NetBird Proxy gRPC.
+ location ~ ^/(signalexchange\.SignalExchange|management\.(ManagementService|ProxyService))/ {
+ grpc_pass grpc://netbird_server;
+ grpc_read_timeout 1d;
+ grpc_send_timeout 1d;
+ grpc_socket_keepalive on;
+ }
+
+ # REST API and embedded identity provider.
+ location ~ ^/(api|oauth2)/ {
+ proxy_pass http://netbird_server;
+ 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 https;
+ proxy_set_header X-Forwarded-Host $host;
+ }
+
+ # Dashboard catch-all must remain last and least specific.
+ location / {
+ proxy_pass http://netbird_dashboard;
+ proxy_http_version 1.1;
+ proxy_set_header Connection "";
+ }
+}
+```
+
+Replace every `netbird.example.com` occurrence, then validate and reload:
+
+```bash
+sudo nginx -t
+sudo systemctl reload nginx
+sudo certbot renew --dry-run
+```
+
+## Multi-container layout
+
+The Compose file from [Manual multi-container setup](/selfhosted/manual-setup/multi-container) publishes all private backends on loopback. Use this Nginx configuration:
+
+```nginx
+upstream netbird_dashboard {
+ server 127.0.0.1:8080;
+ keepalive 10;
+}
+
+upstream netbird_signal_grpc {
+ server 127.0.0.1:10000;
+}
+
+upstream netbird_signal_http {
+ server 127.0.0.1:8083;
+}
+
+upstream netbird_management {
+ server 127.0.0.1:8081;
+}
+
+upstream netbird_relay {
+ server 127.0.0.1:8084;
+}
+
+server {
+ listen 80;
+ listen [::]:80;
+ server_name netbird.example.com;
+ return 301 https://$host$request_uri;
+}
+
+server {
+ listen 443 ssl;
+ listen [::]:443 ssl;
+ http2 on;
+ server_name netbird.example.com;
+
+ ssl_certificate /etc/letsencrypt/live/netbird.example.com/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/netbird.example.com/privkey.pem;
+ ssl_protocols TLSv1.2 TLSv1.3;
+
+ 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-Forwarded-Proto https;
+ proxy_set_header X-Forwarded-Host $host;
+ grpc_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+
+ location /relay {
+ proxy_pass http://netbird_relay;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ 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 https;
+ proxy_set_header X-Forwarded-Host $host;
+ proxy_read_timeout 1d;
+ }
+
+ location /ws-proxy/signal {
+ proxy_pass http://netbird_signal_http;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ 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 https;
+ proxy_set_header X-Forwarded-Host $host;
+ proxy_read_timeout 1d;
+ }
+
+ location /signalexchange.SignalExchange/ {
+ grpc_pass grpc://netbird_signal_grpc;
+ grpc_read_timeout 1d;
+ grpc_send_timeout 1d;
+ grpc_socket_keepalive on;
+ }
+
+ location ~ ^/(api|oauth2)/ {
+ proxy_pass http://netbird_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 https;
+ proxy_set_header X-Forwarded-Host $host;
+ }
+
+ location /ws-proxy/management {
+ proxy_pass http://netbird_management;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ 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 https;
+ proxy_set_header X-Forwarded-Host $host;
+ proxy_read_timeout 1d;
+ }
+
+ location ~ ^/management\.(ManagementService|ProxyService)/ {
+ grpc_pass grpc://netbird_management;
+ grpc_read_timeout 1d;
+ grpc_send_timeout 1d;
+ grpc_socket_keepalive on;
+ }
+
+ location / {
+ proxy_pass http://netbird_dashboard;
+ proxy_http_version 1.1;
+ proxy_set_header Connection "";
+ }
+}
+```
+
+Replace the hostname, then run:
+
+```bash
+sudo nginx -t
+sudo systemctl reload nginx
+sudo certbot renew --dry-run
+```
+
+The `relay` service must still publish UDP 3478 directly.
+
+## Nginx running in Docker
+
+When Nginx is a container, join it and all NetBird services to `netbird-proxy`, assign Nginx `172.30.0.10`, and use these upstreams instead of loopback:
+
+| Layout | Logical upstream | Docker upstream |
+|---|---|---|
+| Combined | Dashboard | `netbird-dashboard:80` |
+| Combined | Server | `netbird-server:80` |
+| Multi-container | Dashboard | `netbird-dashboard:80` |
+| Multi-container | Signal gRPC | `netbird-signal:10000` |
+| Multi-container | Signal WebSocket | `netbird-signal:80` |
+| Multi-container | Management | `netbird-management:80` |
+| Multi-container | Relay | `netbird-relay:80` |
+
+Mount the certificate and configuration paths into Nginx, or run ACME from a companion container. Change NetBird's trusted proxy address to `172.30.0.10/32`. Do not publish the NetBird HTTP backend ports when Nginx can reach them by container name.
+
+## Verify Nginx and NetBird
+
+Check listeners and Nginx state:
+
+```bash
+sudo systemctl --no-pager --full status nginx
+sudo ss -lntp | grep -E ':(80|443)\b'
+```
+
+From another network, verify HTTPS and OIDC:
+
+```bash
+NETBIRD_DOMAIN="netbird.example.com"
+
+curl -fsSI "http://${NETBIRD_DOMAIN}/"
+curl -fsSI "https://${NETBIRD_DOMAIN}/setup"
+curl -fsS "https://${NETBIRD_DOMAIN}/oauth2/.well-known/openid-configuration"
+```
+
+Then run the gRPC and WebSocket probes from [Manual multi-container setup](/selfhosted/manual-setup/multi-container#9-verify-the-complete-deployment). Create the first owner and enroll two clients to prove Management, Signal, and Relay—not only the Dashboard.
+
+## Troubleshooting Nginx
+
+### `nginx -t` reports that `http2` is invalid
+
+Your Nginx is older than 1.25.1. Change:
+
+```nginx
+listen 443 ssl;
+http2 on;
+```
+
+to:
+
+```nginx
+listen 443 ssl http2;
+```
+
+### The gRPC probe returns HTML
+
+The request reached the Dashboard. Check the exact `location` expression, keep it above the catch-all, and make sure it uses `grpc_pass grpc://...`.
+
+### WebSocket requests return `400` or disconnect
+
+Confirm HTTP/1.1, `Upgrade`, `Connection`, and the one-day read timeout are present. Check both Nginx's error log and the target service log:
+
+```bash
+sudo tail -n 100 /var/log/nginx/error.log
+docker compose logs --tail=100 netbird-server
+```
+
+For the multi-container layout, inspect `signal`, `relay`, or `management` according to the failing path.
+
+### Certificate issuance or renewal fails
+
+Confirm DNS points directly to this host and TCP 80 is reachable. Validate Nginx before retrying Certbot:
+
+```bash
+sudo nginx -t
+sudo certbot renew --dry-run
+```
+
+### Forwarded addresses are ignored
+
+Check the actual bridge gateway:
+
+```bash
+docker network inspect netbird-proxy \
+ --format '{{(index .IPAM.Config 0).Gateway}}'
+```
+
+Use that single address with `/32` in both `trustedHTTPProxies` and `trustedPeers`, then restart the NetBird server or Management container.
diff --git a/src/pages/selfhosted/manual-setup/reverse-proxies/traefik.mdx b/src/pages/selfhosted/manual-setup/reverse-proxies/traefik.mdx
new file mode 100644
index 000000000..a027ef476
--- /dev/null
+++ b/src/pages/selfhosted/manual-setup/reverse-proxies/traefik.mdx
@@ -0,0 +1,312 @@
+import {Warning} from "@/components/mdx"
+
+export const description =
+ "Configure Traefik for manual NetBird combined-container and multi-container deployments, including TLS, h2c gRPC, WebSockets, and trusted proxies."
+
+# Use Traefik with a manual NetBird deployment
+
+This guide configures an existing or dedicated Traefik instance for either NetBird layout:
+
+- **Combined container:** `dashboard` plus `netbird-server`. This is recommended for most setups.
+- **Multi-container:** separate `dashboard`, `management`, `signal`, and `relay` services.
+
+Start with [Manual setup](/selfhosted/manual-setup) for the combined layout or [Manual multi-container setup](/selfhosted/manual-setup/multi-container) for the multi-container layout. The examples below assume that Traefik and NetBird share the external Docker network `netbird-proxy`, with Traefik fixed at `172.30.0.10`.
+
+## What Traefik must handle
+
+Traefik terminates public TLS on TCP 443. It uses normal HTTP upstreams for the Dashboard, API, OIDC, Relay, and WebSocket paths, and an `h2c` service for native gRPC paths. NetBird publishes UDP 3478 directly because an HTTP reverse proxy cannot carry STUN.
+
+
+Do not route gRPC requests to the Dashboard fallback. A Dashboard that loads successfully does not prove that clients can connect. Keep the gRPC routers at a higher priority and set their upstream scheme to `h2c`.
+
+
+## 1. Create or reuse the proxy network
+
+If the base NetBird Compose project has not created the network yet, create it once:
+
+```bash
+docker network create \
+ --driver bridge \
+ --subnet 172.30.0.0/24 \
+ --gateway 172.30.0.1 \
+ netbird-proxy
+```
+
+If you use another subnet, change Traefik's fixed address and NetBird's trusted proxy configuration together.
+
+For a combined `config.yaml`:
+
+```yaml
+server:
+ reverseProxy:
+ trustedHTTPProxies:
+ - "172.30.0.10/32"
+ trustedPeers:
+ - "172.30.0.10/32"
+```
+
+For multi-container `management.json`:
+
+```json
+{
+ "ReverseProxy": {
+ "TrustedHTTPProxies": ["172.30.0.10/32"],
+ "TrustedHTTPProxiesCount": 0,
+ "TrustedPeers": ["172.30.0.10/32"]
+ }
+}
+```
+
+Trust the exact proxy address, not the entire Docker subnet.
+
+## 2. Configure Traefik
+
+This dedicated Compose file enables Docker discovery, HTTP-to-HTTPS redirects, long-lived stream timeouts, and Let's Encrypt TLS-ALPN validation. Set a real hostname and contact email in the NetBird project's `.env` before starting it.
+
+```yaml
+services:
+ traefik:
+ image: traefik:v3.6
+ container_name: netbird-traefik
+ restart: unless-stopped
+ networks:
+ netbird-proxy:
+ ipv4_address: 172.30.0.10
+ command:
+ - "--log.level=INFO"
+ - "--accesslog=true"
+ - "--providers.docker=true"
+ - "--providers.docker.exposedbydefault=false"
+ - "--providers.docker.network=netbird-proxy"
+ - "--entrypoints.web.address=:80"
+ - "--entrypoints.websecure.address=:443"
+ - "--entrypoints.websecure.allowACMEByPass=true"
+ - "--entrypoints.websecure.transport.respondingTimeouts.readTimeout=0"
+ - "--entrypoints.websecure.transport.respondingTimeouts.writeTimeout=0"
+ - "--entrypoints.websecure.transport.respondingTimeouts.idleTimeout=0"
+ - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
+ - "--entrypoints.web.http.redirections.entrypoint.scheme=https"
+ - "--certificatesresolvers.letsencrypt.acme.email=${LETSENCRYPT_EMAIL}"
+ - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
+ - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
+ - "--serverstransport.forwardingtimeouts.responseheadertimeout=0s"
+ - "--serverstransport.forwardingtimeouts.idleconntimeout=0s"
+ ports:
+ - "80:80"
+ - "443:443"
+ volumes:
+ - /var/run/docker.sock:/var/run/docker.sock:ro
+ - traefik_letsencrypt:/letsencrypt
+
+volumes:
+ traefik_letsencrypt:
+
+networks:
+ netbird-proxy:
+ external: true
+```
+
+
+Mounting the Docker socket gives Traefik read access to sensitive Docker metadata. Keep `exposedByDefault=false`, do not publish Traefik's insecure API, and consider a Docker socket proxy when your threat model requires stronger isolation.
+
+
+If Traefik already exists, merge the equivalent entrypoint, provider, certificate-resolver, and timeout settings into its static configuration. Use its existing network, HTTPS entrypoint, and resolver names in the labels below.
+
+## Combined container layout
+
+Use these labels on the `dashboard` and `netbird-server` services. Do not publish their HTTP ports when Traefik shares their Docker network; publish only UDP 3478 from `netbird-server`.
+
+```yaml
+services:
+ dashboard:
+ image: netbirdio/dashboard:latest
+ container_name: netbird-dashboard
+ restart: unless-stopped
+ networks: [netbird-proxy]
+ env_file:
+ - ./dashboard.env
+ labels:
+ - "traefik.enable=true"
+ - "traefik.http.routers.netbird-dashboard.rule=Host(`${NETBIRD_DOMAIN}`)"
+ - "traefik.http.routers.netbird-dashboard.entrypoints=websecure"
+ - "traefik.http.routers.netbird-dashboard.tls=true"
+ - "traefik.http.routers.netbird-dashboard.tls.certresolver=letsencrypt"
+ - "traefik.http.routers.netbird-dashboard.priority=1"
+ - "traefik.http.services.netbird-dashboard.loadbalancer.server.port=80"
+
+ netbird-server:
+ image: netbirdio/netbird-server:latest
+ container_name: netbird-server
+ restart: unless-stopped
+ networks: [netbird-proxy]
+ ports:
+ - "3478:3478/udp"
+ volumes:
+ - netbird_data:/var/lib/netbird
+ - ./config.yaml:/etc/netbird/config.yaml:ro
+ command: ["--config", "/etc/netbird/config.yaml"]
+ labels:
+ - "traefik.enable=true"
+
+ # Native gRPC: TLS from the client, h2c to netbird-server.
+ - "traefik.http.routers.netbird-grpc.rule=Host(`${NETBIRD_DOMAIN}`) && (PathPrefix(`/signalexchange.SignalExchange/`) || PathPrefix(`/management.ManagementService/`) || PathPrefix(`/management.ProxyService/`))"
+ - "traefik.http.routers.netbird-grpc.entrypoints=websecure"
+ - "traefik.http.routers.netbird-grpc.tls=true"
+ - "traefik.http.routers.netbird-grpc.tls.certresolver=letsencrypt"
+ - "traefik.http.routers.netbird-grpc.service=netbird-server-h2c"
+ - "traefik.http.routers.netbird-grpc.priority=100"
+
+ # REST, OIDC, Relay, and gRPC-over-WebSocket fallback paths.
+ - "traefik.http.routers.netbird-backend.rule=Host(`${NETBIRD_DOMAIN}`) && (PathPrefix(`/relay`) || PathPrefix(`/ws-proxy/`) || PathPrefix(`/api`) || PathPrefix(`/oauth2`))"
+ - "traefik.http.routers.netbird-backend.entrypoints=websecure"
+ - "traefik.http.routers.netbird-backend.tls=true"
+ - "traefik.http.routers.netbird-backend.tls.certresolver=letsencrypt"
+ - "traefik.http.routers.netbird-backend.service=netbird-server"
+ - "traefik.http.routers.netbird-backend.priority=100"
+
+ - "traefik.http.services.netbird-server.loadbalancer.server.port=80"
+ - "traefik.http.services.netbird-server-h2c.loadbalancer.server.port=80"
+ - "traefik.http.services.netbird-server-h2c.loadbalancer.server.scheme=h2c"
+
+volumes:
+ netbird_data:
+
+networks:
+ netbird-proxy:
+ external: true
+```
+
+The two Traefik services intentionally target the same backend port. Only their upstream protocols differ.
+
+## Multi-container layout
+
+Add these labels to the services from [Manual multi-container setup](/selfhosted/manual-setup/multi-container). Traefik, Dashboard, Management, Signal, and Relay must all join `netbird-proxy`.
+
+```yaml
+services:
+ dashboard:
+ networks: [netbird-proxy]
+ labels:
+ - "traefik.enable=true"
+ - "traefik.http.routers.netbird-dashboard.rule=Host(`${NETBIRD_DOMAIN}`)"
+ - "traefik.http.routers.netbird-dashboard.entrypoints=websecure"
+ - "traefik.http.routers.netbird-dashboard.tls=true"
+ - "traefik.http.routers.netbird-dashboard.tls.certresolver=letsencrypt"
+ - "traefik.http.routers.netbird-dashboard.priority=1"
+ - "traefik.http.services.netbird-dashboard.loadbalancer.server.port=80"
+
+ signal:
+ networks: [netbird-proxy]
+ labels:
+ - "traefik.enable=true"
+ - "traefik.http.routers.netbird-signal-ws.rule=Host(`${NETBIRD_DOMAIN}`) && PathPrefix(`/ws-proxy/signal`)"
+ - "traefik.http.routers.netbird-signal-ws.entrypoints=websecure"
+ - "traefik.http.routers.netbird-signal-ws.tls=true"
+ - "traefik.http.routers.netbird-signal-ws.tls.certresolver=letsencrypt"
+ - "traefik.http.routers.netbird-signal-ws.service=netbird-signal-ws"
+ - "traefik.http.routers.netbird-signal-ws.priority=100"
+ - "traefik.http.services.netbird-signal-ws.loadbalancer.server.port=80"
+ - "traefik.http.routers.netbird-signal-grpc.rule=Host(`${NETBIRD_DOMAIN}`) && PathPrefix(`/signalexchange.SignalExchange/`)"
+ - "traefik.http.routers.netbird-signal-grpc.entrypoints=websecure"
+ - "traefik.http.routers.netbird-signal-grpc.tls=true"
+ - "traefik.http.routers.netbird-signal-grpc.tls.certresolver=letsencrypt"
+ - "traefik.http.routers.netbird-signal-grpc.service=netbird-signal-grpc"
+ - "traefik.http.routers.netbird-signal-grpc.priority=100"
+ - "traefik.http.services.netbird-signal-grpc.loadbalancer.server.port=10000"
+ - "traefik.http.services.netbird-signal-grpc.loadbalancer.server.scheme=h2c"
+
+ relay:
+ networks: [netbird-proxy]
+ labels:
+ - "traefik.enable=true"
+ - "traefik.http.routers.netbird-relay.rule=Host(`${NETBIRD_DOMAIN}`) && PathPrefix(`/relay`)"
+ - "traefik.http.routers.netbird-relay.entrypoints=websecure"
+ - "traefik.http.routers.netbird-relay.tls=true"
+ - "traefik.http.routers.netbird-relay.tls.certresolver=letsencrypt"
+ - "traefik.http.routers.netbird-relay.priority=100"
+ - "traefik.http.services.netbird-relay.loadbalancer.server.port=80"
+
+ management:
+ networks: [netbird-proxy]
+ labels:
+ - "traefik.enable=true"
+ - "traefik.http.routers.netbird-management-grpc.rule=Host(`${NETBIRD_DOMAIN}`) && (PathPrefix(`/management.ManagementService/`) || PathPrefix(`/management.ProxyService/`))"
+ - "traefik.http.routers.netbird-management-grpc.entrypoints=websecure"
+ - "traefik.http.routers.netbird-management-grpc.tls=true"
+ - "traefik.http.routers.netbird-management-grpc.tls.certresolver=letsencrypt"
+ - "traefik.http.routers.netbird-management-grpc.service=netbird-management-h2c"
+ - "traefik.http.routers.netbird-management-grpc.priority=100"
+ - "traefik.http.routers.netbird-management-http.rule=Host(`${NETBIRD_DOMAIN}`) && (PathPrefix(`/api`) || PathPrefix(`/oauth2`) || PathPrefix(`/ws-proxy/management`))"
+ - "traefik.http.routers.netbird-management-http.entrypoints=websecure"
+ - "traefik.http.routers.netbird-management-http.tls=true"
+ - "traefik.http.routers.netbird-management-http.tls.certresolver=letsencrypt"
+ - "traefik.http.routers.netbird-management-http.service=netbird-management"
+ - "traefik.http.routers.netbird-management-http.priority=100"
+ - "traefik.http.services.netbird-management.loadbalancer.server.port=80"
+ - "traefik.http.services.netbird-management-h2c.loadbalancer.server.port=80"
+ - "traefik.http.services.netbird-management-h2c.loadbalancer.server.scheme=h2c"
+
+networks:
+ netbird-proxy:
+ external: true
+```
+
+The `relay` service must still publish `3478:3478/udp` directly. Traefik handles only its secure WebSocket endpoint on TCP 443.
+
+## Start and verify
+
+Validate both Compose projects before starting them:
+
+```bash
+cd "$HOME/netbird"
+docker compose config --quiet
+docker compose up -d
+
+docker compose -f /path/to/traefik/docker-compose.yml config --quiet
+docker compose -f /path/to/traefik/docker-compose.yml up -d
+```
+
+From an external machine, verify OIDC and the certificate:
+
+```bash
+NETBIRD_DOMAIN="netbird.example.com"
+
+curl -fsS "https://${NETBIRD_DOMAIN}/oauth2/.well-known/openid-configuration"
+openssl s_client \
+ -connect "${NETBIRD_DOMAIN}:443" \
+ -servername "$NETBIRD_DOMAIN" /dev/null \
+ | openssl x509 -noout -subject -issuer -dates
+```
+
+Then run the gRPC and WebSocket checks from [Manual multi-container setup](/selfhosted/manual-setup/multi-container#9-verify-the-complete-deployment), create the first owner, and enroll two clients.
+
+## Troubleshooting Traefik
+
+### Traefik serves its default certificate
+
+Check Traefik logs for ACME errors:
+
+```bash
+docker logs --tail=100 netbird-traefik
+```
+
+Confirm that the hostname resolves directly to the server, TCP 443 is reachable, the email is valid, and the ACME storage volume is writable. Do not bypass an untrusted certificate in a NetBird client.
+
+### Dashboard works but clients do not connect
+
+Inspect the access log while a client runs `netbird up`. Native gRPC requests must select the `netbird-grpc`, `netbird-signal-grpc`, or `netbird-management-grpc` router and an `h2c` service. If they hit the Dashboard router, check path spelling and router priority.
+
+### WebSockets close after a fixed interval
+
+Set both entrypoint responding timeouts and server transport forwarding timeouts for long-lived connections. Also check any load balancer in front of Traefik; its idle timeout can terminate an otherwise correct route.
+
+### Forwarded addresses are ignored
+
+Confirm Traefik is actually `172.30.0.10`:
+
+```bash
+docker inspect netbird-traefik \
+ --format '{{(index .NetworkSettings.Networks "netbird-proxy").IPAddress}}'
+```
+
+Then confirm that both `trustedHTTPProxies` and `trustedPeers` contain that exact `/32` address and restart the NetBird server or Management service after changes.
diff --git a/src/pages/selfhosted/migration/enable-reverse-proxy.mdx b/src/pages/selfhosted/migration/enable-reverse-proxy.mdx
index d812417c6..2ec721e50 100644
--- a/src/pages/selfhosted/migration/enable-reverse-proxy.mdx
+++ b/src/pages/selfhosted/migration/enable-reverse-proxy.mdx
@@ -15,7 +15,7 @@ The NetBird proxy container manages its own TLS certificates (via Let's Encrypt
This capability is called **TLS passthrough**, and among common reverse proxies, **only Traefik supports it** via its TCP routers. Other reverse proxies (Nginx, Caddy, Nginx Proxy Manager) terminate TLS themselves and cannot forward the raw encrypted connection, which breaks the proxy's certificate management.
-If your current deployment uses a reverse proxy other than Traefik, you'll need to switch before enabling this feature. See [Switching to Traefik](/selfhosted/external-reverse-proxy#traefik) for instructions.
+If your current deployment uses a reverse proxy other than Traefik, you'll need to switch before enabling this feature. See [Switching to Traefik](/selfhosted/manual-setup/reverse-proxies/traefik) for instructions.
## Overview of changes
@@ -568,7 +568,7 @@ If you use the `extra_hosts` approach above, you **must** assign a static IP to
## For users not on Traefik
-If your self-hosted deployment currently uses Nginx, Caddy, or another reverse proxy, you'll need to switch to Traefik before enabling the Reverse Proxy feature. See the [Traefik setup instructions](/selfhosted/external-reverse-proxy#traefik) for a step-by-step guide on configuring Traefik for your NetBird deployment.
+If your self-hosted deployment currently uses Nginx, Caddy, or another reverse proxy, you'll need to switch to Traefik before enabling the Reverse Proxy feature. See the [Traefik setup instructions](/selfhosted/manual-setup/reverse-proxies/traefik) for a step-by-step guide on configuring Traefik for your NetBird deployment.
## Environment variable reference
diff --git a/src/pages/selfhosted/selfhosted-guide.mdx b/src/pages/selfhosted/selfhosted-guide.mdx
index 3df643630..757beb6f3 100644
--- a/src/pages/selfhosted/selfhosted-guide.mdx
+++ b/src/pages/selfhosted/selfhosted-guide.mdx
@@ -7,9 +7,9 @@ a 3rd party open-source STUN/TURN service [Coturn](https://github.com/coturn/cot
If you would like to learn more about the architecture please refer to the [architecture section](/about-netbird/how-netbird-works).
-
-This guide uses the **older multi-container architecture** with separate `management`, `signal`, `relay`, and `coturn` containers, configured via `management.json`. For new deployments, the [Quickstart guide](/selfhosted/selfhosted-quickstart) uses the **combined setup** with a single `netbird-server` container and `config.yaml`. If you are on the older setup, see the [migration guide](/selfhosted/migration/combined-container) to upgrade.
-
+
+This guide uses the **multi-container architecture** with separate `management`, `signal`, `relay`, and `coturn` containers, configured via `management.json`. The [Quickstart guide](/selfhosted/selfhosted-quickstart) uses the **combined setup** with a single `netbird-server` container and `config.yaml`, which is recommended for most setups. If you want to consolidate an existing multi-container deployment, see the [migration guide](/selfhosted/migration/combined-container).
+
**New to self-hosting?** The [Quickstart guide](/selfhosted/selfhosted-quickstart) uses the built-in identity provider and is the fastest way to get started. This advanced guide is for users who need to integrate with an existing IdP or have specific enterprise requirements.
@@ -216,7 +216,7 @@ Not all reverse proxies are supported as NetBird uses *gRPC* for various compone
**Existing deployments:** Use the configuration templates on the dedicated reverse proxy page.
See the [External Reverse Proxy Configuration](/selfhosted/external-reverse-proxy) page for:
-- Complete configuration templates for [Traefik](/selfhosted/external-reverse-proxy#traefik), [Nginx](/selfhosted/external-reverse-proxy#nginx), [Caddy](/selfhosted/external-reverse-proxy#caddy-external), and [Nginx Proxy Manager](/selfhosted/external-reverse-proxy#nginx-proxy-manager)
+- Complete configuration templates for [Traefik](/selfhosted/manual-setup/reverse-proxies/traefik), [Nginx](/selfhosted/manual-setup/reverse-proxies/nginx), [Caddy](/selfhosted/manual-setup/reverse-proxies/caddy), and [Nginx Proxy Manager](/selfhosted/manual-setup/reverse-proxies/nginx-proxy-manager)
- Required routing endpoints and port mappings
- Docker Compose examples for external proxies
- Troubleshooting tips