Zero-Trust Mesh VPN: How to Deploy NetBird with Docker Compose and Caddy for High-Performance Peer-to-Peer Networking

Eine Netzwerkarchitektin konfiguriert ein NetBird Zero-Trust Mesh VPN mit Docker Compose und Caddy im Rechenzentrum
Self-hosting NetBird with Docker Compose, Coturn STUN/TURN, Management API, and Caddy reverse proxy for high-performance zero-trust overlay networking.

Legacy virtual private networks (VPNs) built on OpenVPN or classic IPsec architectures were designed for an era when corporate workers accessed a centralized physical data center. Today, engineering teams, remote developers, and homelab operators navigate a decentralized reality: workloads span multiple public cloud providers (AWS, Hetzner, GCP), edge Kubernetes clusters (K3s), home server racks, and roving laptops behind double Carrier-Grade NAT (CGNAT).

Centralized hub-and-spoke VPNs introduce painful bottlenecks: every packet between two remote servers must detour through a central gateway, inflating latency, consuming expensive cloud egress bandwidth, and establishing a single point of failure. If the central tunnel dies, your entire fleet loses connectivity.

NetBird resolves this paradigm by combining kernel-level WireGuard encryption with automated Interactive Connectivity Establishment (ICE / WebRTC) signaling and eBPF packet filtering. Rather than tunneling traffic through a central broker, NetBird automatically negotiates direct, encrypted point-to-point mesh tunnels between peers. When direct UDP traversal is blocked by symmetric corporate firewalls, lightweight STUN/TURN relays bridge the connection with negligible overhead.

In this comprehensive guide, we will deploy a fully self-hosted, sovereign NetBird infrastructure using Docker Compose, backed by an embedded SQLite/PostgreSQL datastore, Coturn for STUN/TURN traversal, the NetBird Management and Signal engines, and Caddy with native gRPC multiplexing and automated Let’s Encrypt TLS.

Architecture Blueprint: How NetBird Mesh Orchestration Works

NetBird cleanly decouples the Control Plane (peer registration, policy enforcement, public key distribution) from the Data Plane (WireGuard peer-to-peer traffic). The management server never inspects or proxies your raw network packets.

+---------------------------------------------------------------------------------+
|                           NetBird Control Plane                                 |
|                                                                                 |
|   [ Caddy Reverse Proxy & TLS ] (Ports 80/443 TCP, HTTP/2 & gRPC Multiplexing)   |
|                 |                                             |                 |
|                 v                                             v                 |
|   +---------------------------+                 +---------------------------+   |
|   |  NetBird Management API   |                 |   NetBird Signal Engine   |   |
|   |  - Web UI Dashboard       |<--------------->|   - WebRTC ICE Signaling  |   |
|   |  - ACL Policy Engine      |                 |   - Peer Discovery Broker |   |
|   +---------------------------+                 +---------------------------+   |
|                 |                                                               |
+-----------------|---------------------------------------------------------------+
                  |
                  | Config & WireGuard Keys
                  v
+---------------------------------------------------------------------------------+
|                            Overlay Data Plane (Peers)                           |
|                                                                                 |
|       +----------------------+              +----------------------+            |
|       | Peer A (DevOps Work) |<============>| Peer B (Cloud VPS)   |            |
|       | IP: 100.64.0.5       |  Direct P2P  | IP: 100.64.0.10      |            |
|       +----------------------+  WireGuard   +----------------------+            |
|                   \                 UDP                /                        |
|                    \                                  /                         |
|                     v                                v                          |
|             +------------------------------------------------+                  |
|             |          Coturn STUN / TURN & NetBird Relay    |                  |
|             |        (Fallback only if NAT Traversal fails)  |                  |
|             +------------------------------------------------+                  |
+---------------------------------------------------------------------------------+

Prerequisites & Firewall Port Matrix

Before launching the compose stack, you must allocate a public domain name (e.g., netbird.yourdomain.com) pointing to your public static IP or cloud VPS. Ensure your cloud security group or router firewall forwards the following ports:

  • 80/TCP & 443/TCP: Caddy Reverse Proxy (Web Dashboard, REST API, and gRPC Signaling).
  • 3478/UDP: Coturn STUN service (discovers external public IP/port mappings for NAT punching).
  • 33073/UDP: Coturn TURN relay service (encapsulated fallback relay when symmetric NAT prevents direct P2P).
  • 10000/UDP: NetBird Relay service (high-performance WireGuard fallback protocol).

Step 1: Host Directory Structure & Network Setup

Create a structured installation directory under /opt/netbird and prepare the persistent storage mounts:

sudo mkdir -p /opt/netbird/{management,signal,turn,caddy_data,caddy_config}
cd /opt/netbird

Create an external bridge network so NetBird microservices and Caddy communicate seamlessly without port collisions on the loopback interface:

docker network create netbird-net

