来源:
intranet-tunnel/docs/docker-build.md(整篇)(原文 6354 字符)
Docker 镜像构建与部署说明
一、构建命令
构建上下文必须是仓库根目录(服务端依赖 ../shared 模块):
docker build -f server/Dockerfile -t intranet-tunnel/server:1.0.0 -t intranet-tunnel/server:latest .
或用 compose(同时构建并启动服务端与 PostgreSQL):
docker compose up -d --build
前提:前端产物必须先构建
服务端通过 go:embed 内嵌 server/internal/api/webui/dist。镜像构建阶段不会运行前端构建(.dockerignore 已排除 web/ 与 node_modules),因此必须先在宿主机完成:
cd web && npm ci && npm run build # 产物输出到 server/internal/api/webui/dist
验证是否打进镜像:比较本地与容器返回的 index.html 中 Vite 产物文件名(含内容 hash),一致即说明是最新前端。
二、本次构建结果
| 项目 | 值 |
|---|---|
| 镜像标签 | intranet-tunnel/server:1.0.0、intranet-tunnel/server:latest |
| 镜像大小 | 47.9 MB(单层 12.6 MB) |
| 架构 | linux/amd64 |
| 基础镜像 | golang:1.22-alpine(构建) → alpine:3.19(运行) |
| 运行用户 | tunnel (uid/gid 10001),非 root |
| 暴露端口 | 47800 控制连接、47801 管理 API、48080 流量入口、48443 HTTPS、48081-48100 隧道端口池、49000-49100 端口转发 |
三、持久化目录权限(重要)
镜像声明了三个 VOLUME:/app/logs、/app/backups、/app/certs。
这四个目录必须在 VOLUME 指令之前创建并 chown:
RUN mkdir -p /app/data /app/logs /app/backups /app/certs \
&& chown -R tunnel:tunnel /app
VOLUME /app/logs /app/backups /app/certs
若顺序写反(先 VOLUME 后 chown,或只 chown 了 /app/data),Docker 会把挂载点建成 root:root,而已知服务端以 tunnel(10001) 运行 —— 文件日志与自动备份会静默失败:容器日志中没有任何报错,只是 /app/logs 一直是空的。
这个问题在本次构建中实测确认过:
| 检查项 | 修复前 | 修复后 |
|---|---|---|
/app/logs 属主 | root:root 755 | tunnel:tunnel 755 |
| tunnel 用户写入 | 不可写 | 可写 |
/app/logs 内容 | 空 | app.log(4461 bytes) |
| 容器日志是否报错 | 无任何报错 | — |
bind mount 场景
若把持久化目录改为绑定宿主机路径(compose 中形如 ./logs:/app/logs),容器内看到的属主由宿主机目录决定,镜像内的 chown 不再生效。Linux 上需要:
mkdir -p ./logs ./backups ./certs
sudo chown -R 10001:10001 ./logs ./backups ./certs
Docker Desktop(Windows / macOS)通过文件共享层自动映射权限,通常无需处理。
四、验证清单
本次构建后逐项实测通过:
| 验证项 | 结果 |
|---|---|
| 镜像元数据 | 架构 amd64/linux;入口 /usr/local/bin/tunnel-server、命令 -env /app/.env、健康检查、运行用户、VOLUME 均正确 |
| 内嵌前端一致性 | 本地与容器的 Vite 产物文件名完全相同(index-IrA3J4KU.js / index-BPntY-J9.css) |
| 静态资源 | index-*.js → 200 (1191 KB)、index-*.css → 200 (385 KB) |
| history 深链接 | /login、/account、/dashboard、/settings、/logs 全部 200 |
| 图形验证码 | /api/auth/captcha 返回合法 PNG,用图像能力确认图中字符与 API 返回的 code 一致 |
| 登录链路 | 容器内全新数据库:口令登录成功 → /api/auth/config → POST /api/sms/test 成功 |
| 运行用户 | uid=10001(tunnel) gid=10001(tunnel),非 root |
| 运行环境 | ca-certificates ✓、tzdata ✓、默认时区 UTC |
| 健康检查 | healthy,失败次数 0,重启次数 0 |
| 持久化目录 | 四个目录属主 tunnel:tunnel、tunnel 可写、app.log 已落盘 |
| compose 配置 | docker compose config --quiet 通过,解析出的镜像名与本地一致 |
五、compose 部署实测
docker compose up -d 完整栈(服务端 + PostgreSQL)的验证结果:
| 验证项 | 结果 |
|---|---|
| 容器状态 | tunnel-server healthy、tunnel-postgres healthy,重启次数均为 0 |
| 依赖顺序 | postgres 先进入 healthy,服务端才开始启动(depends_on.condition: service_healthy 生效) |
| 端口绑定 | 47801 / 48080 / 48443 仅绑 127.0.0.1(符合「只对外开放 Nginx」的设计);47800 与端口池绑 0.0.0.0 |
| 数据库连接 | /healthz 返回 database: ok;PostgreSQL 中 AutoMigrate 建出 17 张表,含新增的 sms_codes、sms_send_logs |
| 前端产物 | 容器返回的 Vite 产物文件名与本地一致(index-IrA3J4KU.js) |
| 图形验证码 | 生产配置下不返回明文(仅 mock 模式回传);用视觉识别图中字符后登录成功 |
| 验证码一次性 | 重放同一验证码返回 400 图形验证码错误或已失效;识别错误时同样被拒 |
| 登录链路 | 口令 + 验证码登录成功 → /api/auth/me → /api/overview → /api/clients → /api/tunnels 全部正常 |
| 数据持久化 | 命名卷 postgres-data 保留了此前会话的测试数据(pt-victim 客户端、web 隧道),证明卷未被重建 |
| bind mount 权限 | 挂载点为 root:root 777,tunnel 用户可写 |
| 命名卷权限 | /app/data 为 tunnel:tunnel 755(继承镜像内 chown,即第三节的修复生效) |
| 日志跨挂载落盘 | 容器内 /app/logs/app.log 与宿主机 ./logs/app.log 同步增长 |
| 环境判定 | /api/overview 返回 env: production |
bind mount 与命名卷的权限差异(本次实测确认)
- 命名卷(
tunnel-data:/app/data)会继承镜像内该路径的属主,因此 Dockerfile 中的
chown -R tunnel:tunnel /app 对它是生效的 —— 实测 tunnel:tunnel 755。
- bind mount(
./logs:/app/logs)的属主由宿主机决定: - Docker Desktop(Windows / macOS):实测
root:root 777,tunnel用户可写,无需额外处理; - Linux:需手动
chown -R 10001:10001 ./logs ./backups ./certs,
否则会出现第三节描述的静默写入失败(容器日志无报错,目录一直是空的)。
六、数据库外部访问(Navicat / DBeaver / pgAdmin)
compose 已把 PostgreSQL 映射到宿主机,可直接用图形化工具连接:
| 参数 | 值 |
|---|---|
| 主机 | 127.0.0.1(本机);若工具装在其它机器上则填宿主机内网 IP |
| 端口 | 5432(可用 .env 的 DB_EXPOSE_PORT 修改) |
| 数据库 | tunnel(.env 的 DB_NAME) |
| 用户名 | tunnel_user(.env 的 DB_USER) |
| 密码 | .env 的 POSTGRES_PASSWORD(默认 change_me_db_password,生产务必修改) |
| SSL | 关闭(容器内网通信,未启用 TLS) |
安全边界
映射只绑定宿主机回环地址,因此本机工具可连,而局域网其它机器与公网均无法直连数据库。
若确需从其它机器连接,必须同时修改两处,缺一不可:
docker-compose.yml中把"127.0.0.1:${DB_EXPOSE_PORT:-5432}:5432"改为"0.0.0.0:...";- 云安全组 / 主机防火墙放行该端口。
更推荐的做法是不放开端口,改用 SSH 隧道:
ssh -L 5432:127.0.0.1:5432 user@your-server然后 Navicat 连接本机的
127.0.0.1:5432。这样数据库始终不对外暴露。
关闭外部访问
注释掉 docker-compose.yml 中 postgres 服务的整个 ports 段即可。服务端容器通过 compose 网络用服务名 postgres 访问数据库,不依赖该端口映射。
实测确认
| 验证项 | 结果 |
|---|---|
| 端口映射 | 5432/tcp -> 127.0.0.1:5432 |
| TCP 连通 | 127.0.0.1:5432 连接成功 |
| 外部客户端 | 从容器外以 host.docker.internal:5432 连接成功;current_database()=tunnel、current_user=tunnel_user、inet_server_port()=5432 |
| 业务表可见 | \dt 列出全部 17 张表,owner 均为 tunnel_user |
| 服务端不受影响 | 重建 postgres 容器后 /healthz 仍返回 database: ok(数据卷保留) |
七、管理员账号与口令
首次启动时,服务端在 users 表为空的前提下,用 .env 的 ADMIN_USERNAME / ADMIN_PASSWORD 创建初始管理员,口令以 bcrypt 摘要存储。
当前生效的口令就是 .env 里的 ADMIN_PASSWORD 的值(不是变量名本身)。
改口令的正确姿势
EnsureAdmin 只在 users 表为空时执行,所以直接修改 .env 的 ADMIN_PASSWORD 后重启 并不会更新已存在的账号。两种可行做法:
方式一:先登录后台再改(推荐)
用当前口令登录 → 「系统设置」→「登录验证设置」→ 修改口令 → 保存。 提交后会以 bcrypt 摘要写入设置并同步到账号。
方式二:重置数据库中的账号,让服务端重建
# 1) 先把 .env 的 ADMIN_PASSWORD 改成想要的新口令
# 2) 删除现有管理员记录
docker compose exec postgres psql -U tunnel_user -d tunnel -c "DELETE FROM users WHERE username='admin';"
# 3) 重启服务端,它会用新口令重建管理员
docker compose restart tunnel-server
方式二只删除账号,不影响隧道、客户端与日志数据。
另外注意
.env的AUTH_DEFAULT_PASSWORD是「设置页初始值」,与上面的ADMIN_PASSWORD(首次建号用)不是同一个东西,留空即可。
八、常用运维命令
# 查看状态与健康
docker compose ps
docker inspect --format '{{.State.Health.Status}}' tunnel-server
# 查看日志(容器内 app.log 也已落盘到挂载目录)
docker compose logs -f server
# 进入容器排查
docker compose exec server sh
# 停止并保留数据
docker compose down
# 停止并清空数据卷(谨慎)
docker compose down -v
来源:
intranet-tunnel/docs/deployment-checklist.md(整篇)(原文 3985 字符)
上线部署检查清单
按顺序执行,每完成一项打勾。命令均在仓库根目录或标注的目录下执行。
一、部署前准备
- [ ] 云服务器已开放所需端口:
443(业务)、47800(客户端控制连接)、
按需开放 TCP 隧道的外部端口(如 22022)
- [ ] 域名已解析到服务器公网 IP:
admin.example.com(管理后台)、
*.example.com(业务隧道,泛解析 A 记录)
- [ ] Nginx 版本 ≥ 1.20 且编译时带
--with-stream(nginx -V 2>&1 | grep -o with-stream) - [ ] Docker ≥ 24、Docker Compose v2 已安装
二、配置环境变量
- [ ]
cp .env .env.local(或在.env上直接修改),逐项替换change_me: - [ ]
DB_PASSWORD/POSTGRES_PASSWORD(使用openssl rand -base64 24) - [ ]
JWT_SECRET、TOKEN_SALT(使用openssl rand -hex 32) - [ ]
ADMIN_PASSWORD(强口令,至少 12 位) - [ ]
EXTERNAL_IP(填服务器公网 IP,用于后台展示 TCP 隧道入口) - [ ]
APP_ENV=production - [ ]
LOG_FORMAT=json、LOG_LEVEL=info - [ ]
API_ALLOWED_CIDRS设为办公网出口 IP(如203.0.113.10/32) - [ ]
MAX_TUNNELS_PER_CLIENT与MAX_BANDWIDTH_KBPS符合业务预期 - [ ]
API_ALLOWED_CIDRS/ADMIN_PASSWORD确认无误后,chmod 600 .env
三、启动服务端
- [ ]
docker compose up -d --build - [ ]
docker compose ps显示两个服务均为healthy - [ ]
curl -s http://127.0.0.1:47801/healthz返回"status":"ok" - [ ]
docker compose logs tunnel-server | grep -i error无异常 - [ ] 确认服务端生成了自签证书:
docker compose exec tunnel-server ls /app/data/certs
四、部署 Nginx
- [ ] 放置证书到
/etc/nginx/certs/(详见deploy/nginx/certs/README.md) - [ ]
fullchain.pem、privkey.pem(通配符证书) - [ ]
default.crt、default.key(兜底自签证书) - [ ] 复制配置:
cp deploy/nginx/nginx.conf /etc/nginx/nginx.conf
mkdir -p /etc/nginx/conf.d/stream
cp deploy/nginx/conf.d/tunnel-http.conf /etc/nginx/conf.d/
cp deploy/nginx/conf.d/stream/tunnel-stream.conf /etc/nginx/conf.d/stream/
- [ ] 替换配置中的
example.com为实际域名(tunnel-http.conf共 4 处) - [ ] 按需启用
tunnel-stream.conf中的端口转发规则 - [ ]
nginx -t通过 - [ ]
nginx -s reload - [ ] 浏览器访问
https://admin.example.com能打开登录页 - [ ]
http://admin.example.com能 301 跳转到 HTTPS
五、创建客户端与隧道
- [ ] 登录管理后台,进入「客户端」→「新建客户端」
- [ ] 立即保存返回的 Token(关闭对话框后无法再次查看)
- [ ] 进入「隧道管理」→「新建隧道」,按需创建 TCP / HTTP / HTTPS 映射
- [ ] 记录 TCP 隧道的远端端口(若填 0 则由服务端自动分配)
六、部署客户端
- [ ] 在管理后台重置一次 Token 并保存(若曾在前端页面泄露)
- [ ] 编辑
client/.env:SERVER_ADDR、CLIENT_ID、TOKEN、TLS_ENABLE=true - [ ] TLS 校验方式二选一:
- [ ] 严格:把
server.crt放到内网机器,设置TLS_CA_CERT=/etc/intranet-tunnel/server.crt - [ ] 宽松:设置
TLS_INSECURE=true(仅建议过渡期使用) - [ ] 安装二进制与 systemd 单元(见 README 4.4)
- [ ]
systemctl status intranet-tunnel-client为active (running) - [ ] 管理后台「客户端」页面显示该客户端为在线,且多路复用为已启用
七、端到端验证
- [ ] HTTP 隧道:
curl -H "Host: app.example.com" https://app.example.com/返回内网服务内容 - [ ] TCP 隧道:
ssh -p 22022 <邮箱已脱敏>能登录内网主机 - [ ] WebSocket:内网如有 WS 服务,确认能正常握手(
Upgrade头已透传) - [ ] 断网恢复:
systemctl restart intranet-tunnel-client后 30 秒内自动恢复 - [ ] 服务端重启:
docker compose restart tunnel-server后客户端自动重连并恢复隧道 - [ ] 客户端离线:停止客户端后访问域名应返回 502(而非 404)
- [ ] 流量统计:管理后台「流量统计」页面能看到数据增长
- [ ] 数据库落库:等待 30 秒后刷新统计页,窗口汇总数值持续增长
八、安全加固复查
- [ ]
server/data/certs/server.crt未随意外泄(若使用严格校验模式则分发给客户端) - [ ]
.env权限为600,未被提交到版本库 - [ ] 管理后台只能从白名单 IP 访问(
API_ALLOWED_CIDRS或 Nginxallow/deny) - [ ] 47801 与 48080 在宿主机上只监听
127.0.0.1(ss -lntp | grep -E '47801|48080') - [ ] 云安全组只放行
443、47800与显式开放的 TCP 隧道端口 - [ ] 已为各隧道设置合理带宽上限(防止单客户端占满出口)
- [ ] 数据库使用独立低权限账号,未使用
postgres超级用户 - [ ] 日志轮转生效(
docker compose logs输出不超过10m × 5)
九、日常运维
- [ ] 证书续期:
certbot renew --deploy-hook "nginx -s reload"已加入 cron - [ ] 数据库备份:
docker compose exec postgres pg_dump -U tunnel_user tunnel > backup.sql已加入定时任务 - [ ] 监控:
/healthz已接入探活 - [ ] 统计清理:服务端每 6 小时自动清理 30 天前的
stats记录(无需人工干预) - [ ] 升级流程:
git pull && docker compose up -d --build
回滚预案
- 服务端回滚:
docker compose down && git checkout <上一个版本> && docker compose up -d --build
(数据库表结构由 DB_AUTO_MIGRATE 自动迁移,向前兼容;如需回滚结构请提前用 pg_dump 备份)
- 客户端回滚:替换
/usr/local/bin/tunnel-client后systemctl restart intranet-tunnel-client - Nginx 回滚:保留上一版配置副本,
nginx -t && nginx -s reload - 紧急切断:管理后台停用全部隧道,或在 Nginx 层直接
return 503