[Self-hosted] Proxy Manager Service Creation SSL Issues

Describe the problem

Hello all! I’ve just joined the NetBird self-hosted club after hearing about the introduction of the reverse proxy feature. I’m having some difficulties (probably due to some misunderstandings about the architecture). I don’t want to make this too long but I feel there’s a lot of context, thanks in advance!

So I have a homelab on a Raspberry Pi running a variety of typical services. Jellyfin, Pi-Hole, Immich, etc. Previously, I used Nginx as the proxy manager and NetBird cloud to VPN to my services when away from home. Now I am trying to transition to self-hosting NetBird as a proxy manager and making the services available publicly, lowering the barrier of entry for friends and family looking for something that “just works”. The way I have it set up now is with an old laptop running Arch. The NetBird instance is running in a VM on it (also Arch) which through several layers of firewalls is completely locked down from the rest of my LAN, but ports 80, 443, and 3478/udp are forwarded to it on my router (my best attempt at a VLAN substitution). I am using two domains managed by Cloudflare, andin addition to the management server, also have a NetBird client running so it can connect to the rest of my LAN as a peer, so it can use NetBird IPs and I can block requests to the local subnet.

Setting all that up was a pain, but the real issue I’m having is that when I create a service that points to a peer and port, the certificate is never issued. I’ve tried a couple of different ways of doing ssl: Letting the proxy handle it, letting the netbird-server handle it, downloading certificates from cloudflare and pointing to them. I’m not entirely sure where to start anymore. What’s the best way to go about this?

Are you using NetBird Cloud?

No, I am self-hosting.

NetBird version

0.65.3

Is any other VPN software installed?

No

Debug output

Compose.yml

