Docker 部署

构建镜像

Dockerfile 是多阶段构建:先编译 Vue 管理后台,再编译 Go 二进制,最后使用非 root distroless 运行时镜像。

docker build --build-arg APP=server -t tunnelmesh:server .
docker build --build-arg APP=agent  -t tunnelmesh:agent .
docker build --build-arg APP=client -t tunnelmesh:client .

APP 只允许 server、agent、client。镜像默认工作目录为 /var/lib/tunnelmesh,本地 SQLite 数据库默认写入该目录。

本地模式

mkdir -p data/server
docker compose -f docker-compose.local.yml up -d --build server agent
docker compose -f docker-compose.local.yml --profile client run --rm client login

Server 数据保存在 Compose volume tunnelmesh-server-data。需要备份时先停止 Server,再复制 volume 或绑定目录中的 SQLite 文件。

集群模式

集群模式必须使用 MySQL,并要求 MySQL TLS 和节点 ID。docker-compose.cluster.yml 默认启用两个 Server 之间的 relay(TUNNELMESH_SERVER_RELAY_ENABLED=true);不开启 relay 时两个节点各自独立, Client 连到没有目标 Agent 路由的节点就无法访问另一节点上的 Agent。

1. 准备证书

MySQL CA 放在 deploy/certs/mysql-ca.pem,并确认 DSN 的 TLS 配置与证书匹配。relay mTLS 材料由 脚本生成,节点 ID 必须与 Compose 里的 TUNNELMESH_NODE_ID 完全一致,因为它会写入证书 SAN:

./scripts/gen-relay-certs.sh server-1 server-2

生成结果位于 deploy/certs/(已 gitignore):

deploy/certs/relay/relay-ca.pem              CA 证书,每个节点都挂载
deploy/certs/relay/<node-id>/relay.pem       节点证书
deploy/certs/relay/<node-id>/relay-key.pem   节点私钥,0600
deploy/certs/private/relay-ca-key.pem        CA 私钥,0600,只在宿主机

Compose 只挂载 CA 证书和该节点自己的目录,因此任一容器都读不到 CA 私钥或其它节点的私钥。 新增节点时重跑脚本并追加节点 ID 即可,已有 CA 会被复用;--force 只重签命令行上列出的节点证书, 不会替换 CA。不要把密码、私钥或证书提交到仓库。

容器必须能读到挂载进去的私钥。 运行时镜像是 distroless,以 USER nonroot:nonroot (uid/gid 65532)运行;bind mount 保留宿主机属主和权限,而脚本生成的节点私钥是 0600 且属于 执行脚本的用户,容器内会因权限不足无法加载,relay 在启动时报证书读取失败。为 Compose 准备材料时 把属主交给容器用户:

sudo chown -R 65532:65532 deploy/certs/relay

这只影响 relay/ 下会被挂载的 CA 证书与节点材料,private/ 里的 CA 私钥不挂载,保持 0700/0600 不变。用 systemd 直接部署时不要 chown 给 65532,而是按 Relay mTLS 证书生成与配置 第 6 节安装成 root:tunnelmesh 且 0640。

2. 引导 relay token

relay 需要 server_node 类型的 service token,而 token 只能由已运行的 Server 签发,因此首次 启动先关闭 relay:

export MYSQL_PASSWORD='change-me'
export MYSQL_ROOT_PASSWORD='change-root-me'
export TUNNELMESH_STORAGE_MYSQL_DSN='tunnelmesh:change-me@tcp(mysql:3306)/tunnelmesh?parseTime=true&tls=true'
# 必须 export 而不是只加在 up 前面:compose 对 exec 同样会重新插值 service 的
# environment 块,否则 exec 里 relay 仍为 enabled 且 token 为空,bootstrap 会先失败。
export TUNNELMESH_SERVER_RELAY_ENABLED=false

docker compose -f docker-compose.cluster.yml up -d --build

# 容器内三个二进制统一叫 /usr/local/bin/tunnelmesh(由 APP build arg 决定)。
docker compose -f docker-compose.cluster.yml exec server-1 \
  /usr/local/bin/tunnelmesh admin bootstrap

在管理后台创建一个 server_node service token:scope.serverNodeIds 留空表示 fleet token, 允许所有启用且未逻辑删除的节点;填写时只允许列出的节点。明文只返回一次。

3. 启用 relay

export TUNNELMESH_SERVER_RELAY_NODE_TOKEN='replace-with-server-node-service-token'
# 覆盖上一步的 export,或直接在当前 shell 取消它。
unset TUNNELMESH_SERVER_RELAY_ENABLED
docker compose -f docker-compose.cluster.yml up -d
docker compose -f docker-compose.cluster.yml logs -f server-1 server-2

token 为空且 relay 开启时 check-config 会以 server relay node token is required 直接失败, 不会静默降级为单节点。TUNNELMESH_SERVER_RELAY_ENDPOINT 已由 Compose 按节点设为 server-1:9443 与 server-2:9443;relay 端口只在 Compose 网络内可达,不发布到宿主机。

