NetBird 服务端手工部署教程(Docker Compose + Caddy)
1. 部署前先理解架构
这套方案里一共有 3 个关键部分:
Caddy:运行在宿主机,占用80/tcp和443/tcpdashboard:NetBird 控制台前端,运行在 Docker 中netbird-server:NetBird 的 management / signal / API / embedded IdP,运行在 Docker 中
另外还有一个必须直接暴露到公网的端口:
3478/udp:STUN 端口,不能走 HTTP 反向代理
推荐的流量路径如下:
- 浏览器访问
https://netbird.example.com - Caddy 根据请求类型,把流量转发给:
127.0.0.1:8080上的dashboard127.0.0.1:8081上的netbird-server
2. 域名是不是必须
建议当作必须。
原因:
- Caddy 申请公开 TLS 证书需要域名
- NetBird 客户端连接控制端也更适合固定域名
- embedded IdP 的回调地址本身就是
https://你的域名/...
所以本文默认你已经准备好一个域名,例如:
netbird.example.com
并且 DNS 已解析到你的服务器公网 IP。
3. 前提条件
至少满足下面这些条件:
- 一台公网 Linux 服务器
- 已安装
Docker - 已安装
docker compose - 已安装
Caddy - 已安装
openssl - 已放行端口:
80/tcp443/tcp3478/udp
如果你使用云服务器,还要检查云厂商安全组。
4. 目录规划
下面用 /opt/netbird 作为示例目录:
/opt/netbird/
├── docker-compose.yml
├── config/
│ └── config.yaml
└── env/
└── dashboard.env
创建目录:
sudo mkdir -p /opt/netbird/config /opt/netbird/env
sudo chown -R "$USER":"$USER" /opt/netbird
cd /opt/netbird
5. 先生成两个随机密钥
config.yaml 里至少会用到两个随机值:
authSecretencryptionKey
生成方式示例:
openssl rand -base64 32
openssl rand -base64 32
你可以先把它们记下来,例如:
AUTH_SECRET=替换成你生成的第一个随机串
ENCRYPTION_KEY=替换成你生成的第二个随机串
注意:
- 这两个值不要随便改
encryptionKey最好备份- 如果你已经有正式环境,不要轻易重建这两个值
6. 手工创建 docker-compose.yml
文件路径:
/opt/netbird/docker-compose.yml
内容如下:
services:
dashboard:
# NetBird 控制台前端
image: netbirdio/dashboard:latest
container_name: netbird-dashboard
restart: unless-stopped
# 只绑定到本机回环地址,避免被公网直接访问
ports:
- "127.0.0.1:8080:80"
# 控制台前端的环境变量文件
env_file:
- ./env/dashboard.env
networks:
- netbird
netbird-server:
# NetBird combined container,内含 management / signal / embedded IdP
image: netbirdio/netbird-server:latest
container_name: netbird-server
restart: unless-stopped
ports:
# HTTP / API / gRPC / WebSocket 只开放给本机,由宿主机 Caddy 反代
- "127.0.0.1:8081:80"
# STUN 端口必须直接暴露到公网
- "3478:3478/udp"
volumes:
# 主配置文件
- ./config/config.yaml:/etc/netbird/config.yaml:ro
# 持久化数据目录,包含 sqlite 数据和 embedded IdP 数据
- netbird_data:/var/lib/netbird
command:
- --config
- /etc/netbird/config.yaml
networks:
- netbird
volumes:
netbird_data:
# 使用 Docker volume 持久化数据
networks:
netbird:
driver: bridge
关于镜像版本
上面示例用了 latest,是为了写法直观。
生产环境更建议你把镜像固定成明确版本,例如:
image: netbirdio/netbird-server:vX.Y.Z
image: netbirdio/dashboard:vX.Y.Z
这样后续升级更可控。
7. 手工创建 config.yaml
文件路径:
/opt/netbird/config/config.yaml
内容如下,把域名和密钥换成你自己的:
server:
# 容器内实际监听地址
listenAddress: ":80"
# 对外暴露给客户端的 management 地址
exposedAddress: https://netbird.example.com:443
# 内置 STUN 监听端口列表
stunPorts:
- 3478
# 内部指标端口,不对外暴露
metricsPort: 9090
# 容器内部健康检查地址,不对外暴露
healthcheckAddress: ":9000"
# 日志等级,可选 info / debug / warn / error
logLevel: info
# 日志输出到 stdout
logFile: console
# embedded IdP 和内部认证使用的随机密钥
authSecret: "替换成你生成的 AUTH_SECRET"
# 数据目录,要和 docker-compose 里的持久化目录对应
dataDir: /var/lib/netbird
# 让 NetBird 信任来自宿主机 Caddy 的 X-Forwarded-* 头
reverseProxy:
trustedHTTPProxies:
- 127.0.0.1/32
- ::1/128
auth:
# 使用内置 embedded IdP
issuer: https://netbird.example.com/oauth2
# 是否启用签名密钥自动轮换
signKeyRefreshEnabled: true
# Dashboard 登录后的回调地址
dashboardRedirectURIs:
- https://netbird.example.com/nb-auth
- https://netbird.example.com/nb-silent-auth
# CLI 登录回调地址
cliRedirectURIs:
- http://localhost:53000/
store:
# 默认使用 sqlite,数据会写到 dataDir 中
engine: sqlite
# sqlite 模式下可以留空
dsn: ""
# 用于加密数据库中敏感字段的密钥
encryptionKey: "替换成你生成的 ENCRYPTION_KEY"
这份配置里最关键的 6 个值
server.exposedAddressserver.stunPortsserver.authSecretserver.auth.issuerserver.store.engineserver.store.encryptionKey
只要这几个值的域名和密钥没写错,基础部署通常就能跑起来。
为什么这里加了 reverseProxy.trustedHTTPProxies
官方脚本在自动生成配置时,会让服务信任它前面的反向代理。
你现在的反向代理是宿主机上的 Caddy,所以这里把 127.0.0.1/32 和 ::1/128 加进去,让 NetBird 正确识别 X-Forwarded-For、X-Forwarded-Proto 等请求头。
8. 手工创建 dashboard.env
文件路径:
/opt/netbird/env/dashboard.env
内容如下:
# Dashboard 自己对外展示的域名
NETBIRD_MGMT_API_ENDPOINT=https://netbird.example.com
# Dashboard 调用后端 API 的地址
NETBIRD_MGMT_GRPC_API_ENDPOINT=https://netbird.example.com
# embedded IdP 的发行者地址
AUTH_AUTHORITY=https://netbird.example.com/oauth2
# embedded IdP 的 audience
AUTH_AUDIENCE=netbird-dashboard
# Dashboard 的 OAuth Client ID
AUTH_CLIENT_ID=netbird-dashboard
# embedded IdP 默认无需 client secret
AUTH_CLIENT_SECRET=
# embedded IdP,不是 Auth0
USE_AUTH0=false
# Dashboard 登录回调地址
AUTH_REDIRECT_URI=/nb-auth
# 静默刷新登录态的回调地址
AUTH_SILENT_REDIRECT_URI=/nb-silent-auth
# embedded IdP 使用的 scope
AUTH_SUPPORTED_SCOPES=openid profile email groups
# dashboard 容器内部 nginx 的 HTTPS 端口变量
NGINX_SSL_PORT=443
# 关闭 dashboard 内置证书申请,由现有 Caddy 接管 TLS
LETSENCRYPT_DOMAIN=none
说明:
- 这里的
NETBIRD_MGMT_API_ENDPOINT和AUTH_AUTHORITY都要指向同一个外部域名 AUTH_AUDIENCE=netbird-dashboard和AUTH_CLIENT_ID=netbird-dashboard是 embedded IdP 默认配套值AUTH_REDIRECT_URI和AUTH_SILENT_REDIRECT_URI必须和config.yaml里的 redirect URIs 对应
9. 启动容器
启动前先检查文件:
cd /opt/netbird
docker compose config
如果没有报错,再启动:
docker compose up -d
docker compose ps
查看日志:
docker compose logs -f
10. 配置宿主机 Caddy 反向代理
如果你的 Caddy 运行在宿主机上,可以直接把下面这个站点块放进:
/etc/caddy/Caddyfile
示例配置如下:
# 你可以把这段作为可复用片段
(netbird_common) {
# 压缩响应
encode zstd gzip
# 一些基础安全头,可按自己现网风格调整
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options nosniff
X-Frame-Options SAMEORIGIN
Referrer-Policy no-referrer-when-downgrade
}
# 访问日志,可按需删掉
log {
output file /var/log/caddy/netbird-access.log {
roll_size 20MiB
roll_keep 10
roll_keep_for 720h
}
format json
}
}
netbird.example.com {
import netbird_common
# gRPC 请求转发到 netbird-server
@grpc header Content-Type application/grpc*
reverse_proxy @grpc h2c://127.0.0.1:8081
# API / OAuth2 / WebSocket 等后端请求也交给 netbird-server
@backend path /relay* /ws-proxy/* /api/* /oauth2/*
reverse_proxy @backend 127.0.0.1:8081
# 其他前端页面请求交给 dashboard
reverse_proxy 127.0.0.1:8080
}
如果你的系统里还没有 /var/log/caddy/,可以二选一:
- 先创建该目录并给 Caddy 写权限
- 或者先删掉上面示例里的
log配置块
这段 Caddy 配置为什么不能只写一个 reverse_proxy
因为 NetBird 不是单纯的静态网页:
- 控制台页面来自
dashboard - API、OAuth2、WebSocket、gRPC 来自
netbird-server
如果你把所有请求都反代到 dashboard,页面可能能打开,但登录、设备注册、CLI 登录或客户端连接经常会出问题。
11. 校验并重载 Caddy
先校验:
sudo caddy validate --config /etc/caddy/Caddyfile
再重载:
sudo systemctl reload caddy
如果你不是 systemd 管理 Caddy,按你的实际启动方式重载即可。
12. 首次初始化
浏览器访问:
https://netbird.example.com/setup
第一次访问会进入初始化页面,用于创建第一个管理员账号。
创建完成后:
- 你可以登录 Dashboard
- 再去添加设备
- 客户端和 CLI 会通过这个域名完成认证和管理连接
13. 启动后建议做的 5 个检查
1)检查容器状态
docker compose ps
2)检查本地监听端口
ss -lntup | grep -E '8080|8081|3478'
你应该看到:
127.0.0.1:8080127.0.0.1:80810.0.0.0:3478或对应的 UDP 监听
3)检查 Caddy 反代前的本地访问
curl -I http://127.0.0.1:8080
curl -I http://127.0.0.1:8081
4)检查 HTTPS 是否正常
curl -I https://netbird.example.com
5)检查初始化页面
浏览器打开:
https://netbird.example.com/setup
14. 常用维护命令
拉取最新镜像:
cd /opt/netbird
docker compose pull
重新创建容器:
docker compose up -d
查看日志:
docker compose logs -f
重启:
docker compose restart
停止:
docker compose down
15. 常见问题
1)网页能打开,但客户端连不上
优先检查:
3478/udp是否真的已放行- 云厂商安全组是否开放
3478/udp server.exposedAddress对应的域名是否正确server.stunPorts是否包含3478
2)登录时报错,或者回调页失败
优先检查:
config.yaml里的dashboardRedirectURIsdashboard.env里的AUTH_REDIRECT_URI- 域名是不是前后一致
- 是否混用了
http和https
3)证书申请失败
优先检查:
- 域名 DNS 是否已生效
80/tcp和443/tcp是否可以从公网访问- 是否有别的服务占用了
80/443
4)能打开控制台,但接口偶发 502
优先检查:
- Caddy 是否按本文区分了 gRPC 和普通 HTTP 请求
reverse_proxy指向的本地端口是否写对- 容器是否真的监听在
127.0.0.1:8080和127.0.0.1:8081
5)后面我还想启用 NetBird Proxy
这时不要继续沿用本文方案。
官方说明 NetBird Proxy 依赖 Traefik 提供 TLS passthrough。也就是说:
- 本文方案适合“管理面 + 客户端接入”
- 如果你要启用 NetBird Proxy,请改用 Traefik 路线
16. 什么时候该考虑改成 PostgreSQL
如果你只是自用、小团队或轻量使用,先用默认 sqlite 就够了。
如果你有下面这些需求,再考虑把 store.engine 改成 PostgreSQL:
- 用户量明显增大
- 希望把数据库单独备份
- 希望更方便做高可用或迁移
本文先不展开 PostgreSQL 版,先把最小可用的手工部署跑通。
17. 最小可用清单
如果你只想快速核对有没有漏项,看这 8 条就够了:
- 有公网域名,例如
netbird.example.com - DNS 已解析到服务器公网 IP
- 宿主机已安装 Docker、docker compose、Caddy、openssl
- 防火墙已放行
80/tcp、443/tcp、3478/udp /opt/netbird/docker-compose.yml已创建/opt/netbird/config/config.yaml已创建/opt/netbird/env/dashboard.env已创建/etc/caddy/Caddyfile已加入 NetBird 站点块
18. 参考文档
- NetBird 官方 Quickstart:
- NetBird 官方 External Reverse Proxy: