DryDock 分布式容器更新监控部署实录:一屏管五台机器
我有 5 台机器跑着 Docker,分布在 NAS、内网几台小主机、还有一台荷兰 VPS。每次更新镜像要 SSH 进去逐个 docker compose pull,漏了哪台全靠运气发现。
调研了一圈镜像更新监控工具,Watchtower 太粗暴(动不动自动重启),Diun 只通知不管理。最后选了 DryDock—— 一个 Controller 管所有 Agent,Web UI 统一查看,支持 23 个镜像注册表,20+ 通知渠道。
环境:fnOS NAS(Controller)、AI 主机(.216)、n8n 工具机(.181)、Frigate NVR(.160)、Bandwagon VPS(云主机)。
架构
┌─────────────────────────────────────┐│ fnOS (<NAS_IP>) ││ DryDock Controller :24300 ││ 统一面板 · 通知 · 自动更新 ││ agent.env ← 所有 Agent 连接配置 │└──┬──────┬──────┬──────┬────────────┘ │ │ │ │ ▼ ▼ ▼ ▼ Agent Agent Agent Agent :3000 :3000 :3000 :34300 AI主机 n8n Frigate Bandwagon .216 .181 .160 云主机 ✅ ✅ ✅ ✅通信方式:Controller 主动连 Agent(SSE over HTTP)。Agent 只需要 Docker socket 访问,不暴露给外部 —— 通过 docker-socket-proxy 做一层隔离。
Controller 配置(fnOS)
Controller 跑在 NAS 上,核心是两个容器:DryDock 本体 + docker-socket-proxy(用于监控本机容器)。
services: drydock: image: codeswhat/drydock container_name: drydock restart: unless-stopped env_file: - agent.env # ← Agent 连接独立文件 depends_on: socket-proxy: condition: service_healthy environment: - DD_WATCHER_LOCAL_HOST=socket-proxy - DD_WATCHER_LOCAL_PORT=2375 - DD_AUTH_BASIC_ADMIN_USER=*** - "DD_AUTH_BASIC_ADMIN_HASH=***"35 collapsed lines
ports: - 24300:3000 deploy: resources: limits: memory: 512M cpus: "1.0" logging: driver: json-file options: max-size: "20m" max-file: "3" security_opt: - no-new-privileges:true
socket-proxy: image: tecnativa/docker-socket-proxy container_name: socket-proxy restart: unless-stopped volumes: - /var/run/docker.sock:/var/run/docker.sock environment: - CONTAINERS=1 - IMAGES=1 - EVENTS=1 - SERVICES=1 - POST=1 - NETWORKS=1 - INFO=1 healthcheck: test: wget --spider http://localhost:2375/version || exit 1 interval: 5s timeout: 3s retries: 3 start_period: 5sagent.env 设计
Agent 连接配置不写在 docker-compose.yml 的 environment 里,而是拆到独立的 agent.env:
# === AI 主机 (<AI_HOST_IP>) ===DD_AGENT_AI_HOST=<AI_HOST_IP>DD_AGENT_AI_SECRET=***
# === n8n 工具机 (<N8N_HOST_IP>) ===DD_AGENT_N8N_HOST=<N8N_HOST_IP>DD_AGENT_N8N_SECRET=***
# === Frigate NVR (<FRIGATE_IP>) ===DD_AGENT_FRIGATE_HOST=<FRIGATE_IP>DD_AGENT_FRIGATE_SECRET=***
# === Bandwagonhost VPS (<VPS_IP>) ===DD_AGENT_VPSNL2_HOST=<VPS_IP>DD_AGENT_VPSNL2_PORT=343003 collapsed lines
DD_AGENT_VPSNL2_SECRET=***
DD_AGENT_ALLOW_INSECURE_SECRET=*** # LAN 明文 HTTPAgent 部署模板
每台目标机器上跑两个容器:docker-socket-proxy + drydock-agent(command: --agent)。
services: socket-proxy: image: tecnativa/docker-socket-proxy container_name: drydock-socket-proxy restart: unless-stopped volumes: - /var/run/docker.sock:/var/run/docker.sock environment: - CONTAINERS=1 - IMAGES=1 - EVENTS=1 - SERVICES=1 - POST=1 healthcheck: test: wget --spider http://localhost:2375/version || exit 118 collapsed lines
interval: 5s timeout: 3s retries: 3
drydock-agent: image: codeswhat/drydock command: --agent container_name: drydock-agent restart: unless-stopped depends_on: socket-proxy: condition: service_healthy ports: - "3000:3000" environment: - DD_AGENT_SECRET=*** # 和 Controller agent.env 一致 - DD_WATCHER_LOCAL_HOST=socket-proxy - DD_WATCHER_LOCAL_PORT=2375已部署的 Agent 位置:
| 机器 | 路径 |
|---|---|
| AI 主机 (.216) | /opt/drydock-agent/ |
| n8n (.181) | /opt/drydock-agent/ |
| Frigate NVR (.160) | /home/dpanel/compose/drydock-agent/ |
| Bandwagon VPS(云主机) | /opt/1panel/docker/compose/drydock-agent/,端口 34300 |
新机器加入流程
- SSH 到目标机器
- 创建
/opt/drydock-agent/docker-compose.yml(复制上方模板) - 确认
DD_AGENT_SECRET与 Controller 的agent.env一致 docker compose up -d- 验证:
curl -s -o /dev/null -w "%{http_code}" http://localhost:3000→ 应返回 401(正常,没有认证头) - 在 Controller 的
agent.env追加对应行(端口非 3000 时加_PORT=xxxx) - Controller 重建:
docker compose up -d - 打开
http://<NAS_IP>:24300,左侧栏应出现新 Agent
通知配置
DryDock 支持 20+ 通知渠道(ntfy、Telegram、Lark、Discord 等)。当前配的是自建邮箱 + Lark Webhook 双通道,汇总模式(batch),所有容器更新合成一条消息。
邮件通知
# agent.env(Controller 侧)DD_NOTIFICATION_SMTP_MAIN_HOST=<mail.your-domain.com>DD_NOTIFICATION_SMTP_MAIN_PORT=465DD_NOTIFICATION_SMTP_MAIN_FROM=<your@email.com>DD_NOTIFICATION_SMTP_MAIN_TO=<your@email.com>DD_NOTIFICATION_SMTP_MAIN_USER=<your@email.com>DD_NOTIFICATION_SMTP_MAIN_PASS=***DD_NOTIFICATION_SMTP_MAIN_TLS_ENABLED=true # ⚠️ 465 端口必须显式开启DD_NOTIFICATION_SMTP_MAIN_MODE=batch踩坑:TLS_ENABLED 默认为 false。465 端口是隐式 TLS—— 服务器先等 TLS 握手,DryDock 发纯文本 SMTP,结果就是 Greeting never received,30 秒超时。必须设为 true。587 端口(STARTTLS)不受影响。
Lark(飞书)Webhook
DD_NOTIFICATION_HTTP_LARK_URL=https://open.larksuite.com/open-apis/bot/v2/hook/<hook-id>DD_NOTIFICATION_HTTP_LARK_MODE=batchLark 可能间歇性返回 400,不影响实际送达。
Apprise 方案(已放弃)
曾尝试用 Apprise(caronc/apprise,130+ 通知服务)统一管理通知。部署了一个 sidecar 容器后发现两个问题:
- 资源开销大:默认按
CPU 核数 × 4启动 worker(4 核 → 16 worker),需限制 512M 内存 +APPRISE_WORKER_COUNT=2 - Stalwart 兼容 Bug:Apprise 发完邮件后发
QUIT时 Stalwart 已关连接,报SMTPServerDisconnected。Gmail / QQ 等主流服务不受影响
仅为邮件通知多跑一个容器 + 16 worker 进程,且有兼容问题,回退到原生 SMTP。
三种通知模式:
| 模式 | 效果 |
|---|---|
simple | 每个容器一条(刷屏) |
batch | 全部汇总一条 ✅ |
digest | 每天定时摘要 |
踩坑记录
env_file 中的 $ 被 shell 吃掉
YAML compose 文件通过 SSH + heredoc 传到远程时,$argon... 变成了 argon...(丢了一个 $)。SSH + shell 对 $ 做了二次解释。解决:用 Python yaml.dump 写到远程,完全绕过 shell。
Agent 端口不匹配
VPS 上 Agent 映射了 34300:3000,但 Controller 的 agent.env 没指定端口,默认连 3000 → 连接失败。解决:加 DD_AGENT_{name}_PORT=34300。
Agent 返回 401 是正常的
Agent 收到无认证的请求返回 401。Controller 通过 DD_AGENT_{name}_SECRET 认证后正常通信。用 curl 测试看到 401 说明 Agent 在跑,不是挂了。
Agent 显示 0 容器(v1.4.x handshake 竞态 bug)
部署完过了一段时间,打开 Web UI 发现几个 Agent 下面一个容器都不显示。SSH 上去 docker ps 明明都在跑,Agent 日志也正常。
翻 Controller 日志发现关键错误链:
Agent ai connected → Handshake successful. Received 5 containers. ✅...(SSE 长连接断开)SSE Connection failed: abortedAgent ai connected → Handshake successful. Received 0 containers. ❌Pruning container drydock-agent (removed on Agent) 🗑 全部清零Pruning container frpc...—— 重连后报 0 容器,Controller 把之前同步的 5 个容器全部清理了。n8n 和 Frigate 的 Agent 更惨,SSE 断开后直接 ECONNREFUSED,放弃重试。
搜到 GitHub Issue #386,这是一个已知的 handshake 冷启动竞态问题:Controller 重连 Agent 的 SSE 流时立即握手(GET /api/containers),而此时 Agent 的第一个 watch cycle 还没跑完,容器 store 真的是空的 —— 握手正确返回 0。几秒后 Agent 的 watch 完成并推送真实数据,但 Controller 侧 UI 已经在握手阶段清零且不再刷新。
修复在 v1.5.0-rc.28:Agent 不在 watch cycle 完成前响应握手,返回空时 Controller 保留上一次的已知状态。修复后日志会看到:
Handshake returned 0 containers; preserving last-known state until the first watch cycle completesSMTP 通知 Greeting never received
465 端口配了 SMTP 但 Controller 日志持续报错:
WARN Error (Greeting never received)WARN Event handler timed out after 30000ms容器内 openssl s_client 明明能连通。根因:TLS_ENABLED 默认为 false,465 端口需要隐式 TLS 但 DryDock 没开,发了纯文本 SMTP,服务器不响应。加一行解决:
DD_NOTIFICATION_SMTP_MAIN_TLS_ENABLED=true升级 v1.5.x:修复与验证
当前运行的 v1.4.5 / v1.4.6 需要升级到 v1.5.0-rc.28+。实际升到了 rc.37(2026-06-15,覆盖多项 CVE 修复)。
fnOS 网络受限无法直接 docker pull,采用本地拉取 → 打包传输 → 远端加载的方式:
# 本地拉取docker pull codeswhat/drydock:1.5.0-rc.37
# 打包docker save codeswhat/drydock:1.5.0-rc.37 | gzip > /tmp/drydock-rc37.tar.gz
# 传输到各主机sshpass scp /tmp/drydock-rc37.tar.gz <host>:/tmp/
# 各主机加载并重启docker load < /tmp/drydock-rc37.tar.gzsed -i 's|codeswhat/drydock$|codeswhat/drydock:1.5.0-rc.37|' <compose-file>docker compose up -d各主机的 compose 路径不同,别搞混:
| 主机 | compose 路径 |
|---|---|
| fnOS (Controller) | /vol1/1000/docker/drydock/docker-compose.yml |
| AI 主机 (.216) | /opt/drydock-agent/docker-compose.yml |
| n8n (.181) | /opt/drydock-agent/docker-compose.yml |
| Frigate NVR (.160) | /home/dpanel/compose/drydock-agent/docker-compose.yaml |
| VPS(云主机) | /opt/1panel/docker/compose/drydock-agent/docker-compose.yml |
升级后所有 Agent 重连,Controller 日志确认正常:
| Agent | 握手结果 |
|---|---|
| AI 主机 | 5 个容器(preserving last-known state) |
| n8n | 3 个容器(preserving last-known state) |
| Frigate | 4 个容器 |
| VPS | 8 个容器 |
| fnOS 本地 | 26 个容器 |
Agent 升级过程中容器重建导致旧容器 ID 被 prune、新 ID 自动注册,最终数据一致。
标签配置:让 DryDock 识别镜像版本
DryDock 默认只对 semver 标签(如 1.2.3、v2.0.1)自动检查更新。对于非 semver 标签(latest、nightly、alpine)或未能自动识别的标签(16-alpine、1.26.1),会显示黄标 / 灰标警告且不检查更新。
没有全局开关 —— 这是设计决策,因为 digest watching 会频繁调用 Docker Hub Pull API,容易触发限流(匿名 100 次 / 6h)。需要按容器逐个配置。
两类标签
| 场景 | Label | 示例 |
|---|---|---|
| semver 但未被识别 | dd.tag.family=loose | gitea:1.26.1、postgres:16-alpine |
| 非 semver,想跟踪更新 | dd.watch.digest=true | rsshub(无 tag)、ttrss:nightly、linkding:latest-alpine |
| 自己 build 的镜像 | 不加 label | homer-health、screenshot-to-code—— 没 registry 可查 |
批量添加
以 fnOS 为例,已为以下容器添加 label:
dd.watch.digest=true:rsshub、linkding、wallos、homebox、donetick、karakeep、homer、vcompose、doocs-md、rustpad、ttrss
dd.tag.family=loose:gitea
不需要改动的:frpc
.69.1(已自动识别 semver)、meilisearch.37.0(已自动识别)、drydock(自监控正常)、bili-sync-rs/mercury/opencc/redis/browserless/alpine-chrome(基础设施,不频繁更新)。与 1Panel 的关系
DryDock 和 1Panel 互补,不是替代:
| 功能 | DryDock | 1Panel |
|---|---|---|
| 镜像更新检测 | ✅ 23 个注册表 | ❌ |
| 更新通知推送 | ✅ 20+ 通道 | ❌ |
| 容器操作(启停) | ✅ | ✅ |
| 网站 / SSL 管理 | ❌ | ✅ |
| 数据库管理 | ❌ | ✅ |
| Docker Compose GUI | ❌ | ✅ |
有 1Panel 的机器两者都跑;没 1Panel 的机器 DryDock Agent 就够了。
文件清单
fnOS:/vol1/1000/docker/drydock/├── docker-compose.yml # Controller + socket-proxy├── agent.env # Agent 连接列表└── docker-compose.yml.bak
AI 主机:/opt/drydock-agent/└── docker-compose.yml # 标准模板
n8n:/opt/drydock-agent/└── docker-compose.yml # 标准模板
Frigate:/home/dpanel/compose/drydock-agent/└── docker-compose.yaml # Dpanel 托管
VPS:/opt/1panel/docker/compose/drydock-agent/1 collapsed line
└── docker-compose.yml # 1Panel 托管,端口 34300总结
DryDock 填补了 Watchtower(太自动)和 Diun(只通知)之间的空白。Controller-Agent 架构对于多机器场景很顺手:加一台机器就是复制一份 compose + 加三行 env,5 分钟的事。
目前 5 台全部接入(Frigate NVR 已连通)。v1.4.x 的 0 容器 bug 通过升级到 v1.5.0-rc.37 修复,加了标签配置后非标准镜像也能正确检测更新。后续打算把通知切到 ntfy,邮件通知容易被 Gmail 归到垃圾箱。

