远程 Docker 服务同步 — 从仓库到远程宿主
代码在工作机上,服务跑在一台远程 Docker 宿主上。更新一个服务看起来只是三条命令:同步目录、远端构建、重启容器。但真正让人白折腾的是另外三件事:顺序(谁必须先构建)、状态(哪些东西不随容器重建而更新)、验证到什么程度算完。
目录
- 何时使用
- 参数与占位符
- 症状索引
- 心智模型:三层状态,各自独立
- 同步五步
- 新服务首次上线清单
- 七条顺序与禁令
- 验证到什么程度算完
- 回滚
- 错误处理约定
- 失败模式与恢复
- 完整走查(端到端样例)
- 不适用场景
何时使用
自动触发:用户描述与下表场景相关时直接启用,不需要点名本技能。
| 用户会这么说 | 实际场景 |
|---|---|
| 「改了配置同步上去,重启后还是老行为」 | 三层状态只动了一层 → 顺序 1 |
| 「主服务重启后,依赖它的容器全起不来」 | 镜像必须先于进程 → 顺序 2 |
| 「容器一直重启,日志里看不出原因」 | restart policy 是设计决策 → 顺序 3 |
| 「端口在听,外网就是连不上」 | 防火墙双链 → 防火墙一节 |
| 「线上手改了反代配置,下次部署又被覆盖」 | 权威源在仓库 → 顺序 7 |
手动调用:对话里说「用远程 Docker 服务同步处理」,或直接用 @ 引用本技能(平台的完整 slug 可能带作者后缀,不必写全名)。
不适合:本机单机服务、K8s 部署、无 SSH 权限的托管服务——见「不适用场景」。
参数与占位符
动手前先替换下表占位符;未替换就跑等于用错环境。
| 占位符 | 含义 | 从哪里取 |
|---|---|---|
<service> / <svc> |
服务名 | ls <repo>/deploy/(目录名即服务名) |
<deploy-dir> |
远端部署根目录 | 部署环境 env 文件里的部署路径项 |
<port> |
SSH 端口 | 部署环境 env 文件里的 SSH_PORT |
<env-file> |
连接参数文件 | 部署目录下的 env 文件(set -a; source …) |
症状索引
| 症状 | 看哪节 |
|---|---|
| 改了配置/代码,同步并重启后行为没变 | 顺序 1(镜像未重建) |
| 重启主服务后,依赖它的容器起不来 | 顺序 2(镜像必须先于进程) |
| 容器反复重启,日志看不出原因 | 顺序 3(restart policy 是设计决策) |
| 本地改了某服务,远程覆盖不生效 | 顺序 4(权威源在外部) |
| 端口在听却连不上,或重启宿主后规则丢了 | 防火墙(双链 + 幂等重放) |
| 线上手改了反代配置,下次部署被覆盖 | 顺序 7(配置回写仓库) |
心智模型:三层状态,各自独立
更新一个服务,实际在动三样东西,它们不会一起更新:
- 源文件(部署目录里的 compose、Dockerfile、配置)——
scp同步 - 镜像(由 Dockerfile 构建出来的产物)—— 必须显式
build,compose 不会因为文件变了就重建 - 运行中的容器(进程 + 挂载 + 环境)—— 必须显式
up -d或restart,且只在镜像/配置真的变了时才有意义
"我同步了但没生效" 九成是这三层只动了一层。
同步五步
# 1) 载入连接参数(从部署环境的 env 文件读,不要硬编码主机名)
set -a; source ./<env-file>; set +a
# 2) 只读检查:先看清现状再动手
ssh -i "$SSH_KEY" -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" "docker ps -a"
# 3) 只同步相关目录(整仓同步会连带覆盖远程的生成物)
scp -P "$SSH_PORT" -r <repo>/deploy/<service>/ "$SSH_USER@$SSH_HOST:<deploy-dir>/"
# 4) 远端重建:先 build 再 up
ssh ... "cd <deploy-dir>/<service> && docker compose build && docker compose up -d"
# 5) 容器三件套
ssh ... "docker ps --filter name=<service> && docker image ls | head && docker logs --tail 50 <container>"
收尾验证——端到端冒烟:从反向代理 / 网关侧真实打一次功能请求。容器内日志干净不等于验证完成——端口没通、鉴权没接上、证书不匹配,容器日志全都正常。
> 端口参数别写错:scp 是大写 -P,ssh 是小写 -p;两者都从同一份 env 读 $SSH_PORT,避免一处改了一处没改。
> Windows 用户注意:set -a; source ... 是 bash 语法。PowerShell 下请改用 WSL / Git Bash,或逐条显式设置变量——直接粘贴到 PowerShell 会静默失败。
新服务首次上线清单
- [ ] 部署物进
deploy/<service>/:compose 或 build 定义 + README 写清安装与回滚步骤 - [ ] 端口暴露想清楚:控制面绑 loopback,对外流量一律走反向代理 + 平台鉴权,不要直接把管理端口开到公网
- [ ] 防火墙:先区分三种情况——①只用
-p发布端口(走 Docker 自动规则,通常不需要动DOCKER-USER);②宿主端口被别的进程占用;③宿主有自定义前向策略。只有第 ③ 种才需要补DOCKER-USER—— 先跑iptables -L DOCKER-USER -n看这条链里有没有自定义规则,再决定加不加;只加INPUT而宿主又确实过滤了前向流量时,才会出现"本机能连、容器连不上" - [ ] 规则落进幂等的 systemd 服务,而不是手工
iptables——docker 重启会重建链,手工规则会丢 - [ ] 凭据不进 git:仓库只放
.env.example,现场生成(如openssl rand -hex 32) - [ ] 有状态数据落到独立卷目录,不随容器生命周期;备份策略与保留份数一并定下来
- [ ] 需要开机存活/周期执行的,装 systemd unit 后
daemon-reload+enable --now - [ ] 验证:容器三件套 + 端到端冒烟 + 重启宿主后服务仍在(这一步才真正验证 enable 生效)
七条顺序与禁令
这些是"看起来可以合并、实际合并就出事"的操作顺序。
1. 改了文件不等于重建了镜像
docker compose up -d 在镜像已存在时不会重新构建——哪怕源文件全变了。要么显式 build,要么用 up -d --build。这是"同步了没生效"最常见的原因。
2. 镜像必须先于依赖它的进程
如果主服务(例如 API)在启动时可能拉起某个兄弟容器,那么镜像必须在主服务重启之前构建好。顺序错了,表现为"重启后那个容器起不来",而日志往往落在另一个服务里。
判定方法:画出启动依赖——谁会被谁在启动时拉起,先构建被拉起者的镜像。
3. restart policy 是设计决策,不是默认值
对由外部看门狗管理的容器,restart: no 往往是有意为之:看门狗在主进程退出后停容器、按需重建。把它改成 always 会与看门狗打架,表现为无限重启或资源泄漏。
改这一行之前,先问:这个容器平时是谁负责拉起?
4. 权威源在外部时,本地目录只是镜像
有些目录虽然在你的仓库里,但权威源已经迁移到另一个仓库/产品——本地这份是同步副本。在这种目录里改东西,改完会被下次同步覆盖,且改动不会生效。
判定方法:目录里如果有"权威源已迁走"的说明,或者它的变更流程要求先改另一处,就按镜像对待:去源头改,再同步回来。
5. 安全控制类服务不要配 ExecStop
做流量管控/出站白名单这类服务,restart 时必须保持管控在位。给它们配 ExecStop 会在重启窗口里把管控摘掉——这个窗口正是最危险的时刻。
同理,oneshot + RemainAfterExit 的 timer 要注意:这种组合下 OnUnitActiveSec 不会再次排期(算出来是无穷),必须用 OnCalendar 这类绝对时间表达式。
6. 共享密钥必须在两侧同步轮换
当两个组件(如平台 API 与某个独立服务)共享同一个签名密钥时,.env 里各存一份——轮换必须两侧同时改,只改一侧 = 一侧签一侧验不过,表现为"登录/令牌突然全部失效"。
7. 反代配置改完要回写仓库
线上手改反向代理配置很快,但下次部署会覆盖它。改完立刻把变更回写到仓库里的配置副本,否则线上与仓库持续漂移,最后没人知道哪份是对的。
验证到什么程度算完
| 层级 | 检查 | 算不算完成 |
|---|---|---|
| 容器在跑 | docker ps |
❌ 只说明进程没退出 |
| 镜像已更新 | docker image ls 看创建时间 |
❌ 只说明构建成功 |
| 日志干净 | docker logs --tail 50 |
❌ 只说明启动没报错 |
| 端口可达 | 从外部 curl 一次 |
⚠️ 接近,但不含鉴权与业务逻辑 |
| 端到端 | 从网关侧真实调一次业务请求 | ✅ 完成 |
| 重启存活 | 重启宿主后服务自动回来 | ✅ 新服务必做 |
回滚
- 镜像回退到上一个 tag(保留最近 2–3 个 tag 是低成本保险)。
docker compose down后切回旧 compose,再up -d。- 反向代理恢复备份配置 → 先 test 再 reload。
- 有状态数据:卷不随容器删除,但迁移/降级前先备份。
错误处理约定
本技能把出错分成四类,处置与重试策略不同,不要混着来:
| 类别 | 典型情况 | 处置 | 可重试 |
|---|---|---|---|
| 输入错误 | 参数缺失、路径或格式不对 | 直接拒绝,指出缺哪个字段 | 否 |
| 环境错误 | 权限、网络、依赖服务不可用 | 给检查命令,停下等修复 | 修复后 |
| 证据不足 | 日志、指标、状态缺失 | 先补证据,不做判断 | 补证据后 |
| 结论冲突 | 多个判据指向不同根因 | 列冲突点,先安全后性能 | 否 |
统一输出格式(每次给结论都按这四段写,便于他人复核):
判据:<看到的现象与具体取值>
结论:<属于哪一类 / 最可能的根因>
动作:<最小修改,或需要补的证据>
验证:<怎么确认修好了>
本技能常见错误码:
| 代码 | 现象 | 处置 |
|---|---|---|
E-ENV-01 |
同步并重启后行为没变 | 远端 docker compose build <svc> 再 up -d(顺序 1) |
E-ENV-02 |
依赖服务起不来 | 按「镜像 → 进程」顺序重来(顺序 2) |
E-ENV-03 |
容器反复重启 | 先查崩溃原因,再定 policy(顺序 3) |
E-ENV-04 |
端口在听但外网不通 | 按「无自定义策略 / 端口被占 / 自定义前向策略」三种情况分别处置 |
E-INP-05 |
scp / ssh 连不上(认证失败) | 修部署环境 env 里的连接参数 |
输入校验清单、重试与退出语义见 references/error-handling.md。
失败模式与恢复
| 失败现象 | 常见原因 | 检查方法 | 恢复动作 | 可否重试 |
|---|---|---|---|---|
| 同步并重启后行为没变 | 只同步了源文件,镜像没重建 | docker inspect -f '{{.Created}}' <image> 看镜像构建时间 |
远端 docker compose build <svc> 再 up -d(顺序 1) |
可重试 |
| 依赖服务起不来 | 顺序错:进程先于镜像 | docker ps -a 看退出码与启动先后 |
按「镜像 → 进程」顺序重来(顺序 2) | 可重试 |
| 容器反复重启 | restart policy 与崩溃原因叠加 | docker inspect -f '{{.RestartCount}}' <c>,再 docker logs --tail 50 <c> |
先查崩溃原因,再定 policy(顺序 3) | 不重试(先查因) |
| 端口在听但外网不通 | 宿主有自定义前向策略时只加 INPUT 不够(无自定义策略则通常与 DOCKER-USER 无关) |
iptables -S 或 nft list ruleset 查两条链,并确认端口是否真被 -p 发布 |
按情况补 DOCKER-USER 或宿主端口规则,并做幂等重放 |
可重试 |
| scp / ssh 连不上(认证失败) | 密钥路径、端口或用户配错 | ssh -i <key> -p <port> <user>@<host> "docker ps" |
修部署环境 env 里的连接参数 | 可重试 |
失败时优先中止:远端构建或 up -d 失败时先停,不要把半更新的容器留在生产;保留上一版镜像 tag 以便回滚。
完整走查(端到端样例)
走查一:scp 了 Dockerfile 但没重建镜像
场景:改了 Dockerfile 并同步到远端,重启后行为没变(镜像未重建,顺序 1)。
输入:用户原话「Dockerfile 和配置都 scp 上去了,容器也重启了,跑起来还是老逻辑」。
scp -P <port> -r deployment/<service>/ "$SSH_USER@$SSH_HOST:<deploy-dir>/<service>/"
ssh -p <port> "$SSH_USER@$SSH_HOST" "docker inspect -f '{{.Created}}' <service>/api:latest"
ssh -p <port> "$SSH_USER@$SSH_HOST" "docker ps --format '{{.Names}} {{.Image}} {{.Status}}'"
中间输出(示例):
2026-10-24T03:11:52.612Z
api-1 app/api:latest Up 6 minutes
定位与修复:容器是 Up 的,但镜像是 10-24 构建的,早于本次改动 → 只同步了源文件、没重建镜像(三层状态只动了一层)→
- 远端
cd <deploy-dir> && docker compose build api; docker compose up -d api;- 保留上一版 tag 以便回滚(顺序 1、顺序 2)。
修复后复测:
$ ssh -p <port> "$SSH_USER@$SSH_HOST" "cd <deploy-dir> && docker compose build api && docker compose up -d api"
[+] Building 41.2s (12/12) FINISHED
Container api-1 Recreated
$ ssh -p <port> "$SSH_USER@$SSH_HOST" "docker inspect -f '{{.Created}}' <service>/api:latest"
2026-10-31T08:31:07.884Z
$ curl -s -o /dev/null -w '%{http_code}\n' https://<site>/api/health
200
结论:验证「镜像必须先于进程」——重启容器不会让新 Dockerfile 生效,只有 build 才会。
走查二:build 成功但容器仍跑旧镜像(tag 复用 + 未 recreate)
输入:用户原话「build 明明 FINISHED,重启容器代码还是旧的」。
ssh -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" "cd <deploy-dir> && docker compose build <service>"
ssh -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" "docker image ls --format '{{.Repository}}:{{.Tag}} {{.ID}} {{.CreatedSince}}' <service>"
ssh -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" "docker inspect -f '{{.Image}}' <container>"
中间输出(示例):
[+] Building 12.4s (10/10) FINISHED
app/api:latest 3f9ac1b2d4e5 2 seconds ago
app/api:latest 9c02f7d5a1b3 6 days ago
sha256:9c02f7d5a1b3...
定位:同一个 latest tag 下有两个镜像 ID,而运行中容器的 .Image 仍是 6 天前那个——build 只更新了镜像层,容器没有 up -d 换过去(同步五步的第 4 步被跳过)。重启容器会复用旧镜像,所以「重启也没用」。
修复:docker compose up -d --force-recreate <service>;长期做法是给镜像打不可变 tag(时间戳或 commit),不要用 latest 覆盖。
复测:
ssh -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" "docker inspect -f '{{.Image}} {{.Config.Image}}' <container>"
sha256:3f9ac1b2d4e5... app/api:latest
结论:验证三层状态(源文件 / 镜像 / 容器)——build 只动第二层,验证要把容器的 .Image 与镜像 ID 对齐,而不是只看 build 日志。
分工:走查一覆盖「镜像没重建」,走查二覆盖「镜像重建了但容器没换」,正是三层状态里最容易被跳过的那一层。
不适用场景
- Kubernetes 部署:滚动更新、探针、配置分发是另一套模型,本文的顺序规则不成立。
- 无 Docker 的裸机 systemd 服务:没有镜像层,"build 先于进程"这条无意义。
- 已完全流水线化的 CI/CD 发布:如果发布由流水线触发,手工同步反而会造成漂移。
反模式与常见问题
反模式
- 只
scp不build—— 最常见的"改了没生效",浪费最多时间。 - 整仓
scp—— 覆盖远程的生成物与本地配置,把小改动放大成事故。 - 只加
INPUT防火墙规则 —— 本机 curl 通、容器连不上,排查方向全错。 - 把 watchdog 容器的 restart policy 改成
always—— 与看门狗互相拉起,表现为无限重启。 - 在镜像副本目录里改配置 —— 改动被下次同步覆盖,且没人知道为什么没生效。
- 拿"容器日志正常"当验收 —— 端口、鉴权、证书全都可能坏着。
- 线上手改反代配置不回写 —— 线上与仓库漂移,下次部署悄悄回退。
- 共享密钥只轮换一侧 —— 全部登录态失效,且报错指向无关模块。
常见问题(FAQ)
Q:docker compose up -d 到底会不会重建镜像?
镜像不存在时会构建;镜像已存在时不会。源文件变了必须显式 --build 或先 build。
Q:为什么容器在跑,从外部却连不上?
三个高发原因:只绑了 loopback、端口没真正发布或宿主有自定义前向策略(仅第三种才涉及 DOCKER-USER)、反向代理没配或证书不匹配。按这个顺序查。
Q:同步目录时能不能直接同步整个仓库?
不行。远程常有本地没有的生成物(日志、数据、证书),整仓同步会覆盖它们。只同步目标服务目录。
Q:重启宿主后服务没起来,怎么查?
systemctl status <unit> 看是否 enable;docker ps -a 看容器退出码;再看 compose 的 restart policy 是否被改过。开机存活是新服务验收的必做项。
延伸阅读
references/error-handling.md