Getting Started with NetBird Enterprise Commercial License

Updated

Two scripts deploy enterprise self-hosted NetBird:

  • Fresh installation with getting-started-enterprise.sh.
  • Migration from an existing community combined-server deployment with migrate-to-enterprise.sh.

The scripts generate the Compose files, configuration, secrets, and Postgres setup — plus optional traffic-flow services — so there's no manual YAML to write.

Both run the embedded identity provider, so no external OIDC provider is required. On a fresh install, you create the owner account from the dashboard on first login; you can connect an external provider later.

NetBird issues getting-started-enterprise.sh and the license key with your Enterprise Commercial License. If you wish to upgrade the community edition, migrate-to-enterprise.sh is available below.

1. Fresh Installation

getting-started-enterprise.sh deploys a single-node self-hosted NetBird stack with the embedded IdP. Management, signal, relay, and STUN run in one netbird-server container alongside Traefik, the dashboard, and Postgres.

1.1 Prerequisites

  • A Linux host with Docker and Docker Compose installed.
  • At least 5 GB of free disk space (the enterprise images and Postgres total ~2.2 GB, plus container and volume overhead).
  • bash, curl, jq, and openssl available on the host.
  • A real DNS-resolvable FQDN with an A record pointing at the host. Bare IP addresses are not supported.
  • Open inbound ports: 80/tcp (HTTP→HTTPS redirect), 443/tcp (dashboard, API, gRPC, relay, and Let's Encrypt validation), and 3478/udp (STUN).
  • An enterprise license key authorized for the products you want to enable.

1.2 Run the script

Create an empty directory and run the script from inside it:

mkdir -p netbird-enterprise
cd netbird-enterprise
curl -fsSL https://pkgs.netbird.io/getting-started-enterprise.sh | bash

The script prompts for:

  • Whether to enable traffic flow — required for traffic event logging and streaming.
  • The public NetBird domain.
  • A single license key (used for all enabled products and features).

It then generates the deployment files, pulls the required images, starts Postgres, waits for it to become ready, and starts the remaining services.

1.3 Generated files

The script writes the generated files into the current directory:

FilePurposeMode
.envRuntime configuration, license key, and generated secrets600
docker-compose.ymlCompose stack for the NetBird server and optional traffic-flow services644
config.yamlNetBird server configuration (YAML)600

Reverse proxy and automatic HTTPS are configured through Traefik labels and command flags inside docker-compose.yml, so there is no separate proxy configuration file.

The script aborts if generated files already exist in the directory. This avoids overwriting secrets or replacing an existing deployment by accident.

1.4 First login

Open https://<your-domain> in a browser. The dashboard detects that setup is required and walks through the first-login flow to create the owner account. No external OIDC provider is required, and no static credentials are printed or stored anywhere.

1.5 Validate the installation

After the stack starts:

  • Run docker compose ps and confirm the expected services are running.
  • Check docker compose logs -f netbird-server traefik.
  • Open the dashboard and complete owner setup.
  • Add a test peer and confirm it connects.

1.6 Stack components

The combined stack:

ServiceImageNotes
traefiktraefik:v3.6TLS termination, reverse proxy, HTTP→HTTPS redirect
dashboardghcr.io/netbirdio/dashboard-cloud:latestUI
postgrespostgres:17Datastore for management, embedded IdP, traffic events
netbird-serverghcr.io/netbirdio/netbird-server-cloud:latestManagement + signal + relay + embedded STUN on UDP/3478

Enabling traffic flow adds:

ServiceImageNotes
natsnats:2JetStream for traffic events
receiverghcr.io/netbirdio/flow-receiver-cloud:latestTraffic flow ingest
enricherghcr.io/netbirdio/flow-enricher-cloud:latestTraffic flow enrichment

2. Connect Identity Providers

We recommend using SCIM provisioning where possible. In the following setup guides, you may skip JWT group settings and use our group syncing integration instead.

Entra ID

  1. Enable Entra SSO
  2. Sync Users and Groups via SCIM (Recommended)
  3. Sync Users and Groups via API

Google Workspace

  1. Enable Google SSO
  2. Sync Users and Groups

JumpCloud

  1. Enable JumpCloud SSO
  2. Sync Users and Groups

Keycloak

  1. Enable Keycloak SSO
  2. Sync Users and Groups

Duo

Okta

Auth0

3. Migrate an Existing Community Combined Deployment

Use migrate-to-enterprise.sh to convert an existing community combined-server deployment to the enterprise images. The same script also migrates SQLite data to Postgres and enables traffic flow, so no manual Compose or config.yaml edits are required for the supported migration path.

The script targets an existing combined-server deployment that has a docker-compose.yml, a bind-mounted config.yaml, and a persistent /var/lib/netbird data volume.

3.1 Prerequisites

  • The existing community combined-server deployment is healthy, the dashboard loads, and an admin can sign in.
  • The deployment uses Docker Compose.
  • bash, openssl, Docker, and Docker Compose are available on the host.
  • At least 5 GB of free disk space — the enterprise images (~2.2 GB) are pulled while the community images are still present (~1.5 GB extra), plus a SQLite backup and Postgres data.
  • yq is installed using the Mike Farah implementation; the Python wrapper is not supported.
  • An enterprise license key authorized for the products you want to enable.
  • Access to the deployment directory that contains docker-compose.yml.

3.2 What the migration script can do

The script asks which steps to apply:

StepWhat it does
Image swapReplaces the community server and dashboard images with the enterprise images and adds the enterprise license key.
Postgres migrationAdds Postgres, generates config.yaml.enterprise, backs up the SQLite data volume, and runs migrate-store --verify.
Traffic flowAdds NATS, a flow receiver, and a flow enricher. Requires Postgres and traffic-flow licenses.

For SQLite deployments, the Postgres migration is handled by the script through the built-in migrate-store command. It copies the legacy SQLite stores (store.db, integrations.db, events.db, and idp.db when present) into Postgres and verifies row counts after the copy.

3.3 Run the migration script

Run the script from the directory that contains the existing docker-compose.yml:

cd /path/to/existing/netbird/deployment
curl -fsSL https://pkgs.netbird.io/migrate-to-enterprise.sh | bash

The script detects the combined-server service name, the dashboard service name, the host path for config.yaml, the data volume mounted at /var/lib/netbird, and the Compose network. It then prompts for the license key, whether to migrate to Postgres, and whether to enable traffic flow.

3.4 Files created by the migration script

The script does not modify the existing docker-compose.yml or original config.yaml directly. It writes migration artifacts next to them:

File or directoryPurpose
docker-compose.override.ymlCompose override with the enterprise images and optional Postgres or traffic-flow services.
config.yaml.enterpriseGenerated only when Postgres migration is selected. Points the enterprise server at Postgres.
.env additionsLicense key and generated secrets used by the override file.
backups/sqlite-pre-enterprise-*SQLite data backup created before the Postgres migration.

Docker Compose automatically merges docker-compose.override.yml with the existing docker-compose.yml.

3.5 Validate after migration

After the script completes:

  • Run docker compose ps and confirm the expected services are running.
  • Check docker compose logs -f netbird-server.
  • Open the existing NetBird dashboard URL and sign in with an existing admin user.
  • Confirm users, peers, policies, routes, setup keys, and account settings are present.
  • If Postgres migration was selected, confirm historical activity and embedded IdP data are present.
  • If traffic flow was enabled, confirm flow data appears in the dashboard after peers generate traffic.

3.6 Rollback

At the end of the run, the script prints the rollback commands for the choices made during migration. Use those commands as the source of truth for your deployment.

Rollback generally means:

  • Stop the stack with Docker Compose.
  • If Postgres migration was selected, remove the generated Postgres volume and restore the SQLite backup created by the script.
  • Remove docker-compose.override.yml and config.yaml.enterprise.
  • Remove the migration entries added to .env.
  • Start the original stack again.

Keep the SQLite backup until the enterprise deployment has been validated and your rollback window has passed.

4. Troubleshooting

Each entry follows the same structure: Symptom → Cause → Resolution → Verification.

Boot fails: server.store.encryptionKey is required

  • Symptom: The server exits immediately with this message in the logs.
  • Cause: config.yaml is missing server.store.encryptionKey, or the value is the empty string.
  • Resolution: Generate a key with openssl rand -base64 32 and set it under server.store.encryptionKey.
  • Verification: Restart; the server should now reach the Management server created and Starting CloudServer log lines.

Traffic flow warning: traffic flow disabled: server.store.engine is "sqlite" but flow requires postgres

  • Symptom: The server boots, but the warning above appears and no flow events are written.
  • Cause: server.trafficFlow.enabled: true while running on SQLite.
  • Resolution: Run migrate-to-enterprise.sh, select the Postgres migration step, and enable traffic flow when prompted. The warning disappears once the server runs with Postgres-backed stores and traffic flow enabled.
  • Verification: psql -c '\dt' netbird shows network_traffic_events (and related) tables; events appear in the dashboard's flow view.

Licensed features unavailable: license is not valid

  • Symptom: Enterprise features remain unavailable, or the server logs show license is not valid.
  • Cause: NB_LICENSE_KEY is missing, expired, or incorrect.
  • Resolution: Confirm the env var is exported and matches the key issued by NetBird.
  • Verification: The server logs show successful license validation and enterprise features unlock.

Embedded IdP discovery returns 404

  • Symptom: curl https://<your-domain>/oauth2/.well-known/openid-configuration returns 404.
  • Cause: The reverse proxy is routing /oauth2/* to a different upstream or stripping the prefix, or server.auth.issuer does not match the public URL.
  • Resolution:
    1. Confirm server.auth.issuer is https://<your-domain>/oauth2.
    2. Confirm your reverse proxy forwards /oauth2/* to the NetBird server upstream.
    3. Restart the server.
  • Verification: The discovery document JSON is returned with issuer: https://<your-domain>/oauth2.

Boot loops on Postgres connection

  • Symptom: failed to connect to postgres: dial tcp ... connect: connection refused, repeated every few seconds.
  • Cause: Not a startup-ordering issue on the generated stacks — the server already waits for a healthy Postgres. This usually means Postgres is unhealthy or unreachable: a crash-looping container, a wrong DSN, a POSTGRES_PASSWORD that no longer matches the existing netbird_postgres volume, or a blocked network path.
  • Resolution: Run docker compose ps; if postgres isn't healthy, check docker compose logs postgres, then confirm the DSN uses host postgres with a matching user, database, and password.
  • Verification: docker compose ps shows postgres as healthy; the server connects once on startup with no connection-refused loop.

Traefik cannot issue a TLS certificate

  • Symptom: HTTPS to the dashboard is unreachable or shows a TLS error; docker compose logs traefik shows ACME challenge failures.
  • Cause: The FQDN does not resolve to the host, or TCP/443 is not reachable from Let's Encrypt's validation servers. The generated stack validates with the TLS-ALPN-01 challenge only (--certificatesresolvers.letsencrypt.acme.tlschallenge=true), which always runs over port 443. Port 80 is used for the HTTP→HTTPS redirect and is never used for certificate validation, so opening it does not help issuance.
  • Resolution: Confirm the DNS A record for NETBIRD_DOMAIN points at the host, and that the firewall / cloud security group allows inbound TCP/443.
  • Verification: docker compose logs traefik shows Validations succeeded; requesting certificates followed by Server responded with a certificate; curl -sI https://<your-domain>/ returns a valid response with a trusted certificate.

First-login owner-setup page is not shown

  • Symptom: The dashboard loads, but the regular sign-in screen appears instead of the owner-setup flow.
  • Cause: An owner account already exists, so the API reports setupRequired: false. Either setup completed earlier in this deployment, or a previous run left state behind.
  • Resolution: Sign in with the existing owner credentials if available. To start over from scratch:
    docker compose down --volumes   # removes containers and the postgres/embedded-IdP data
    rm -f .env docker-compose.yml config.yaml
    curl -fsSL https://pkgs.netbird.io/getting-started-enterprise.sh | bash
    
  • Verification: curl -s https://<your-domain>/api/instance | jq . returns setupRequired: true; the dashboard now shows the owner-setup flow on first visit.

Appendix: Using a custom TLS certificate

By default, Traefik obtains a certificate from Let's Encrypt and renews it automatically. Stay on that path unless you need your own certificate, because everything below makes renewal your responsibility. Operators using an internal PKI, a corporate CA, or a pre-issued wildcard certificate serve their own certificate through Traefik's file provider, which you enable below.

Place the certificate and key on the host

PEM-encoded, with the full chain in the certificate file:

sudo mkdir -p /etc/netbird/certs
sudo cp /path/to/fullchain.pem /etc/netbird/certs/cert.pem
sudo cp /path/to/privkey.pem   /etc/netbird/certs/key.pem
sudo chmod 600 /etc/netbird/certs/key.pem

Create the dynamic configuration file

Create a traefik directory next to docker-compose.yml and put a dynamic.yaml in it:

mkdir -p traefik

traefik/dynamic.yaml:

tls:
  stores:
    default:
      defaultCertificate:
        certFile: /certs/cert.pem
        keyFile: /certs/key.pem

/certs is a path inside the container; the next step mounts /etc/netbird/certs there. Setting defaultCertificate serves this certificate on every connection, including from clients that send no SNI (Server Name Indication).

If your Traefik already has a file provider, add this tls block to its existing dynamic configuration and skip the Enable the file provider edit below, because providers.file.directory and providers.file.filename are mutually exclusive.

Update docker-compose.yml

Enable the file provider. Add this to the traefik service's command: list:

      - "--providers.file.directory=/etc/traefik/dynamic"

Mount the configuration directory and the certificates. Add these to its volumes: list:

      - ./traefik:/etc/traefik/dynamic:ro
      - /etc/netbird/certs:/certs:ro

Mount the directory, not the file. A bind-mounted single file is pinned to one inode, so anything that replaces the file instead of editing it in place leaves the container reading the old one, silently.

Remove ACME. Delete these three lines from the same command: list:

      - "--certificatesresolvers.letsencrypt.acme.email=${NETBIRD_LETSENCRYPT_EMAIL}"
      - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
      - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"

Then delete every router label that references the resolver. A fresh install has four. List them rather than relying on that count, so you also catch docker-compose.override.yml on a migrated deployment:

grep -n 'tls.certresolver' docker-compose*.yml

Each match looks like this, and all of them go:

      - traefik.http.routers.netbird-dashboard.tls.certresolver=letsencrypt

Leave the corresponding traefik.http.routers.*.tls=true labels in place.

Apply and verify

docker compose up -d
echo | openssl s_client -connect <your-domain>:443 -servername <your-domain> 2>/dev/null \
  | openssl x509 -noout -subject -issuer -enddate

Operational notes

  • The certificate must cover NETBIRD_DOMAIN from .env. A wildcard like *.example.com works for netbird.example.com.
  • If the certificate is signed by a private CA, every peer must trust the issuing CA. Install the CA bundle in each peer's system trust store. A peer that does not trust it fails to connect at all, logging x509: certificate signed by unknown authority.
  • Renewals. Replace both files at the same paths, then touch traefik/dynamic.yaml. Traefik watches the dynamic configuration, not the certificates it references, so replacing them alone has no effect. Touching it reloads them with no container restart.
  • You can leave the netbird_traefik_letsencrypt volume in place. With no resolver configured, the stored certificate is inert.
  • Dropping the 80:80 mapping does not affect certificates: this deployment validates over TLS-ALPN-01 on port 443 and never uses port 80 for ACME. You do lose the HTTP→HTTPS redirect.