services:
  # Traefik reverse proxy (automatic TLS via Let's Encrypt)
  traefik:
    image: traefik:v3.6
    container_name: netbird-traefik
    restart: unless-stopped
    networks:
      netbird:
        ipv4_address: 172.30.0.10
    command:
      # Logging
      - "--log.level=INFO"
      - "--accesslog=true"
      # Docker provider
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--providers.docker.network=netbird"
      # Entrypoints
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--entrypoints.websecure.allowACMEByPass=true"
      # Disable timeouts for long-lived gRPC streams
      - "--entrypoints.websecure.transport.respondingTimeouts.readTimeout=0"
      - "--entrypoints.websecure.transport.respondingTimeouts.writeTimeout=0"
      - "--entrypoints.websecure.transport.respondingTimeouts.idleTimeout=0"
      # HTTP to HTTPS redirect
      - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
      - "--entrypoints.web.http.redirections.entrypoint.scheme=https"
      # Let's Encrypt ACME
      - "--certificatesresolvers.letsencrypt.acme.email=example@gmail.com"
      - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
      - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
      # gRPC transport settings
      - "--serverstransport.forwardingtimeouts.responseheadertimeout=0s"
      - "--serverstransport.forwardingtimeouts.idleconntimeout=0s"
      - "--providers.file.filename=/etc/traefik/dynamic.yaml"
    ports:
      - '443:443/tcp'
      - '443:443/udp'
      - '80:80'
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./netbird_traefik_letsencrypt:/letsencrypt
      - ./traefik-dynamic.yaml:/etc/traefik/dynamic.yaml:ro
    logging:
      driver: "json-file"
      options:
        max-size: "500m"
        max-file: "2"

  # UI dashboard
  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.domain1.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.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"

  # Combined server (Management + Signal + Relay + STUN)
  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
    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.domain1.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=1000 #100
      # Backend router (relay, WebSocket, API, OAuth2)
      - traefik.http.routers.netbird-backend.rule=Host(`netbird.domain1.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
    logging:
      driver: "json-file"
      options:
        max-size: "500m"
        max-file: "2"

  # NetBird Proxy - exposes internal resources to the internet
  proxy:
    image: netbirdio/reverse-proxy:latest
    container_name: netbird-proxy
    ports:
    - 51820:51820/udp
    restart: unless-stopped
    networks: [netbird]
    depends_on:
      - netbird-server
    env_file:
      - ./proxy.env
    volumes:
      - ./netbird_proxy_certs:/certs
      - ./netbird_proxy_data:/var/lib/netbird
      - ./wireguard:/var/run/wireguard
    labels:
      # TCP passthrough for any unmatched domain (proxy handles its own TLS)
      - traefik.enable=true
      - traefik.tcp.routers.proxy-passthrough.entrypoints=websecure
      - traefik.tcp.routers.proxy-passthrough.rule=HostSNI(`*`)
      - traefik.tcp.routers.proxy-passthrough.tls.passthrough=true
      - traefik.tcp.routers.proxy-passthrough.service=proxy-tls
      - traefik.tcp.routers.proxy-passthrough.priority=1
      - traefik.tcp.services.proxy-tls.loadbalancer.server.port=8443
      - traefik.tcp.services.proxy-tls.loadbalancer.serverstransport=pp-v2@file
    logging:
      driver: "json-file"
      options:
        max-size: "500m"
        max-file: "2"

volumes:
  netbird_data:
  netbird_traefik_letsencrypt:
  netbird_proxy_certs:

networks:
  netbird:
    name: netbird
    driver: bridge
    ipam:
      config:
        - subnet: 172.30.0.0/24
          gateway: 172.30.0.1

proxy.env

NB_PROXY_TOKEN=nbx_<redacted>
NB_PROXY_MANAGEMENT_ADDRESS=https://netbird.domain1.com
NB_PROXY_DOMAIN=proxy-domain.online
NB_PROXY_ADDRESS=:8443
NB_PROXY_ACME_CERTIFICATES=false
NB_LOG_LEVEL=debug
NB_PROXY_ACME_CHALLENGE_TYPE=tls-alpn-01
NB_PROXY_CERTIFICATE_DIRECTORY=/certs
NB_PROXY_ALLOW_INSECURE=true
NB_PROXY_CERTIFICATE_FILE=tls.crt
NB_PROXY_CERTIFICATE_KEY_FILE=tls.key

Docker proxy log

netbird-proxy  | 2026-02-24T05:52:07.015Z WARN [peer: <redacted>] client/internal/peer/worker_ice.go:160: ICE Agent is not initialized yet
netbird-proxy  | 2026-02-24T05:52:07.015Z WARN [peer: <redacted>] client/internal/peer/worker_ice.go:160: ICE Agent is not initialized yet
netbird-proxy  | 2026-02-24T05:54:15.403Z WARN client/internal/peer/guard/ice_monitor.go:65: Failed to check ICE changes: wait for gathering timed out
netbird-proxy  | 2026-02-24T05:55:39.488Z WARN [peer: <redacted>] client/internal/peer/worker_ice.go:160: ICE Agent is not initialized yet


Screenshots

Additional context

Add any other context about the problem here.

Have you tried these troubleshooting steps?

  • Reviewed client troubleshooting (if applicable)
  • Checked for newer NetBird versions
  • Searched for similar issues on GitHub (including closed ones)
  • Restarted the NetBird client
  • Disabled other VPN software
  • Checked firewall settings

There are likely multiple things going on here, based on my own experience.

First, Netbird’s reverse proxy is currently designed as if you weren’t using one internally. In my network, I use local DNS to make my local traefik and my domain <domain> work. It matches my Cloudflare domain, so for daemon services (like Nextcloud’s auto-upload, calendar and contacts sync, etc.), the server is reachable at https://nextcloud.<domain>, whether I’m in my home or outside of it.

I don’t typically expose any ports with a Docker Compose ports directive, hardly ever, as for example, a lot of services want to run on port 8080, and if you expose ports, you have to map this container to 8080:8080, that one to 8081:8080, a third to 8082:8080, etc. If I don’t include a ports directive at all, I never have to worry about port conflicts, and my local traefik lets me get to the service just fine.

The Reverse Proxy doesn’t work with that setup as of now, as far as I can tell. Netbird’s Reverse Proxy terminates TLS, so first you’ll get X509 errors about the target IP address not being in the SNI of the certificate if you target https://, and if you target http:// it seems to do too many redirects or you’ll get an error saying it’s not redirecting properly.

So problem 1 is that the Reverse Proxy has no option for TLS passthrough to a local proxy (the way Pangolin apparently works, as the above setup worked fine with Pangolin).

To share my Jellyfin server with a cousin, I put a ports directive on it, exposing 8096, and then I discovered a second problem. If I try to target http://<Netbird Peer IP of Docker VM>:8096 for Jellyfin, it doesn’t work, likely because while the request is reaching my VM, Docker is not forwarding it onto the container (probably because it’s on a docker bridge network rather than running on the host network).

So problem 2 is that if your internal docker containers are using bridged networking, targeting the peer IP may not reach the containers because the Netbird network interface on the peer isn’t forwarded to the container by docker unless you’ve done something to make it listen on all interfaces.

What has worked for me is that I have two piholes connected as peers, and I created a Network in Netbird, created a Resource in that Network for my local subnet, and added my piholes as routing peers on that Resource. I also added Quad9 as a catch-all DNS server, and added the LAN IPs of my piholes as local DNS servers for queries to <domain>. This way, if I’m connected to Netbird and put in a request for any of my subdomains, my piholes route it to my local traefik through local DNS, and traefik serves the service as expected, as if I was within my LAN.

Because of this setup, in the Reverse Proxy if instead of targeting the Netbird peer, I instead target the Resource for my local subnet and put in http://<LAN IP of docker VM>:8096, it serves Jellyfin as expected.

The proxy is a beta feature, so I’m hoping that overtime some of these issues just get ironed out as a prerequisite for a full release.