Step 2: Environment Configuration (.env)

Generate cryptographic tokens using openssl rand -base64 32. Store these parameters in your .env file:

# /opt/netbird/.env

# Public FQDN (Fully Qualified Domain Name)
NETBIRD_DOMAIN=netbird.yourdomain.com

# Core Component Ports
NETBIRD_MGMT_API_PORT=33073
NETBIRD_SIGNAL_PORT=10000

# Coturn STUN/TURN Authentication
COTURN_SECRET=v9B8kL21mNxP87YtQ45rW63zA10dC99xK72mJ81bN54=
COTURN_PORT=3478
COTURN_MIN_PORT=49152
COTURN_MAX_PORT=65535

# Dashboard & OIDC Authentication
# For initial single-tenant setup without external IdP:
NETBIRD_AUTH_DEVICE_AUTH_PROVIDER=none
NETBIRD_AUTH_SUPPORTED_SCOPES=openid,profile,email

Step 3: Coturn Configuration File

Create the Coturn configuration file at /opt/netbird/turn/turnserver.conf to enforce ephemeral time-limited credentials and strict cryptographic handshakes:

listening-port=3478
tls-listening-port=5349
min-port=49152
max-port=65535

verbose
fingerprint
lt-cred-mech
use-auth-secret
static-auth-secret=v9B8kL21mNxP87YtQ45rW63zA10dC99xK72mJ81bN54=
realm=netbird.yourdomain.com

no-multicast-peers
no-cli
no-loopback-peers

Step 4: Crafting the NetBird Docker Compose Stack

Create docker-compose.yml. This configuration deploys the Management Service, Signal Service, Dashboard Web UI, Coturn, and Caddy:

services:
  coturn:
    image: coturn/coturn:4.6.2-alpine
    container_name: netbird-coturn
    restart: unless-stopped
    network_mode: host
    volumes:
      - ./turn/turnserver.conf:/etc/coturn/turnserver.conf:ro
    command:
      - "-c"
      - "/etc/coturn/turnserver.conf"

  signal:
    image: netbirdio/signal:latest
    container_name: netbird-signal
    restart: unless-stopped
    networks:
      - netbird-net
    command:
      - "--port"
      - "10000"
      - "--log-level"
      - "info"

  management:
    image: netbirdio/management:latest
    container_name: netbird-management
    restart: unless-stopped
    depends_on:
      - signal
    networks:
      - netbird-net
    volumes:
      - ./management:/var/lib/netbird
      - ./management/management.json:/etc/netbird/management.json:ro
    command:
      - "--config"
      - "/etc/netbird/management.json"
      - "--log-level"
      - "info"

  dashboard:
    image: netbirdio/dashboard:latest
    container_name: netbird-dashboard
    restart: unless-stopped
    networks:
      - netbird-net
    environment:
      - NETBIRD_MGMT_API_ENDPOINT=https://${NETBIRD_DOMAIN}:443
      - NETBIRD_MGMT_GRPC_API_ENDPOINT=https://${NETBIRD_DOMAIN}:443
      - NGINX_SSL_PORT=80

  caddy:
    image: caddy:2.8-alpine
    container_name: netbird-caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    networks:
      - netbird-net
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - ./caddy_data:/data
      - ./caddy_config:/config

networks:
  netbird-net:
    external: true

Step 5: Management Service JSON Specification

NetBird’s management daemon reads structural parameters from /opt/netbird/management/management.json. Generate this file with your network subnet allocation and STUN/TURN credentials:

{
  "DataDir": "/var/lib/netbird",
  "HttpConfig": {
    "Address": "0.0.0.0:33073"
  },
  "IdpManagerConfig": {
    "ManagerType": "none"
  },
  "Signal": {
    "URI": "signal:10000",
    "Proto": "http"
  },
  "TURNConfig": {
    "TimeBasedCredentials": true,
    "CredentialsTTL": 86400,
    "Secret": "v9B8kL21mNxP87YtQ45rW63zA10dC99xK72mJ81bN54=",
    "Turns": [
      {
        "Proto": "udp",
        "URI": "turn:netbird.yourdomain.com:3478",
        "Username": "",
        "Password": ""
      }
    ]
  },
  "STUNConfig": {
    "URI": "stun:netbird.yourdomain.com:3478"
  },
  "Network": {
    "Net": "100.64.0.0/16"
  }
}

Step 6: Configuring Caddy with gRPC Multiplexing

The NetBird client communicates with the control plane over two protocols simultaneously on port 443: regular HTTP REST for the dashboard and authentication handshakes, and HTTP/2 gRPC streams for real-time peer topology notifications. Caddy handles both effortlessly via path matching and h2c upstream transport.

Create /opt/netbird/Caddyfile:

