From fb4c110df31021e7e5876e2178260608d1c3bb99 Mon Sep 17 00:00:00 2001 From: Jack Carter <128555021+SunsetDrifter@users.noreply.github.com> Date: Thu, 18 Jun 2026 17:37:35 +0200 Subject: [PATCH] docs: add Enterprise Commercial License self-hosted getting-started page (#801) * docs: add unlisted Enterprise Commercial License getting-started page Self-hosted NetBird stack guide with embedded IdP, served at /selfhosted/enterprise/getting-started. Reachable by direct link only; intentionally not added to the sidebar nav. * docs: add custom TLS certificate appendix to Enterprise getting-started * docs: rewrite Enterprise Commercial getting-started from script-based deployment guide Replace the topology-choice page with the current script-based flow: getting-started.sh for fresh installs and migrate-to-commercial.sh for community-to-enterprise migrations. Add the migration path, numbered sections, and a troubleshooting section; standardize on enterprise terminology and placeholders. Keep the IdP-connection links and the custom-TLS appendix. * docs: clarify traffic-flow note in Enterprise getting-started * docs: link Docker install in Enterprise getting-started prerequisites --- .../selfhosted/enterprise/getting-started.mdx | 310 ++++++++++++++++++ 1 file changed, 310 insertions(+) create mode 100644 src/pages/selfhosted/enterprise/getting-started.mdx diff --git a/src/pages/selfhosted/enterprise/getting-started.mdx b/src/pages/selfhosted/enterprise/getting-started.mdx new file mode 100644 index 00000000..d7a839d2 --- /dev/null +++ b/src/pages/selfhosted/enterprise/getting-started.mdx @@ -0,0 +1,310 @@ +export const description = 'Deploy or migrate a self-hosted NetBird Enterprise Commercial stack with the built-in embedded identity provider using the getting-started.sh and migrate-to-commercial.sh scripts — no external OIDC provider required.' + +# Getting Started with NetBird Enterprise Commercial License + +Two scripts deploy enterprise self-hosted NetBird: + +- **Fresh installation** with `getting-started.sh`. +- **Migration** from an existing community combined-server deployment with `migrate-to-commercial.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.sh`, the GHCR credentials, and the license key with your [Enterprise Commercial License](https://netbird.io/pricing#on-prem). `migrate-to-commercial.sh` is available on request — contact your account team. + +## 1. Fresh Installation + +`getting-started.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 Caddy, the dashboard, and Postgres. + +### 1.1 Prerequisites + +- A Linux host with [Docker and Docker Compose](https://docs.docker.com/engine/install/) 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`, `443/tcp`, and `3478/udp`. +- GHCR username and token associated with the enterprise license. +- 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: + +```bash +mkdir -p netbird-enterprise +cd netbird-enterprise +bash getting-started.sh +``` + +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). +- GHCR username and token. + +It then generates the deployment files, logs in to GHCR, pulls the required images, starts Postgres, waits for it to become ready, and starts the remaining services. + + +Enabling traffic flow adds NATS, a flow receiver, and a flow enricher to the stack. Traffic flow is required for traffic event logging and streaming. + + +### 1.3 Generated files + +The script writes the generated files into the current directory: + +| File | Purpose | Mode | +| --- | --- | --- | +| `.env` | Runtime configuration, license key, and generated secrets | `600` | +| `docker-compose.yml` | Compose stack for the NetBird server and optional traffic-flow services | `644` | +| `Caddyfile` | Reverse proxy and automatic HTTPS configuration | `644` | +| `config.yaml` | NetBird server configuration (YAML) | `600` | + +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://` 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 caddy`. +- Open the dashboard and complete owner setup. +- Add a test peer and confirm it connects. + +## 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](/selfhosted/identity-providers/managed/microsoft-entra-id) +2. [Sync Users and Groups via SCIM](/manage/team/idp-sync/embedded/microsoft-entra-id-scim-sync) (Recommended) +3. [Sync Users and Groups via API](/manage/team/idp-sync/embedded/microsoft-entra-id-sync) + +### Google Workspace + +1. [Enable Google SSO](/selfhosted/identity-providers/managed/google-workspace) +2. [Sync Users and Groups](/manage/team/idp-sync/embedded/google-workspace-sync) + +### JumpCloud + +1. [Enable JumpCloud SSO](/selfhosted/identity-providers/managed/jumpcloud) +2. [Sync Users and Groups](/manage/team/idp-sync/embedded/jumpcloud-sync) + +### Keycloak + +1. [Enable Keycloak SSO](/selfhosted/identity-providers/keycloak) +2. [Sync Users and Groups](/manage/team/idp-sync/embedded/keycloak-sync) + +### Duo + +- [Enable Duo SSO](/selfhosted/identity-providers/managed/duo) + +### Okta + +- [Enable Okta SSO](/selfhosted/identity-providers/managed/okta) + +### Auth0 + +- [Enable Auth0 SSO](/selfhosted/identity-providers/managed/auth0) + +## 3. Migrate an Existing Community Combined Deployment + +Use `migrate-to-commercial.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 migration script is available on request — contact your account team to obtain it. + + +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](https://github.com/mikefarah/yq); the Python wrapper is not supported. +- GHCR username and token associated with the enterprise license. +- 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: + +| Step | What it does | +| --- | --- | +| Image swap | Replaces the community server and dashboard images with the enterprise images and adds the enterprise license key. | +| Postgres migration | Adds Postgres, generates `config.yaml.commercial`, backs up the SQLite data volume, and runs `migrate-store --verify`. | +| Traffic flow | Adds 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`: + +```bash +cd /path/to/existing/netbird/deployment +bash migrate-to-commercial.sh +``` + +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, GHCR credentials, 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 directory | Purpose | +| --- | --- | +| `docker-compose.override.yml` | Compose override with the enterprise images and optional Postgres or traffic-flow services. | +| `config.yaml.commercial` | Generated only when Postgres migration is selected. Points the enterprise server at Postgres. | +| `.env` additions | License key and generated secrets used by the override file. | +| `backups/sqlite-pre-commercial-*` | 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.commercial`. +- 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.** + +### 4.1 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. + + +**Do not rotate the key on an existing deployment.** If you have already booted once with a key, that exact value must remain — otherwise encrypted records become unreadable. + + +### 4.2 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-commercial.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. + +### 4.3 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. + +### 4.4 Embedded IdP discovery returns 404 + +- **Symptom:** `curl https:///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:///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:///oauth2`. + +### 4.5 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. + +### 4.6 Caddy cannot issue a TLS certificate + +- **Symptom:** HTTPS to the dashboard is unreachable or shows a TLS error; `docker compose logs caddy` shows ACME challenge failures. +- **Cause:** The FQDN does not resolve to the host, or TCP/80 + TCP/443 are not reachable from Let's Encrypt's validation servers. Caddy tries HTTP-01 (port 80) first and falls back to TLS-ALPN-01 (port 443); at least one must be reachable from the public internet for cert issuance to succeed. +- **Resolution:** Confirm the DNS A record for `NETBIRD_DOMAIN` points at the host, and that the firewall / cloud security group allows inbound TCP/80 and TCP/443. +- **Verification:** `docker compose logs caddy` shows "certificate obtained successfully"; `curl -sI https:///` returns a valid response with a trusted certificate. + +### 4.7 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: + ```bash + docker compose down --volumes # removes containers and the postgres/embedded-IdP data + rm -f .env docker-compose.yml Caddyfile config.yaml + bash getting-started.sh + ``` +- **Verification:** `curl -s https:///api/instance | jq .` returns `setupRequired: true`; the dashboard now shows the owner-setup flow on first visit. + +### 4.8 `docker login ghcr.io` fails during the script + +- **Symptom:** `getting-started.sh` aborts at the GHCR login step with an authentication error. +- **Cause:** The token lacks the `read:packages` scope, or the username does not match the one issued alongside the enterprise license. +- **Resolution:** Confirm the token at [github.com/settings/tokens](https://github.com/settings/tokens) has `read:packages` scope, and that the username matches the one NetBird issued with the license. +- **Verification:** `echo "$TOKEN" | docker login ghcr.io -u "" --password-stdin` returns `Login Succeeded`. + +## Appendix: Using a custom TLS certificate + +By default the generated `Caddyfile` uses Caddy's automatic TLS via Let's Encrypt. Operators using an internal PKI, a corporate CA, or a pre-issued wildcard certificate can swap to their own cert with three manual edits after the script runs. Caddy stays in front of the stack either way — all reverse-proxy routes (signal, management, OAuth2, relay, dashboard) flow through it. + +1. **Place the cert and key on the host.** PEM-encoded, full chain in the cert file: + ```bash + 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 + ``` + +2. **Edit `Caddyfile`** and add a `tls` directive inside the `:443` block: + ``` + {$CADDY_SECURE_DOMAIN}:443 { + tls /etc/caddy/certs/cert.pem /etc/caddy/certs/key.pem + import security_headers + # … existing reverse_proxy lines unchanged … + } + ``` + +3. **Edit `docker-compose.yml`** and add a read-only volume mount on the `caddy` service: + ```yaml + caddy: + volumes: + - netbird_caddy_data:/data + - ./Caddyfile:/etc/caddy/Caddyfile + - /etc/netbird/certs:/etc/caddy/certs:ro + ``` + +4. **Apply the change:** + ```bash + docker compose up -d caddy + ``` + +Notes: + +- The certificate must cover `NETBIRD_DOMAIN` from `.env`. A wildcard like `*.example.com` works for `netbird.example.com`. +- If the cert is signed by a private CA, every peer and CLI client must trust the issuing CA — install the CA bundle in their trust stores. +- Renewals are the operator's responsibility. Replace the files at the same paths and run `docker compose restart caddy`. Caddy will pick up the new cert without a full restart of the rest of the stack. +- Port 80 in the generated `Caddyfile` only does an HTTP→HTTPS redirect; it is not used for ACME challenges in this mode. You can drop the `:80` mapping from the `caddy` service if HTTP isn't needed.