mirror of
https://github.com/netbirdio/docs.git
synced 2026-08-24 16:51:26 +02:00
fix: update self-hosted docs for the Traefik reverse proxy (#911)
* fix: update self-hosted docs for the Traefik reverse proxy `getting-started-enterprise.sh` deploys Traefik instead of Caddy, but the enterprise getting-started page still described the Caddy stack. The worst of it was the custom-TLS appendix, which edits a `Caddyfile` the installer no longer generates, so it could not be followed at all. - Rewrite the custom TLS certificate appendix for Traefik: supply the certificate through the file provider, then remove both the ACME resolver flags and the four router `certresolver` labels. Removing the labels alone is not enough, because the certificate already stored in `acme.json` continues to be served in preference to the supplied one. - Document renewal correctly. Traefik watches the dynamic configuration file rather than the certificate files it references, so replacing the certificates has no effect until that file is touched. No container restart is needed. - Correct the certificate issuance guidance on the enterprise page and in certificate troubleshooting. The generated stack uses the TLS-ALPN-01 challenge only, so TCP/443 is what must be reachable; port 80 serves the HTTP→HTTPS redirect and is never used for validation. The community installer sets the same flag, so this applies to both. - Drop the `Caddyfile` row from the generated-files table. The installer writes `.env`, `docker-compose.yml` and `config.yaml` only, and configures routing and TLS through Traefik labels and command flags. - Replace `caddy` with `traefik` in the stack components table, the log commands, and the cleanup instructions, and fix `selfhosted-guide` still calling the bundled proxy Caddy where the same page elsewhere says Traefik. - Add the consequences that were previously unstated: a peer that does not trust a private issuing CA fails to connect, `config.yaml`'s empty `server.tls` block is intentional, and dropping the `80:80` mapping costs the HTTP→HTTPS redirect. * fix: mount the Traefik dynamic config as a directory, not a single file A bind-mounted single file is pinned to one inode, so any tool that replaces the file rather than editing it in place — most editors, `sed -i`, many configuration-management tools — leaves the container reading the old content indefinitely, with no error and no way to recover by touching the new file. Traefik's own documentation recommends `directory` over `filename` for this reason. Switch to `--providers.file.directory=/etc/traefik/dynamic` with `./traefik` bind-mounted, and move the dynamic configuration to `traefik/dynamic.yaml`. The renewal instruction is otherwise unchanged: the certificates are referenced rather than watched, so they are picked up by touching the dynamic configuration, without restarting any container. Also drop the `tls.certificates` list from the example. It is redundant next to `stores.default.defaultCertificate`, which already covers every connection including clients that send no SNI. * docs: scope the custom TLS appendix to the fresh enterprise install The appendix sits on a page that also documents `migrate-to-enterprise.sh`, and the two paths differ. A migrated deployment is built on the community `getting-started.sh` render, which already configures a file provider when the reverse proxy is enabled. `providers.file.filename` and `providers.file.directory` are mutually exclusive, so following these steps verbatim there would declare a conflicting second provider. Note that deployments which already have a file provider should extend its dynamic configuration instead, and adjust router names to match their own Compose file. * docs: apply NetBird house style to the custom TLS appendix Replace the em dashes added by the previous commits with commas, colons and parentheses. House convention is to reach for an em dash deliberately or not at all, and the appendix had accumulated eleven of them. Also state when not to follow the appendix at all: the default Let's Encrypt path renews itself, and everything in the appendix makes renewal the operator's responsibility. Expand SNI on first use. * docs: correct the migrate-path guidance in the custom TLS appendix Running the appendix against a real migrated deployment (community install with the built-in Traefik, then migrate-to-enterprise.sh with Postgres and traffic flow) showed the previous note pointed at the wrong difference. The four router names are identical to the fresh install, so there is nothing to adjust there. What actually differs is which file holds each label. `netbird-dashboard`, `netbird-grpc` and `netbird-backend` are in `docker-compose.yml`, while `netbird-flow` is on the `flow-receiver` service in `docker-compose.override.yml`, so the deletions span two files. The `traefik` service and the ACME flags stay in `docker-compose.yml`. Also state why a deployment that already has a file provider must extend it rather than add a second one: `providers.file.directory` and `providers.file.filename` are mutually exclusive. * docs: tighten the custom TLS appendix The appendix had grown to four stacked callouts, two of them before the reader reaches the first step. Cut it from 690 to 554 words and from four callouts to two, without dropping anything load-bearing. Move the migrate-path and existing-file-provider caveats out of a 106 word preamble block and into the two steps they actually affect. Drop a decorative sentence about all routes passing through Traefik, shorten the inode note, and trim the SNI gloss to expanding the acronym. * docs: find the resolver labels by grep instead of enumerating them The appendix is the supported way to serve a custom certificate, not a stopgap, so it should not hard-code "four labels, one on dashboard, two on netbird-server, one on receiver". That count goes stale the moment a router is added, and silently. Replace the enumeration with a grep over `docker-compose*.yml`. It is shorter, survives new routers, and spans `docker-compose.override.yml` on a migrated deployment, which removes the need for a separate note about where the `netbird-flow` label lives. Also reword the intro so the file provider reads as the mechanism you enable rather than something the installer failed to configure. * docs: fix three gaps in the custom TLS appendix - The `traefik` directory was never created. "Create `traefik/dynamic.yaml`" fails in any editor that will not create a missing parent, so add the `mkdir -p traefik` the steps assumed. - `/certs` appeared in the dynamic configuration one step before the mount that defines it. Say that it is a container path and where it comes from. - Point the file-provider caveat at the named edit it refers to instead of "the next step's first edit", and reword the grep sentence.
This commit is contained in:
@@ -15,7 +15,7 @@ NetBird issues `getting-started-enterprise.sh` and the license key with your [En
|
||||
|
||||
## 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 Caddy, the dashboard, and Postgres.
|
||||
`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
|
||||
|
||||
@@ -23,7 +23,7 @@ NetBird issues `getting-started-enterprise.sh` and the license key with your [En
|
||||
- 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`.
|
||||
- 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
|
||||
@@ -56,9 +56,10 @@ The script writes the generated files into the current directory:
|
||||
| --- | --- | --- |
|
||||
| `.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` |
|
||||
|
||||
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
|
||||
@@ -70,7 +71,7 @@ Open `https://<your-domain>` in a browser. The dashboard detects that setup is r
|
||||
After the stack starts:
|
||||
|
||||
- Run `docker compose ps` and confirm the expected services are running.
|
||||
- Check `docker compose logs -f netbird-server caddy`.
|
||||
- Check `docker compose logs -f netbird-server traefik`.
|
||||
- Open the dashboard and complete owner setup.
|
||||
- Add a test peer and confirm it connects.
|
||||
|
||||
@@ -80,7 +81,7 @@ The combined stack:
|
||||
|
||||
| Service | Image | Notes |
|
||||
| --- | --- | --- |
|
||||
| `caddy` | `caddy:2` | TLS termination, reverse proxy |
|
||||
| `traefik` | `traefik:v3.6` | TLS termination, reverse proxy, HTTP→HTTPS redirect |
|
||||
| `dashboard` | `ghcr.io/netbirdio/dashboard-cloud:latest` | UI |
|
||||
| `postgres` | `postgres:17` | Datastore for management, embedded IdP, traffic events |
|
||||
| `netbird-server` | `ghcr.io/netbirdio/netbird-server-cloud:latest` | Management + signal + relay + embedded STUN on UDP/3478 |
|
||||
@@ -253,12 +254,12 @@ Each entry follows the same structure: **Symptom → Cause → Resolution → Ve
|
||||
- **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.
|
||||
|
||||
### Caddy cannot issue a TLS certificate
|
||||
### Traefik 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://<your-domain>/` returns a valid response with a trusted 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
|
||||
|
||||
@@ -267,49 +268,108 @@ Each entry follows the same structure: **Symptom → Cause → Resolution → Ve
|
||||
- **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
|
||||
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 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.
|
||||
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.
|
||||
|
||||
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
|
||||
```
|
||||
<Note>
|
||||
Leave `config.yaml` alone. `netbird-server` listens on plain HTTP behind Traefik, so its empty `server.tls` block has no effect on what clients see.
|
||||
</Note>
|
||||
|
||||
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 …
|
||||
}
|
||||
```
|
||||
### Place the certificate and key on the host
|
||||
|
||||
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
|
||||
```
|
||||
PEM-encoded, with the full chain in the certificate file:
|
||||
|
||||
4. **Apply the change:**
|
||||
```bash
|
||||
docker compose up -d caddy
|
||||
```
|
||||
```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
|
||||
```
|
||||
|
||||
Notes:
|
||||
### Create the dynamic configuration file
|
||||
|
||||
Create a `traefik` directory next to `docker-compose.yml` and put a `dynamic.yaml` in it:
|
||||
|
||||
```bash
|
||||
mkdir -p traefik
|
||||
```
|
||||
|
||||
`traefik/dynamic.yaml`:
|
||||
|
||||
```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:
|
||||
|
||||
```yaml
|
||||
- "--providers.file.directory=/etc/traefik/dynamic"
|
||||
```
|
||||
|
||||
**Mount the configuration directory and the certificates.** Add these to its `volumes:` list:
|
||||
|
||||
```yaml
|
||||
- ./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:
|
||||
|
||||
```yaml
|
||||
- "--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:
|
||||
|
||||
```bash
|
||||
grep -n 'tls.certresolver' docker-compose*.yml
|
||||
```
|
||||
|
||||
Each match looks like this, and all of them go:
|
||||
|
||||
```yaml
|
||||
- traefik.http.routers.netbird-dashboard.tls.certresolver=letsencrypt
|
||||
```
|
||||
|
||||
Leave the corresponding `traefik.http.routers.*.tls=true` labels in place.
|
||||
|
||||
<Warning>
|
||||
Deleting the labels alone is not enough. While the `certificatesresolvers` lines remain, Traefik keeps serving the certificate the installer already stored in `acme.json` in preference to the one you supplied, so the stack appears to ignore your certificate. Remove both.
|
||||
</Warning>
|
||||
|
||||
### Apply and verify
|
||||
|
||||
```bash
|
||||
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 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.
|
||||
- **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.
|
||||
|
||||
@@ -65,7 +65,7 @@ Since version 0.29, NetBird uses a consolidated port architecture where Manageme
|
||||
| 3478 | UDP | Coturn STUN/TURN server |
|
||||
|
||||
<Note>
|
||||
The default setup script configures a Caddy reverse proxy that consolidates all services behind ports 80 and 443. If you're running NetBird behind your own reverse proxy, see the [advanced configuration](#advanced-running-net-bird-behind-an-existing-reverse-proxy) section.
|
||||
The default setup script configures a Traefik reverse proxy that consolidates all services behind ports 80 and 443. If you're running NetBird behind your own reverse proxy, see the [advanced configuration](#advanced-running-net-bird-behind-an-existing-reverse-proxy) section.
|
||||
</Note>
|
||||
|
||||
<Note>
|
||||
|
||||
@@ -11,9 +11,11 @@ TLS and certificate problems on a self-hosted deployment. For other areas, start
|
||||
|
||||
**Likely causes and fixes** (most common first):
|
||||
|
||||
1. **Port 80 is not reachable from the internet.** The ACME HTTP challenge (how Let's Encrypt validates your domain) needs inbound TCP/80. Confirm your firewall and cloud security groups allow it.
|
||||
1. **Port 443 is not reachable from the internet.** The bundled Traefik proxy validates with the TLS-ALPN-01 challenge, which runs over inbound TCP/443. Confirm your firewall and cloud security groups allow it. Port 80 only serves the HTTP→HTTPS redirect and is not used for validation, so opening it does not fix issuance.
|
||||
2. **The domain no longer points at this host.** Verify the `A`/`AAAA` record resolves to the server's public IP.
|
||||
3. **A renewal error in the proxy.** Check the certificate manager's logs: `docker compose logs caddy`. If needed, force a reload: `docker exec -it netbird-caddy caddy reload`.
|
||||
3. **A renewal error in the proxy.** Check the proxy's logs: `docker compose logs traefik`. Traefik reloads its configuration on its own, so there is no reload command; if you need to force a retry, restart the container with `docker compose restart traefik`.
|
||||
|
||||
Deployments created before NetBird switched to Traefik may still run Caddy, which can also use the HTTP-01 challenge on port 80. On those, keep inbound TCP/80 open and check `docker compose logs caddy` instead.
|
||||
|
||||
**Confirm**: `curl -vI https://YOUR_DOMAIN 2>&1 | grep -E "issuer|expire"` shows a current Let's Encrypt certificate.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user