受控内网可以改用明文 relay:把 TUNNELMESH_SERVER_RELAY_CA、_CERT、_KEY、_SERVER_NAME 四个变量导出为空字符串。明文模式仍校验 token、节点状态和 epoch,但不提供传输加密,也无法用 证书证明节点身份。四个字段必须同时为空或同时非空,只填一部分会被拒绝。

证书字段含义、SAN 要求和轮换步骤见 Relay mTLS 证书生成与配置。

注册发现

注册发现默认使用数据库 lease;切换 etcd:

export TUNNELMESH_REGISTRY_TYPE=etcd
export TUNNELMESH_REGISTRY_ENDPOINTS='https://etcd-1:2379,https://etcd-2:2379'

MySQL 5.6 契约环境(可选)

mysql56 是一份 opt-in 的一次性测试库,用来在本地复现 CI mysql56 检查的项目下限版本 (mysql:5.6,见 测试与验证)。它不在集群栈里启动:只有显式带 --profile mysql56 才会拉起,独立 volume、无 TLS、只绑 127.0.0.1:3307, 所以既不会污染集群的 mysql 服务,也不会暴露到网络上。

docker compose -f docker-compose.cluster.yml --profile mysql56 up -d mysql56

它比 CI 服务容器多做一件事:挂载 deploy/mysql56/utf8mb4.cnf 复现生产的 utf8mb4_general_ci,因此能验证 migrations/ddl.sql 在 5.6 的 767-byte 索引前缀限制下建得起来。 口令默认为占位值,用 MYSQL56_TEST_PASSWORD / MYSQL56_ROOT_PASSWORD 覆盖即可;两者只服务本地 测试库,不得用于任何真实数据。销毁数据:

docker compose -f docker-compose.cluster.yml --profile mysql56 down -v mysql56

运行时配置

docker run --rm \
  -v "$PWD/data/server:/var/lib/tunnelmesh" \
  -e TUNNELMESH_MODE=local \
  -e TUNNELMESH_STORAGE_AUTO_INIT=true \
  -p 80:80 \
  tunnelmesh:server run

Server 只有一个监听器,容器内默认 :80。映射 443 前必须先启用原生 TLS,否则该端口没有进程监听:

docker run --rm \
  -v "$PWD/data/server:/var/lib/tunnelmesh" \
  -v "$PWD/certs:/certs:ro" \
  -e TUNNELMESH_TLS_ENABLED=true \
  -e TUNNELMESH_TLS_CERT_FILE=/certs/server.pem \
  -e TUNNELMESH_TLS_KEY_FILE=/certs/server-key.pem \
  -e TUNNELMESH_TLS_MIN_VERSION=1.2 \
  -e TUNNELMESH_SERVER_HTTP_ADDR=:443 \
  -p 443:443 \
  tunnelmesh:server run

配置优先级仍为:命令行参数 > 环境变量 > 配置文件 > 默认值。容器内推荐使用环境变量或挂载只读配置文件。

Server 的内置 HTTP runtime 在同一个监听器上提供管理 API、嵌入式 Web、Agent/Client WebSocket、WebSSH 和 TCP bridge。设置 tls.enabled: true(容器内 TUNNELMESH_TLS_ENABLED=true)时该监听器会自行终止 TLS,缺少 cert_file/key_file 或 min_version 不是 1.2/1.3 会在启动时直接失败而不是降级为明文。生产部署仍推荐在反向代理或负载均衡器处终止 HTTPS/WSS,再将受保护的内部 HTTP 连接转发到 Server;同时为 Agent 注入绑定该 Agent 的 agent service token。监听地址与 TLS 的完整说明见配置说明。

Nginx 的完整 HTTPS/WSS、Authorization 透传、Upgrade、超时、限流和 /metrics 保护示例见 Nginx 推荐配置。

容器日志默认输出到 stdout/stderr,由 Docker logging driver 管理;查看方式和其它平台日志位置见 日志位置与查看方式。

安全与升级

  • 镜像以非 root 用户运行。
  • 生产环境将 TLS 证书、MySQL DSN 和 Token 放在 Secret 管理系统中。
  • 升级前备份 MySQL/SQLite,并先在一台节点灰度验证。
  • schema 自动初始化关闭时,必须先执行受控的数据库迁移,再启动应用。
  • 回滚使用上一版本镜像,并保留兼容的数据库 schema。

服务凭据建议使用 Compose/Kubernetes Secret 注入,而不是写入镜像或 YAML:

docker run --rm --env-file ./secrets/tunnelmesh-client.env tunnelmesh:client run

首次创建 Agent/Client token 后,把 secret 写入权限为 0600 的 Secret 文件;轮换时先下发新 secret、验证新连接,再撤销旧 secret。集群 relay 还需要独立的 server_node token、mTLS 证书和显式 server.relay.listen。