[GH-ISSUE #5677] Feature Request: Label-based auto-expose for Docker Compose services (Traefik-style provider) #11626

Open
opened 2026-08-05 01:30:12 -04:00 by saavagebueno · 3 comments
Owner

Originally created by @renne on GitHub (Mar 24, 2026).
Original GitHub issue: https://github.com/netbirdio/netbird/issues/5677

Is your feature request related to a problem? Please describe.

When running NetBird alongside Docker Compose services — either via the sidecar/pod pattern (network_mode: service:netbird) or with the NetBird container joined to a shared Docker network — every service that should be exposed via netbird expose must be wired up manually: either through the dashboard UI/API or by executing netbird expose <port> as a shell command outside of Compose.

This breaks the declarative, infrastructure-as-code workflow that Docker Compose is built for. There is no way to express "expose this service via NetBird" directly in the docker-compose.yml file, next to the service definition, the same way Traefik's Docker provider allows you to place traefik.enable=true and routing labels on a container and have them automatically picked up.

Describe the solution you'd like

The NetBird client (when running as a container in a Docker Compose project) should implement a Docker label-based provider: it watches for Docker Compose service labels and automatically creates or tears down expose sessions accordingly — no manual CLI invocation or dashboard configuration needed.

Proposed label schema

Label Required Values Default
netbird.expose yes (opt-in) true
netbird.expose.port yes port number (1–65535)
netbird.expose.protocol no https, http, tcp, udp, tls https
netbird.expose.subdomain no string (lowercase, hyphens) Compose service name
netbird.expose.domain no pre-configured custom domain
netbird.expose.auth no sso, password, pin none (public)

Supported Docker Compose topologies

1. Sidecar / shared network namespace (network_mode: service:netbird)

The NetBird container and application containers share the same network stack. The target is localhost:<port>.

services:
  netbird:
    image: netbirdio/netbird:latest
    cap_add: [NET_ADMIN, SYS_ADMIN]
    cap_drop: [NET_RAW]
    environment:
      NB_SETUP_KEY: <setup-key>
    volumes:
      - netbird-config:/etc/netbird
    restart: unless-stopped

  api:
    image: myapp/api:latest
    network_mode: "service:netbird"
    depends_on: [netbird]
    labels:
      netbird.expose: "true"
      netbird.expose.port: "8080"
      netbird.expose.protocol: "https"
      netbird.expose.subdomain: "api"
      netbird.expose.auth: "sso"

  dashboard:
    image: myapp/dashboard:latest
    network_mode: "service:netbird"
    depends_on: [netbird]
    labels:
      netbird.expose: "true"
      netbird.expose.port: "3000"
      netbird.expose.protocol: "https"
      netbird.expose.subdomain: "dashboard"
      netbird.expose.auth: "sso"

volumes:
  netbird-config:

2. Shared Docker network

The NetBird container is joined to the same Docker network as the services. Targets are reached by Docker DNS (<service-name>:<port>).

services:
  netbird:
    image: netbirdio/netbird:latest
    cap_add: [NET_ADMIN, SYS_ADMIN]
    cap_drop: [NET_RAW]
    environment:
      NB_SETUP_KEY: <setup-key>
    volumes:
      - netbird-config:/etc/netbird
    networks: [app-net]
    restart: unless-stopped

  api:
    image: myapp/api:latest
    networks: [app-net]
    labels:
      netbird.expose: "true"
      netbird.expose.port: "8080"
      netbird.expose.protocol: "https"
      netbird.expose.subdomain: "api"

  db:
    image: postgres:16
    networks: [app-net]
    labels:
      netbird.expose: "true"
      netbird.expose.port: "5432"
      netbird.expose.protocol: "tcp"
      netbird.expose.subdomain: "db"
      netbird.expose.auth: "pin"

networks:
  app-net:

volumes:
  netbird-config:

Lifecycle behaviour

  • On startup, the NetBird container scans labels of all containers sharing its Compose project (or Docker network).
  • When a labelled service starts, an expose session is automatically created (equivalent to netbird expose <port> --protocol <proto> ...).
  • When a labelled service stops or is removed, the expose session is torn down immediately.
  • Labels are re-evaluated on container events (start, stop, update) so that changes to a running stack take effect without restarting the NetBird container.

Describe alternatives you've considered

  • Manual netbird expose command: requires a separate shell step outside of Compose, breaks the declarative model, and must be re-run after every restart.
  • Dashboard / API service configuration: creates permanent services, not aligned with the ephemeral lifecycle of a Compose deployment. Does not integrate into version-controlled docker-compose.yml files.
  • Traefik in front of NetBird: adds another reverse proxy to the stack, requires its own label schema, and cannot use NetBird's built-in TLS provisioning, authentication (SSO/PIN/password), or the netbird expose session model.

Additional context

This feature closes the gap between NetBird's existing netbird expose primitive and a fully declarative Docker Compose workflow, analogous to what Traefik's Docker provider offers for routing.

Related issues:

  • #5465 — Extend netbird expose to allow raw TCP (+ TLS termination) — the tcp, udp, and tls protocol values proposed here depend on that work landing first.
  • #5319 — Arbitrary TCP/UDP support for the Reverse Proxy (56 👍) — strong signal that the community wants L4 exposure; label-based auto-discovery would make that capability immediately usable from Compose.
Originally created by @renne on GitHub (Mar 24, 2026). Original GitHub issue: https://github.com/netbirdio/netbird/issues/5677 **Is your feature request related to a problem? Please describe.** When running NetBird alongside Docker Compose services — either via the sidecar/pod pattern (`network_mode: service:netbird`) or with the NetBird container joined to a shared Docker network — every service that should be exposed via `netbird expose` must be wired up manually: either through the dashboard UI/API or by executing `netbird expose <port>` as a shell command outside of Compose. This breaks the declarative, infrastructure-as-code workflow that Docker Compose is built for. There is no way to express "expose this service via NetBird" directly in the `docker-compose.yml` file, next to the service definition, the same way Traefik's Docker provider allows you to place `traefik.enable=true` and routing labels on a container and have them automatically picked up. **Describe the solution you'd like** The NetBird client (when running as a container in a Docker Compose project) should implement a **Docker label-based provider**: it watches for Docker Compose service labels and automatically creates or tears down expose sessions accordingly — no manual CLI invocation or dashboard configuration needed. ### Proposed label schema | Label | Required | Values | Default | |---|---|---|---| | `netbird.expose` | yes (opt-in) | `true` | — | | `netbird.expose.port` | yes | port number (1–65535) | — | | `netbird.expose.protocol` | no | `https`, `http`, `tcp`, `udp`, `tls` | `https` | | `netbird.expose.subdomain` | no | string (lowercase, hyphens) | Compose service name | | `netbird.expose.domain` | no | pre-configured custom domain | — | | `netbird.expose.auth` | no | `sso`, `password`, `pin` | none (public) | ### Supported Docker Compose topologies **1. Sidecar / shared network namespace** (`network_mode: service:netbird`) The NetBird container and application containers share the same network stack. The target is `localhost:<port>`. ```yaml services: netbird: image: netbirdio/netbird:latest cap_add: [NET_ADMIN, SYS_ADMIN] cap_drop: [NET_RAW] environment: NB_SETUP_KEY: <setup-key> volumes: - netbird-config:/etc/netbird restart: unless-stopped api: image: myapp/api:latest network_mode: "service:netbird" depends_on: [netbird] labels: netbird.expose: "true" netbird.expose.port: "8080" netbird.expose.protocol: "https" netbird.expose.subdomain: "api" netbird.expose.auth: "sso" dashboard: image: myapp/dashboard:latest network_mode: "service:netbird" depends_on: [netbird] labels: netbird.expose: "true" netbird.expose.port: "3000" netbird.expose.protocol: "https" netbird.expose.subdomain: "dashboard" netbird.expose.auth: "sso" volumes: netbird-config: ``` **2. Shared Docker network** The NetBird container is joined to the same Docker network as the services. Targets are reached by Docker DNS (`<service-name>:<port>`). ```yaml services: netbird: image: netbirdio/netbird:latest cap_add: [NET_ADMIN, SYS_ADMIN] cap_drop: [NET_RAW] environment: NB_SETUP_KEY: <setup-key> volumes: - netbird-config:/etc/netbird networks: [app-net] restart: unless-stopped api: image: myapp/api:latest networks: [app-net] labels: netbird.expose: "true" netbird.expose.port: "8080" netbird.expose.protocol: "https" netbird.expose.subdomain: "api" db: image: postgres:16 networks: [app-net] labels: netbird.expose: "true" netbird.expose.port: "5432" netbird.expose.protocol: "tcp" netbird.expose.subdomain: "db" netbird.expose.auth: "pin" networks: app-net: volumes: netbird-config: ``` ### Lifecycle behaviour - On **startup**, the NetBird container scans labels of all containers sharing its Compose project (or Docker network). - When a labelled service **starts**, an expose session is automatically created (equivalent to `netbird expose <port> --protocol <proto> ...`). - When a labelled service **stops or is removed**, the expose session is torn down immediately. - Labels are **re-evaluated on container events** (start, stop, update) so that changes to a running stack take effect without restarting the NetBird container. **Describe alternatives you've considered** - **Manual `netbird expose` command**: requires a separate shell step outside of Compose, breaks the declarative model, and must be re-run after every restart. - **Dashboard / API service configuration**: creates permanent services, not aligned with the ephemeral lifecycle of a Compose deployment. Does not integrate into version-controlled `docker-compose.yml` files. - **Traefik in front of NetBird**: adds another reverse proxy to the stack, requires its own label schema, and cannot use NetBird's built-in TLS provisioning, authentication (SSO/PIN/password), or the `netbird expose` session model. **Additional context** This feature closes the gap between NetBird's existing `netbird expose` primitive and a fully declarative Docker Compose workflow, analogous to what Traefik's Docker provider offers for routing. Related issues: - #5465 — Extend `netbird expose` to allow raw TCP (+ TLS termination) — the `tcp`, `udp`, and `tls` protocol values proposed here depend on that work landing first. - #5319 — Arbitrary TCP/UDP support for the Reverse Proxy (56 👍) — strong signal that the community wants L4 exposure; label-based auto-discovery would make that capability immediately usable from Compose.
Author
Owner

@renne commented on GitHub (Mar 25, 2026):

Review: Missing internal/external port mapping in label schema

I reviewed the proposed label schema against the existing netbird expose CLI implementation and found two gaps:


Gap 1 — Missing netbird.expose.external-port label (critical)

The existing CLI supports:

netbird expose <internal-port> --protocol tcp|udp|tls [--with-external-port <external-port>]

--with-external-port lets you remap the listen port on the proxy cluster independently from the container's internal port — e.g., expose a Postgres container listening on 5432 as port 5433 on the cluster. This flag maps to req.ListenPort in the ExposeServiceRequest gRPC proto, and defaults to the internal port when not provided.

The proposed label schema has no equivalent label. This means the label provider cannot replicate the full TCP/UDP/TLS port-remapping capability of the CLI.

Proposed addition:

Label Required Value Notes
netbird.expose.port yes int 1–65535 Internal/target port the container listens on
netbird.expose.external-port no int 1–65535 External port on the proxy cluster. Only valid for tcp, udp, tls. Defaults to the same value as netbird.expose.port.

Note: --with-external-port is rejected by the CLI for http/https — the same constraint should apply to the new label.


Gap 2 — auth label is incompatible with TCP/UDP/TLS (schema documentation error)

The current issue example shows:

netbird.expose.protocol: "tcp"
netbird.expose.auth: "pin"

However, the CLI explicitly rejects auth flags for TCP/UDP/TLS protocols (referred to internally as "cluster protocols"):

// client/cmd/expose.go
if isClusterProtocol(exposeProtocol) {
    if exposePin != "" || exposePassword != "" || len(exposeUserGroups) > 0 {
        return 0, fmt.Errorf("auth flags ... are not supported for %s protocol", ...)
    }
}

The label table and examples should note that netbird.expose.auth is only valid for http and https. The db example should either remove the auth label or switch to HTTP.


These were found by comparing the label schema to the implementation in client/cmd/expose.go at 1334617.

<!-- gh-comment-id:4124650461 --> @renne commented on GitHub (Mar 25, 2026): ## Review: Missing internal/external port mapping in label schema I reviewed the proposed label schema against the existing `netbird expose` CLI implementation and found **two gaps**: --- ### Gap 1 — Missing `netbird.expose.external-port` label (critical) The existing CLI supports: ``` netbird expose <internal-port> --protocol tcp|udp|tls [--with-external-port <external-port>] ``` `--with-external-port` lets you remap the listen port on the proxy cluster independently from the container's internal port — e.g., expose a Postgres container listening on `5432` as port `5433` on the cluster. This flag maps to `req.ListenPort` in the `ExposeServiceRequest` gRPC proto, and defaults to the internal port when not provided. **The proposed label schema has no equivalent label.** This means the label provider cannot replicate the full TCP/UDP/TLS port-remapping capability of the CLI. **Proposed addition:** | Label | Required | Value | Notes | |---|---|---|---| | `netbird.expose.port` | yes | int 1–65535 | Internal/target port the container listens on | | `netbird.expose.external-port` | no | int 1–65535 | External port on the proxy cluster. **Only valid for `tcp`, `udp`, `tls`.** Defaults to the same value as `netbird.expose.port`. | Note: `--with-external-port` is rejected by the CLI for `http`/`https` — the same constraint should apply to the new label. --- ### Gap 2 — `auth` label is incompatible with TCP/UDP/TLS (schema documentation error) The current issue example shows: ```yaml netbird.expose.protocol: "tcp" netbird.expose.auth: "pin" ``` However, the CLI explicitly rejects auth flags for TCP/UDP/TLS protocols (referred to internally as "cluster protocols"): ```go // client/cmd/expose.go if isClusterProtocol(exposeProtocol) { if exposePin != "" || exposePassword != "" || len(exposeUserGroups) > 0 { return 0, fmt.Errorf("auth flags ... are not supported for %s protocol", ...) } } ``` The label table and examples should note that **`netbird.expose.auth` is only valid for `http` and `https`**. The `db` example should either remove the auth label or switch to HTTP. --- These were found by comparing the label schema to the implementation in `client/cmd/expose.go` at `1334617`.
Author
Owner

@romainf commented on GitHub (Apr 29, 2026):

Suggestion: Multiple ports

We should support multiple ports per service, as Docker allows this (in the same way that Traefik supports it).

netbird expose does not seem to be able to handle this at the moment, so it would be necessary to launch several instances.

services:
  netbird:
    image: netbirdio/netbird:latest
    cap_add: [NET_ADMIN, SYS_ADMIN]
    cap_drop: [NET_RAW]
    environment:
      NB_SETUP_KEY: <setup-key>
    volumes:
      - netbird-config:/etc/netbird
    restart: unless-stopped

  app:
    image: myapp/combined:latest
    network_mode: "service:netbird"
    depends_on: [netbird]
    labels:
      netbird.expose: "true"

      netbird.expose.api.port: "8080"
      netbird.expose.api.protocol: "https"
      netbird.expose.api.subdomain: "api"
      netbird.expose.api.auth: "sso"

      netbird.expose.dashboard.port: "3000"
      netbird.expose.dashboard.protocol: "https"
      netbird.expose.dashboard.subdomain: "dashboard"
      netbird.expose.dashboard.auth: "sso"

      netbird.expose.db.port: "5432"
      netbird.expose.db.protocol: "tcp"
      netbird.expose.db.subdomain: "db"

volumes:
  netbird-config:
<!-- gh-comment-id:4342403988 --> @romainf commented on GitHub (Apr 29, 2026): ## Suggestion: Multiple ports We should support multiple ports per service, as Docker allows this (in the same way that Traefik supports it). `netbird expose` does not seem to be able to handle this at the moment, so it would be necessary to launch several instances. ``` services: netbird: image: netbirdio/netbird:latest cap_add: [NET_ADMIN, SYS_ADMIN] cap_drop: [NET_RAW] environment: NB_SETUP_KEY: <setup-key> volumes: - netbird-config:/etc/netbird restart: unless-stopped app: image: myapp/combined:latest network_mode: "service:netbird" depends_on: [netbird] labels: netbird.expose: "true" netbird.expose.api.port: "8080" netbird.expose.api.protocol: "https" netbird.expose.api.subdomain: "api" netbird.expose.api.auth: "sso" netbird.expose.dashboard.port: "3000" netbird.expose.dashboard.protocol: "https" netbird.expose.dashboard.subdomain: "dashboard" netbird.expose.dashboard.auth: "sso" netbird.expose.db.port: "5432" netbird.expose.db.protocol: "tcp" netbird.expose.db.subdomain: "db" volumes: netbird-config: ```
Author
Owner

@ronaldmiranda commented on GitHub (Jul 23, 2026):

I have made a controller for Cloudflare, maybe I can make a net bird extension for this: https://github.com/ronaldmiranda/docker-cloudflared-controller

<!-- gh-comment-id:5060060743 --> @ronaldmiranda commented on GitHub (Jul 23, 2026): I have made a controller for Cloudflare, maybe I can make a net bird extension for this: https://github.com/ronaldmiranda/docker-cloudflared-controller
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: DYNR/netbird#11626