netbird.yourdomain.com {
    encode zstd gzip

    # gRPC Traffic for Management API
    @grpc_mgmt {
        protocol grpc
        path /management.ManagementService/*
    }
    reverse_proxy @grpc_mgmt management:33073 {
        transport http {
            versions h2c 2
        }
    }

    # gRPC Traffic for Signal Broker
    @grpc_signal {
        protocol grpc
        path /signal.SignalExchange/*
    }
    reverse_proxy @grpc_signal signal:10000 {
        transport http {
            versions h2c 2
        }
    }

    # REST API Endpoints
    @api {
        path /api/*
    }
    reverse_proxy @api management:33073

    # Web Dashboard UI
    reverse_proxy dashboard:80

    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "SAMEORIGIN"
    }
}

Step 7: Launching NetBird & Initializing Your First Peer

Launch the orchestrated containers in detached mode:

docker compose up -d

Verify that all microservices report healthy and listening:

docker compose ps

Navigate to https://netbird.yourdomain.com in your browser. Upon initial setup, generate an administrative Setup Key under the Setup Keys navigation tab. Choose a reusable key for servers or one-off keys for individual developer workstations.

Connecting a Linux Host via NetBird CLI

On any remote Linux machine, homelab node, or cloud VPS, install the lightweight official NetBird client:

curl -fsSL https://pkgs.netbird.io/install.sh | sh

Connect the peer directly to your self-hosted control plane using your setup key:

sudo netbird up --management-url https://netbird.yourdomain.com --setup-key A8F102B3-4C7D-4E90-B841-F092E15C992E

Inspect the live WireGuard connection status and peer latency:

netbird status -d

The output displays your assigned mesh IP (e.g., 100.64.0.2), direct P2P ICE status (Connected: Direct [P2P]), and the active cryptographic WireGuard interface (wt0).

Advanced Access Control Lists (ACLs) & Routing Rules

Unlike standard VPNs that grant broad network access upon authentication, NetBird implements strict default-deny Zero-Trust policies. In the web dashboard under Access Control, you can construct directional rules:

  • Group Tagging: Assign peers to semantic groups such as homelab-servers, k3s-nodes, developers, and ci-runners.
  • Port & Protocol Restrictions: Allow developers to reach homelab-servers exclusively on TCP port 22 (SSH) and TCP port 443 (HTTPS), while completely blocking arbitrary lateral movement to databases on port 5432.
  • Subnet Routers: Designate a single Docker host or Raspberry Pi as a gateway to expose non-installable devices (printers, IPMI consoles, NAS shares) to the mesh without installing client software on each legacy endpoint.
  • Exit Nodes: Route all public internet traffic from a roaming laptop through an encrypted homelab exit node when connected to insecure public Wi-Fi.

Troubleshooting Common Deployment Pitfalls

1. Client Fails to Connect with “gRPC Transport Unavailable”

Symptom: The NetBird client CLI displays login failed: connection error: desc = "transport: authentication handshake failed: read tcp ... i/o timeout".

Cause: Your reverse proxy is terminating HTTP/1.1 instead of negotiating HTTP/2 prior to proxying gRPC frames to management:33073.

Solution: In your Caddyfile, ensure the transport http block includes versions h2c 2. This instructs Caddy to speak cleartext HTTP/2 to upstream Docker services while presenting fully validated TLS HTTP/2 to the public internet.

2. Peers Fall Back to Relay Mode Instead of Direct P2P

Symptom: netbird status -d indicates Connected: Relay with elevated latency (>100ms) between machines on the same local area network or adjacent cloud regions.

Cause: UDP port 3478 (STUN) or the ephemeral relay range (49152-65535/UDP) is blocked by your hosting provider’s hardware firewall (e.g., AWS Security Group or Hetzner Firewall rule).

Solution: Verify UDP accessibility from an external machine using nc -zvu netbird.yourdomain.com 3478. Ensure Coturn is running with network_mode: host so it can read incoming client UDP packets without Docker bridge NAT translation altering the source port.

3. DNS Conflicts with systemd-resolved on Ubuntu

Symptom: When NetBird connects, custom peer domain names (e.g., database.netbird.cloud) fail to resolve, or general internet browsing stalls.

Cause: NetBird injects an internal DNS stub resolver that collides with Ubuntu’s native systemd-resolved daemon running on 127.0.0.53:53.

Solution: Run systemd-resolve --status wt0 to verify DNS routing scopes. If necessary, configure NetBird’s management server to set a non-conflicting search domain or disable magic DNS for specific peer profiles.

Conclusion

Deploying a self-hosted NetBird mesh gives your infrastructure complete sovereignty over inter-node communication. By migrating from brittle, centralized VPN concentrators to an orchestrated WireGuard mesh with automated NAT traversal, you achieve sub-millisecond local latency, bulletproof cryptographic isolation, and effortless multi-cloud networking. Whether connecting remote microservices, hardening homelab administration, or securing remote worker fleets, NetBird delivers enterprise zero-trust connectivity on your own terms.