0
0
0

Docker 服务栈清单

文章摘要
|

来源: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.0intranet-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 755tunnel: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/configPOST /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_codessms_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 777tunnel 用户可写
命名卷权限/app/datatunnel: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 777tunnel 用户可写,无需额外处理
    • Linux:需手动 chown -R 10001:10001 ./logs ./backups ./certs

否则会出现第三节描述的静默写入失败(容器日志无报错,目录一直是空的)。

六、数据库外部访问(Navicat / DBeaver / pgAdmin)

compose 已把 PostgreSQL 映射到宿主机,可直接用图形化工具连接:

参数
主机127.0.0.1(本机);若工具装在其它机器上则填宿主机内网 IP
端口5432(可用 .envDB_EXPOSE_PORT 修改)
数据库tunnel.envDB_NAME
用户名tunnel_user.envDB_USER
密码.envPOSTGRES_PASSWORD(默认 change_me_db_password生产务必修改
SSL关闭(容器内网通信,未启用 TLS)

安全边界

映射只绑定宿主机回环地址,因此本机工具可连,而局域网其它机器与公网均无法直连数据库。

若确需从其它机器连接,必须同时修改两处,缺一不可:

  1. docker-compose.yml 中把 "127.0.0.1:${DB_EXPOSE_PORT:-5432}:5432" 改为 "0.0.0.0:..."
  2. 云安全组 / 主机防火墙放行该端口。

更推荐的做法是不放开端口,改用 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()=tunnelcurrent_user=tunnel_userinet_server_port()=5432
业务表可见\dt 列出全部 17 张表,owner 均为 tunnel_user
服务端不受影响重建 postgres 容器后 /healthz 仍返回 database: ok(数据卷保留)

七、管理员账号与口令

首次启动时,服务端在 users 表为空的前提下,用 .envADMIN_USERNAME / ADMIN_PASSWORD 创建初始管理员,口令以 bcrypt 摘要存储。

当前生效的口令就是 .env 里的 ADMIN_PASSWORD 的值(不是变量名本身)。

改口令的正确姿势

EnsureAdmin 只在 users 表为空时执行,所以直接修改 .envADMIN_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

方式二只删除账号,不影响隧道、客户端与日志数据。

另外注意 .envAUTH_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-streamnginx -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_SECRETTOKEN_SALT(使用 openssl rand -hex 32
    • [ ] ADMIN_PASSWORD(强口令,至少 12 位)
    • [ ] EXTERNAL_IP(填服务器公网 IP,用于后台展示 TCP 隧道入口)
  • [ ] APP_ENV=production
  • [ ] LOG_FORMAT=jsonLOG_LEVEL=info
  • [ ] API_ALLOWED_CIDRS 设为办公网出口 IP(如 203.0.113.10/32
  • [ ] MAX_TUNNELS_PER_CLIENTMAX_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.pemprivkey.pem(通配符证书)
    • [ ] default.crtdefault.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/.envSERVER_ADDRCLIENT_IDTOKENTLS_ENABLE=true
  • [ ] TLS 校验方式二选一:
    • [ ] 严格:把 server.crt 放到内网机器,设置 TLS_CA_CERT=/etc/intranet-tunnel/server.crt
    • [ ] 宽松:设置 TLS_INSECURE=true(仅建议过渡期使用)
  • [ ] 安装二进制与 systemd 单元(见 README 4.4)
  • [ ] systemctl status intranet-tunnel-clientactive (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 或 Nginx allow/deny
  • [ ] 47801 与 48080 在宿主机上只监听 127.0.0.1ss -lntp | grep -E '47801|48080'
  • [ ] 云安全组只放行 44347800 与显式开放的 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

回滚预案

  1. 服务端回滚docker compose down && git checkout <上一个版本> && docker compose up -d --build

(数据库表结构由 DB_AUTO_MIGRATE 自动迁移,向前兼容;如需回滚结构请提前用 pg_dump 备份)

  1. 客户端回滚:替换 /usr/local/bin/tunnel-clientsystemctl restart intranet-tunnel-client
  2. Nginx 回滚:保留上一版配置副本,nginx -t && nginx -s reload
  3. 紧急切断:管理后台停用全部隧道,或在 Nginx 层直接 return 503

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或者给予支持!

评论