文章目录
📖 介绍 📖
上篇写了怎么把容器的现状问清楚——用一段 inspect 脚本,把镜像、环境变量、端口、挂载、重启策略导成一份能照抄的清单。后台有读者问得很直接:清单在手,然后呢?
「然后」就是剩下的那个问题:怎么把这份清单反向变成一份可靠的 compose 文件。听着像体力活,实际路上的岔口比想象多——有现成工具帮你反推,有手工对照着敲,还有中间那些看起来能省事、实际上会把坑埋得更深的捷径。
下面四条路各走一遍,每条给出适用场景、真实开销和它各自会咬人的地方;最后汇总成一条动手前必须记住的铁律。
🏡 演示环境 🏡
- 🏠 运行平台:飞牛 NAS(fnOS),Docker Engine 28.x,Docker Compose v2
- 📦 容器规模:20 个左右,compose 管理与裸容器混跑
- 🔍 前置产出:上篇的 inventory.sh 清单(含镜像/环境变量/端口/挂载/网络/重启策略)
- 🌐 网络:全内网操作,不涉及公网

📢 注意: 本文内容为个人学习笔记,仅供参考。附:读者须知
📒 反向生成 compose:四条路,一条铁律 📒
🧭 先想清楚:反向生成的目标是「对得上」,不是「跑起来」
这步想偏了,后面全白费。反向生成的目标不是「让容器跑起来」——那太容易了,docker run 一敲就跑。真正的目标是让新文件描述的状态与线上容器的实际状态逐项对得上,包括那些没人写进过任何文件的变更。
上篇强调过一个事实:compose 文件记录的是创建那一刻的意图,容器跑起来之后的变更走的是另一条路。所以反向生成时,你的输入不是「旧 compose 文件」,而是线上容器的实际状态。这条铁律贯穿全文:
🧭 对照的基准是 inspect 输出,不是任何一份旧文件。新文件生成后,必须回到 inspect 逐项核对。
⚖️ 第一条路:现成反推工具
社区里有专门干这件事的工具,思路是读容器对象,自动吐出等价的 docker run 命令或 compose 片段。
runlike 是用得最多的一个,用法是把它作为一个一次性容器跑起来,指向目标容器:
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
assaflavie/runlike <容器名>
它会输出一条完整的 docker run 命令,把端口、挂载、环境变量、重启策略都还原出来。对单容器快速迁移,这条路的效率最高。
它有几处会咬人:
一是环境变量会被原样还原——包括镜像自带的 PATH 这种噪音,也包括那些当初经由部署面板注入、你早忘了来路的值。输出的命令行可能比你想的长得多,逐项过一遍是必须的。
二是它只看容器对象,不会区分「Compose 管理的孩子」和裸容器。对 compose 容器跑 runlike,得到的是一条巨大的 docker run,而你本来可以只维护一份十几行的 compose 文件。所以它更适合裸容器。
三是挂载路径会按容器视角还原成绝对路径,这在单机迁移时没问题,但坑③那类「路径视角错位」的场景里,宿主机路径要人工再核一遍。
适用场景:单容器、裸容器、快速搬迁。不适合 compose 项目批量反推。
🧮 第二条路:手工对照清单敲 compose
上篇的 inventory.sh 清单,本身就按 compose 的字段顺序组织(镜像 → 命令 → 环境变量 → 端口 → 挂载 → 重启策略 → 网络 → 日志)。手工翻译就是把清单的每一行映射到 compose 键:
services:
<服务名>:
image: <镜像名>:<标签>
container_name: <容器名> # 有固定名字的才写;compose 默认会给项目前缀名
restart: unless-stopped # 对应 RestartPolicy.Name
environment:
- APP_DOMAIN=https://<你的域名>
- SIGNUPS_ALLOWED=false
- TZ=Asia/Shanghai # 不要照抄镜像里的 PATH
ports:
- "8080:80" # 对应 PortBindings;注意 HostIp 空串 = 全网卡
volumes:
- /<数据盘>/docker/<服务名>/data:/data # bind 挂载:宿主路径 -> 容器路径
networks:
- <网络名> # 对应 NetworkSettings.Networks
logging:
driver: json-file
options:
max-file: "5"
max-size: "100m" # 对应 LogConfig
手工路的好处是每一行都是你敲的——哪个字段干什么、为什么这么填,过程里自然就懂了。对只有三五个容器的家庭 NAS,这条路的总开销其实最低。
它会咬人的地方:敲漏。清单十几行,compose 五十行,中间的映射全靠注意力。特别是那些「不在清单上但要单独核」的字段——上篇列过的网络别名、健康检查、标签,手敲时最容易忘。所以手工路必须配一个动作:敲完回头跟 inspect 输出 diff 一遍(方法见后文验证节)。
🤖 第三条路:AI 辅助生成(把清单喂给模型)
把 inventory 清单直接贴给 AI 助手,让它翻译成 compose。这条路的真实效率比预想的高,但有讲究。
给什么:完整的 inspect JSON(或规整后的清单)、目标 compose 版本(v2 语法)、以及一句明确的要求——「逐字段映射,不确定的字段标出来问我,不要编」。这句话很重要:模型对不确定的字段会倾向于「填一个看起来合理的」,而 compose 里一个看起来合理的默认值,上线后就是一次意外。
验什么:AI 生成的 compose 必须走和其他路完全一样的验证流程,而且要多验一项——它有没有把你没给的信息编进去。常见编造包括:给没配健康检查的容器加一个 healthcheck、把重启策略从 unless-stopped 自作主张改成 always、给端口加上协议字段。这些「看起来更专业」的补充,全是与线上实际状态不符的偏差。
适用场景:容器多、字段杂、有时间压力。但前提是你有能力审它的输出——审不出来就别用这条路,错误会藏在格式漂亮的 YAML 里。
📦 第四条路:compose 原生反向(docker compose config)
如果容器本来就是 compose 管的,还有一条捷径:找到它的编排文件(上篇说过怎么找,project= 标签里带路径),再配合 docker compose config 把运行时的合并结果打出来:
cd <编排文件所在目录>
docker compose config
它会展开变量替换、合并 override 文件、补上默认值——输出的是「当初部署时实际生效的完整配置」。这对「文件在但被 override 魔改过」的场景特别有用。
它的局限:输出的是「创建那一刻的意图 + 变量展开」,不包含容器跑起来之后的变更(docker update 改的内存、exec 装的包)。所以对 compose 容器,正确姿势是「第四条路打底 + inspect 逐项核对找差异」,而不是直接拿 config 输出当最终答案。
适用场景:compose 容器、需要理解原始部署意图、批量梳理。
📊 四条路摆在一起
| 维度 | 一 现成工具 | 二 手工对照 | 三 AI 辅助 | 四 compose 原生 |
|---|---|---|---|---|
| 适用对象 | 裸容器 / 单容器 | 少量容器 | 批量 / 字段杂 | compose 容器 |
| 速度 | 快 | 慢 | 快 | 快 |
| 准确性 | 高(容器视角) | 取决于细心 | 必须审核 | 高(但只到创建时点) |
| 学习价值 | 低 | 高 | 中 | 中 |
| 主要风险 | 噪音变量 / 路径视角 | 敲漏字段 | 编造合理默认值 | 漏掉运行期变更 |
| 组合建议 | — | 三五容器 | 有审核能力时 | 先打底再核对 |
推荐组合:compose 容器用「四打底 + inspect 核差异」;裸容器少就手工敲(顺便学一遍),多就用 runlike 或 AI 辅助但必须逐字段审核。任何一条路生成的新文件,都要过同一道终验。
✅ 终验:新文件必须回到 inspect 对一遍
不管走哪条路,最后一步都一样:用新文件在测试环境起一个容器,再拿它的 inspect 输出和线上容器比对。肉眼对 YAML 是不牢靠的——缩进、引号、列表语法都可能让一个字段悄悄失效。
最小验证脚本(把两边的关键字段并排打出来):
#!/usr/bin/env bash
# 用法:./diff-inspect.sh <线上容器名> <新起的容器名>
A=${1:?线上容器}
B=${2:?新容器}
for c in "$A" "$B"; do
echo "== $c =="
docker inspect "$c" --format '
image={{.Config.Image}}
restart={{.HostConfig.RestartPolicy.Name}}
env={{range .Config.Env}}{{.}};{{end}}
ports={{json .HostConfig.PortBindings}}
mounts={{range .Mounts}}{{.Source}}->{{.Destination}};{{end}}
net={{range $k, $v := .NetworkSettings.Networks}}{{$k}};{{end}}
log={{json .HostConfig.LogConfig.Config}}'
done
两份输出并排看。期望是逐行相同;有差异就回 compose 文件改,再起再比。特别留意三处:环境变量的顺序无关紧要但内容不能少;挂载的宿主路径必须是真实存在且有权写入的;网络别名在 inspect 的 Networks 段里。
验证通过后,还有一步容易漏的:把数据搬过去。bind 挂载的数据目录直接 rsync;命名卷用 docker run --rm -v <卷>:/from -v <新卷>:/to alpine cp -a /from/. /to/ 这类方式过一遍。新文件 + 空数据目录 = 服务起来但一片空白,这种「半成品状态」比服务挂了更耽误事。
🧨 反向生成里最容易踩的三个坑
坑一:把「能跑」当「迁移完成」。新容器起来了、页面能打开,就容易收工。但定时任务、cron 作业、数据库连接串里写死的地址——这些不在 inspect 清单里(inspect 只看容器对象,看不到容器内部在跑什么),要在应用层单独确认。
坑二:compose 项目名变了。compose 的网络名和卷名默认带项目前缀(<项目名>_<网络名>)。换目录部署时项目名变了,旧容器引用的旧网络名就成了孤儿。稳定做法:compose 文件里显式写 name: <原项目名>,或网络/卷都用 external: true 引用已有资源。
坑三:忽略时区。上篇说过容器默认 UTC;反向生成时新文件如果忘了 TZ 环境变量,日志时间又慢八小时,对账时又是一轮怀疑错方向。清单里有 TZ=Asia/Shanghai 就必须在新文件里看到它。
🪴 反向生成之后:让文件成为唯一真源
迁移完成不等于事情结束。反向生成的 compose 文件,价值在于从今往后它就是唯一真源——所有变更改文件、up -d 应用,别再 docker exec 或 docker update 直改容器。上篇说的「文件和现实的差距会像滚雪球」,反向生成就是把雪球清零的动作,之后的日常维护决定它会不会再滚起来。
一个低成本的习惯:每次 up -d 后顺手跑一遍那个 diff-inspect 脚本,确认现实仍与文件一致。三十秒的事,能让你永远不需要再做一次「反向生成」。
⚠ 说明: 本文基于 Docker Engine 28.x 与 Docker Compose v2 的实测、2026 年 10 月的官方文档整理。Docker 与 Compose 的命令行为、字段名、第三方工具(如 runlike)的能力在版本间可能调整,请前往官方仓库与文档查看最新内容并自行核实,一切以官网当前版本为准;本文所述仅代表整理时点的整理情况,仅供参考。
⚓ 相关链接 ⚓
- XiaoqiangClub
- NAS 上的容器要搬去新机器?先把它现在的样子问清楚(本系列前作)
- Docker 官方文档 · Compose 文件参考
- Docker 官方文档 · docker compose config
- runlike 项目(容器反推 docker run 命令)
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/xiaoqiangclub/article/details/168773808




