2022 年我做一个项目,要把后端应用和它依赖的数据库、Redis 一起用 Docker 跑起来。多个容器怎么一起编排,我用了 Docker Compose。第一版我做得很省事:每个容器写成一个 service,谁依赖谁,就给它写个 depends_on——应用依赖数据库,那就在应用上写 depends_on: [db],让数据库先起、应用后起,顺序不就对了?本地开发时——真不错:我 docker compose up 一敲,几个容器哗啦啦全起来了,应用也连上了数据库,一把就通。我心里很踏实:"编排嘛,不就是把几个容器写进一个 yaml、用 depends_on 排一下先后顺序?"可等这套东西真正拿去部署、在一台干净的机器上反复重启,一串问题冒了出来。第一种最先把我打懵:我明明写了 depends_on,可应用启动时还是报"连不上数据库"直接崩了——我反复确认顺序没错,数据库的容器确实先起来了,可应用就是连不上。第二种最不安全:我把数据库的密码、端口直接写死在 compose 文件里,这个文件一进 Git,密码就跟着进了仓库;而且本地、测试、生产想用不同的配置,我得改 yaml 文件本身。第三种最肉疼:有一次我重建了数据库容器,结果之前存进去的数据全没了——一查才知道,数据写在容器自己的可写层里,容器一删,数据跟着灰飞烟灭。第四种最乱:容器之间怎么互相访问,我一会儿用 localhost、一会儿把所有端口都 ports 出去,结果要么连不上、要么端口在宿主机上撞了车。我盯着这一连串问题想了很久才彻底想明白,第一版错在一个根本的认知上:我以为"Compose 编排,就是把容器写进 yaml、用 depends_on 排个顺序"。这句话把"编排"想成了"排个启动顺序"这么简单。可它不是。多容器编排,真正要管的根本不止"谁先起谁后起"。它要管的是一组容器作为一个整体协同工作时的全部关系:依赖的容器不光要"先启动",还要等它真正"就绪"——而"启动了"和"就绪了"是两回事;每个容器要用的配置,不能写死在编排文件里,得能随环境切换;有状态的容器(比如数据库),它的数据必须独立于容器本身存活,不能容器一删数据就没;容器之间怎么组网、哪些端口该对外暴露、哪些只该内部可见,都要明确设计。depends_on 那行,只碰到了这一整套关系里最浅的一层——启动顺序;而真正会在部署时咬你的,全是它没覆盖到的那几层。真正做好 Docker Compose 编排,核心不是"用 depends_on 排个顺序",而是理解"启动"不等于"就绪"、把配置从编排文件里抽出来、用数据卷让数据独立于容器存活、设计好容器间的网络与端口。这篇文章就把 Docker Compose 多容器编排梳理一遍:为什么"写了 depends_on 就够了"是错的、healthcheck 怎么让依赖真正等到就绪、配置怎么外置、数据怎么持久化、网络与端口怎么设计,以及重启策略、资源限制、多环境 override 这些把编排真正做扎实要避开的坑。
问题背景
先把那串问题的现象和我的误判讲清楚,后面所有的设计都是冲着纠正这个误判去的。
现象:一套"把容器写进 yaml、用 depends_on 排顺序"的 Compose 编排,部署后冒出一串问题:明明写了 depends_on,应用启动时还是连不上数据库,直接崩溃;数据库密码写死在 compose 文件里,随文件一起进了 Git;数据库容器一重建,之前的数据全丢光;容器之间互连一会儿连不上、一会儿宿主机端口撞车。
我当时的错误认知:"Compose 编排,就是把容器写进 yaml、用 depends_on 排个启动顺序。"
真相:这个认知错在它把"编排"窄化成了"排顺序"。depends_on 这个名字有很强的误导性——它叫"依赖于",会让你以为它处理好了"依赖"这件事的全部。可它实际做的只有一件极有限的事:控制容器的启动顺序——保证 db 这个容器,比 app 这个容器先被 Docker 拉起来。问题在于,"容器被拉起来了"和"容器里的服务能干活了",中间隔着一段不短的时间:一个 Postgres 容器被启动后,它内部还要做初始化——加载配置、准备数据目录、开始监听端口,这可能要好几秒。在这几秒里,容器明明已经"启动"了,但数据库根本"还没就绪"、还不能接受连接。而 depends_on 只等到"启动",就放行了 app——于是 app 一上来就连库,正好撞在数据库还没就绪的这个窗口里,连接被拒,崩溃。这就是"启动 ≠ 就绪"。一旦你看清 depends_on 的能力边界只到"启动顺序"为止,你就会明白:编排要操心的事远不止于此。除了"等就绪",还有配置(不能写死)、数据(不能随容器消失)、网络(容器间怎么找到对方)——这些 depends_on 一个都不管,而它们每一个不处理好,都会在部署时给你一记闷棍。
要把 Docker Compose 编排做对,需要几块认知:
- 为什么"写了 depends_on 就够了"是错的——它只管启动顺序,不管"就绪";
- healthcheck——让"依赖"真正等到被依赖方"就绪"再放行;
- 配置外置——密码、端口等配置抽进环境变量,别写死在 yaml 里;
- 数据持久化——用命名数据卷,让数据独立于容器存活;
- 网络与端口、重启策略、多环境 override 这些工程坑怎么处理。
一、为什么"写了 depends_on 就够了"是错的
先把这件最根本的事钉死:一个容器的生命周期里,有两个截然不同的时间点。一个是"启动"——Docker 把这个容器的进程拉起来了,容器状态变成 running。另一个是"就绪"——容器里的那个服务,完成了自己的初始化,真正可以对外提供服务了。对一个无状态的小服务,这两个点也许只差几十毫秒;但对数据库、消息队列这类需要初始化的有状态服务,这两个点之间,可能隔着好几秒甚至更久。depends_on 这个机制,它盯着的是第一个点——"启动";而一个依赖它的容器,真正需要等的,是第二个点——"就绪"。它等错了点。所以"写了 depends_on 就够了"这个想法,根子上是混淆了"启动"和"就绪":你以为你在说"等数据库准备好",可你实际让 Docker 做的,只是"等数据库的容器被拉起来"。这两件事之间那道几秒钟的缝隙,就是你的应用启动时反复掉进去的那个坑。
下面这段配置,就是我那个"一部署就连不上库"的第一版:
# 反面教材:以为写了 depends_on,顺序对了就万事大吉
services:
db:
image: postgres:16
app:
build: .
depends_on:
- db # 以为:db 先起、app 后起,app 就一定能连上 db
# 破绽一:depends_on 只保证 db 容器"先启动",
# 不保证 db 里的数据库"已经能接受连接"。
# 破绽二:app 一启动就连库,可此刻 postgres 还在初始化 —— 连接被拒。
# 破绽三:本地很难复现 —— 本地拉过镜像、有缓存,db 起得飞快。
这段配置在本地开发时表现不错,因为本地你早就拉过 postgres 镜像、跑过很多次,数据目录的初始化几乎是瞬间完成的;db 容器一启动,几乎立刻就就绪了,那道"启动到就绪"的缝隙窄到你根本撞不进去——问题被"本地环境一切都是热的"这件事掩盖了。它的问题不在某一行配置上——depends_on 的语法完全正确——而在一个被忽略的前提:它默认"容器一旦启动,它提供的服务就立刻可用"。可换到一台干净的部署机器上,情况恰恰相反:镜像要现拉、数据目录要从零初始化,"启动到就绪"的那道缝隙会宽到几秒。于是那串问题就有了解释:应用连不上库,是因为 depends_on 只等到了"db 容器启动",就放行了 app,而 db 此刻还没就绪;而这个 bug 在本地死活复现不出来,正是因为本地那道缝隙太窄了。问题的根子清楚了:做好编排的工程量,全在"承认编排要管的是'就绪',而不只是'启动'"之后——你只用 depends_on 排个顺序,它就只替你管了最浅的那一层,剩下的全在部署时还给你。所以下一步,是给"依赖"装上一个能真正判断"就绪"的探针。
二、healthcheck:让依赖真正等到"就绪"
要让 app 真正等到 db 就绪,得先让 Docker 有办法判断"db 到底就绪了没有"。这个办法,就是 healthcheck(健康检查):你给 db 这个 service 配一条"健康检查命令",Docker 会反复执行它,直到它成功,才把这个容器标记为 healthy。然后,app 的 depends_on 就不再是"等 db 启动",而能升级成"等 db 变成 healthy":
# 给 db 加 healthcheck,让 app 真正等到"数据库就绪"再启动
services:
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"] # 探针:库能接受连接吗
interval: 5s # 每 5 秒探一次
timeout: 3s # 单次探测超过 3 秒算失败
retries: 5 # 连续失败 5 次才判定为 unhealthy
app:
build: .
depends_on:
db:
condition: service_healthy # 关键:等 db 健康检查通过,app 才启动
注意 app 的 depends_on 写法变了——从一个简单的列表,变成了带 condition: service_healthy 的形式。这个 condition 才是关键:它把"等启动"明确改成了"等健康"。但光靠 compose 还不够——容器编排不可能万无一失(健康检查也有探测间隔、网络也会偶尔抖动),所以应用自己在启动时,也该带一层连接重试做兜底:
import time
import psycopg2
# 应用侧兜底:compose 的 healthcheck 之外,自己启动时也带连接重试
def connect_with_retry(dsn, max_attempts=10):
"""编排不保证万无一失,应用启动时自己也要扛一下抖动。"""
for attempt in range(max_attempts):
try:
return psycopg2.connect(dsn) # 连上了,直接返回
except psycopg2.OperationalError:
wait = min(2 ** attempt, 30) # 指数退避,但封顶 30 秒
print(f"数据库还没就绪,{wait}s 后重试 ({attempt + 1}/{max_attempts})")
time.sleep(wait)
raise RuntimeError("重试耗尽,仍连不上数据库")
下面这张图,把一次 compose up 里,依赖容器"等就绪"的完整过程画出来:
这里的认知要点是:healthcheck 的本质,是给"就绪"这个抽象的状态,定义一个具体的、可执行的判定标准。"数据库就绪了吗"这个问题,Docker 自己是答不上来的——它只知道容器进程还在不在,不知道容器里那个数据库到底能不能接活。healthcheck 做的,就是把这个判断权交还给你:你最清楚"就绪"对你的服务意味着什么——对 Postgres 是 pg_isready 能连通,对一个 Web 服务可能是 /health 接口返回 200。你把这个标准写成一条命令,Docker 就能拿它反复探测,直到真的就绪。而 depends_on 配上 condition: service_healthy,才算是把"依赖"这件事说完整了:不是"等它出现",而是"等它真的能干活"。还要记住:编排层的保障和应用层的重试,是两道叠加的防线,缺一不可——healthcheck 解决绝大多数情况,应用侧的连接重试兜住那些编排也保证不了的边角抖动。依赖的就绪问题解决了,接下来是另一个被 depends_on 完全无视的东西——配置。
三、配置外置:别把配置写死在 compose 文件里
开头第二个问题——"密码写死在 compose 文件里"——根子在把"配置"和"编排"混在了一起。一个 compose 文件,描述的应该是"这套系统由哪些容器构成、它们怎么连接"这种结构性的东西;而数据库密码、端口、各种开关,是会随环境变化的"配置"。把配置写死进 compose 文件,有两个坏处:一是敏感信息(密码)跟着文件进了 Git,二是本地、测试、生产想用不同的值,就得改文件本身。正确的做法是把配置抽进一个独立的 .env 文件:
# .env —— 配置从 compose 文件里抽出来,单独放。这个文件不进 Git
POSTGRES_USER=appuser
POSTGRES_PASSWORD=s3cret-change-me
POSTGRES_DB=appdb
APP_PORT=8080
然后 compose 文件里,所有原本写死的值,都改成 ${变量名} 的引用——Compose 会自动从同目录的 .env 里把值读进来:
# compose 文件里用 ${VAR} 引用,值来自 .env,不再写死
services:
db:
image: postgres:16
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
app:
build: .
env_file:
- .env # 把整个 .env 注入 app 容器的环境变量
ports:
- "${APP_PORT}:8080" # 端口也来自配置,不写死
这里的认知要点是:compose 文件和 .env 文件,回答的是两个不同的问题。compose 文件回答"这套系统长什么样"——有几个容器、谁依赖谁、怎么组网,这是系统的结构,它在任何环境下都应该是同一份。.env 文件回答"这一次,在这个环境里,具体用什么值"——什么密码、什么端口、什么开关,这是环境相关的参数,它在本地、测试、生产各不相同。把它们分开,你就得到了两个好处:一是结构和参数解耦,你换个环境部署,compose 文件一个字都不用动,只换一份 .env;二是敏感信息和代码解耦,.env 文件加进 .gitignore 不进仓库,而 compose 文件可以放心进 Git。这其实就是"配置与代码分离"那条老原则,在容器编排这个场景下的又一次体现——编排文件是"代码",.env 是"配置",它们就该分家。配置外置之后,还有一类东西比配置更要命,它一旦丢了就再也回不来——那就是数据。
四、数据持久化:让数据独立于容器存活
开头第三个问题——"重建数据库容器,数据全没了"——是最肉疼的一个。要理解它,得先知道容器的存储是怎么回事:一个容器运行时,它写的文件默认是写在容器自己的"可写层"里的;而这个可写层,和容器的生命绑死——容器一旦被删除、被重建,可写层连同里面的一切,全部消失。对一个无状态服务,这没问题(它本来就没有要保留的数据);可对数据库,这是灾难:它的全部数据都在那个目录里,容器一删,数据就跟着没了。解法是数据卷(volume):把数据库存数据的那个目录,挂载到一个由 Docker 独立管理的"命名卷"上,数据就存在了容器之外:
# 数据持久化:用命名数据卷,数据不再随容器删除而消失
services:
db:
image: postgres:16
volumes:
- db_data:/var/lib/postgresql/data # 把数据目录挂到命名卷 db_data
volumes:
db_data: # 声明一个命名卷,由 Docker 独立管理 —— 容器删了它还在
这里要分清两种挂载,它们用途不同:命名卷(上面那种,db_data:/path)适合放数据库数据,由 Docker 托管、跨容器生命周期存活;绑定挂载(./本地路径:/容器内路径)是把宿主机的某个目录直接映射进容器,适合开发时挂源代码:
# 两种挂载,用途不同,别混用
services:
db:
volumes:
- db_data:/var/lib/postgresql/data # 命名卷:放数据,要持久、要托管
app:
volumes:
- ./logs:/app/logs # 绑定挂载:把宿主机目录映射进去
# 命名卷 —— 数据库数据、上传文件这类"要长期留着"的;
# 绑定挂载 —— 开发时挂源码、把日志落到宿主机这类"和宿主机目录联动"的。
这里的认知要点是:容器和数据,生命周期天然就该是分开的。容器被设计成"用完即弃"的东西——你随时可能因为升级镜像、改配置、扩容而删掉一个容器、再造一个新的,这是容器化的常态,而不是异常。正因为容器是"易朽"的,任何"需要活得比容器更久"的东西,就绝不能存在容器的可写层里。数据卷做的,就是把数据从容器这具"易朽的身体"里剥离出来,放到一个独立存活的地方。这样,容器该删就删、该重建就重建,数据卷在一旁安然无恙,新容器一挂上来,数据原封不动。判断一个目录该不该挂卷,标准只有一个:这里面的东西,在容器被重建之后,你还想不想要?想要,就必须挂卷——数据库的数据目录、用户上传的文件,都属于这一类。这件事没有侥幸:没挂卷的有状态容器,就是一个等着在某次重建里清空你所有数据的陷阱。数据安全了,还剩最后一类编排关系——容器之间怎么互相找到、怎么对外开门。
五、网络与端口:服务名互连,按需暴露
开头第四个问题——"容器互连一团乱"——根子在没搞清容器的网络模型。两个要点。第一:Compose 会自动给这套服务建一个内部网络,同一个网络里的容器,可以直接用"服务名"当主机名互相访问。所以 app 要连数据库,host 不该写 localhost(在容器里 localhost 指的是容器自己),而该写 db 这个服务名:
# 容器间互连:用"服务名"当主机名,不是 localhost
services:
app:
build: .
environment:
# 连数据库,host 写服务名 db,不是 localhost、不是 IP
DATABASE_URL: postgres://appuser@db:5432/appdb
networks:
- backend
db:
image: postgres:16
networks:
- backend
networks:
backend: # app 与 db 同在 backend 网络,彼此用服务名直连
第二:ports 和 expose 是两件事,别混用。ports 是"把容器端口映射到宿主机"——只有真正要从宿主机外部访问的服务才需要它;expose 是"只让端口在容器内部网络里可见"——容器间互访靠的就是它,根本不必占用宿主机端口:
# 端口:只把"真正要给外部访问的"映射到宿主机
services:
app:
ports:
- "8080:8080" # 映射到宿主机:外部浏览器要访问 app,需要它
db:
expose:
- "5432" # 只在内部网络可见:app 连 db 用它就够了
# 错误做法:给 db 也写 ports "5432:5432" ——
# 既白占宿主机端口、易和别的服务撞车,又把数据库直接暴露到外网,危险。
这里的认知要点是:容器编排里的网络,要分清"对内"和"对外"两个面。对内,是容器之间互相通信:Compose 给它们建了一个隔离的内部网络,在这个网络里,服务名就是主机名,app 想找 db,直接喊一声 db 就行——既不需要知道对方的 IP(容器 IP 是会变的),也不需要走宿主机端口。对外,是这套系统要不要把某个端口开放给外界:这是一个要审慎对待的决定,因为每暴露一个端口,就是在系统的外墙上开一个洞。原则很简单:默认全部对内,只有确实需要被外部直接访问的(比如对外提供 HTTP 服务的 app),才用 ports 开放出去。像数据库这种,它的所有客户端都在内部网络里,就绝不该用 ports 暴露——把数据库端口映射到宿主机,既无必要,又凭空给自己开了一个安全缺口。一句话:容器间互连用服务名走内部网,对外暴露用 ports 且能省则省。编排的五块主干到这就齐了,最后是几个真正长期运维一套 compose 才会撞见的工程坑。
六、工程坑:重启策略、资源限制与多环境
五块设计之外,还有几个工程坑,不处理就会让你的编排要么不够健壮、要么难以维护。坑 1:给容器配重启策略,让它异常退出能自愈。容器里的进程可能因为各种原因崩溃退出。默认情况下,它崩了就崩了,不会自己起来。给 service 配一个 restart 策略,能让它异常退出后自动重启——常用 unless-stopped:崩溃会自动重启,但你手动停掉它,它就老实不动。坑 2:给容器设资源上限,别让一个容器吃垮整台机器。默认一个容器能用光宿主机的全部 CPU 和内存。一旦某个容器内存泄漏,它能把整台机器拖垮、连累其它容器。所以要给它设 CPU 和内存上限:
# 重启策略 + 资源上限:让容器能自愈,又不会拖垮宿主机
services:
app:
build: .
restart: unless-stopped # 异常退出自动重启;手动停掉则不再起
deploy:
resources:
limits:
cpus: "1.0" # 最多用 1 个 CPU 核
memory: 512M # 内存上限 512M,超了该容器被 OOM 杀掉
# 关键:有了内存上限,泄漏的是"这一个容器被杀",而不是"整机被拖垮"。
坑 3:多环境用 override 文件,而不是维护多份完整 compose。本地、生产需要不同的配置(本地要挂源码、开调试;生产不要)。错误做法是维护两份完整的 compose 文件——它们大部分内容重复,改一处要改两次,迟早改漏。正确做法是:一份 docker-compose.yml 放公共结构,再用一份 docker-compose.override.yml 放本地专属的差异,Compose 会自动把 override 叠加上去:
# docker-compose.override.yml —— 本地开发用,自动叠加在基础配置上
services:
app:
volumes:
- ./src:/app/src # 本地:挂载源码,改代码即时生效,免重建镜像
environment:
DEBUG: "true" # 本地:打开调试开关
db:
ports:
- "5432:5432" # 本地:把 db 端口暴露出来,方便用客户端连
# 基础 compose 只放公共结构;生产部署不加载这个 override,用自己的配置。
坑 4:理解构建缓存,别每次都全量重建。app 用 build: 从 Dockerfile 构建镜像。改了代码后 compose up,Compose 不一定会重新构建——它有缓存。要确保用上新代码,该用 docker compose up --build;而构建慢,往往是 Dockerfile 里没利用好分层缓存(比如先 COPY 全部代码、再装依赖,导致改一行代码就重装所有依赖)——正确顺序是先单独 COPY 依赖清单、装依赖,再 COPY 其余代码。坑 5:容器要打日志到标准输出,而不是写进容器内的文件。容器化的约定是应用把日志直接打到 stdout/stderr,由 Docker 统一收集(docker compose logs 就能看)。如果你把日志写进容器内的某个文件,它既不方便查看,又会随容器删除而丢失,还白白撑大容器的可写层。
关键概念速查
| 概念 / 手段 | 说明 |
|---|---|
| depends_on | 只控制容器启动顺序,不保证被依赖服务已经就绪 |
| 启动 vs 就绪 | 容器进程拉起是"启动",服务初始化完成可用才是"就绪" |
| healthcheck | 给容器配健康检查命令,通过后才标记为 healthy |
| service_healthy | depends_on 的条件,让依赖方等到被依赖方健康才启动 |
| .env 文件 | 把密码、端口等配置抽出来,不写死在 compose 文件里 |
| 命名数据卷 | 由 Docker 托管的卷,数据独立于容器生命周期存活 |
| 绑定挂载 | 把宿主机目录直接映射进容器,适合开发时挂源码 |
| 服务名互连 | 同网络容器用服务名当主机名访问,不用 localhost 或 IP |
| ports 与 expose | ports 映射到宿主机对外开放,expose 仅容器内部可见 |
| override 文件 | 基础 compose 放公共结构,override 叠加环境差异 |
避坑清单
- depends_on 只排启动顺序,不等"就绪",别指望它解决依赖问题。
- 给数据库等有状态容器配 healthcheck,依赖方用 service_healthy 等就绪。
- 应用启动时自己也要带连接重试,作为编排之外的兜底。
- 密码、端口等配置抽进 .env,别写死在 compose 文件里。
- .env 文件加进 .gitignore,绝不让密码随文件进 Git。
- 有状态容器的数据目录必须挂命名卷,否则容器一删数据全丢。
- 分清命名卷与绑定挂载:前者放数据,后者开发挂源码。
- 容器间互连用服务名当主机名,不要用 localhost。
- 只给真正要对外访问的服务配 ports,数据库不要暴露到宿主机。
- 配重启策略与资源上限,多环境差异用 override 文件而非多份配置。
总结
回头看那串"应用连不上库、密码进了 Git、数据全丢、互连一团乱"的问题,以及我后来在编排上接连踩的坑,最该记住的不是某一个配置项的写法,而是我动手前那个想当然的判断——"Compose 编排,就是把容器写进 yaml、用 depends_on 排个启动顺序"。这句话错在它把"编排"窄化成了"排顺序"。我以为多容器协作,难的就是先起谁后起谁。可我忽略了一件事:把一组容器编排成一个能协同工作的整体,要操心的关系远不止"启动顺序"这一条。被依赖的服务,不光要"先启动",更要真的"就绪"——而这两件事中间隔着一道几秒钟的缝隙;每个容器要用的配置,不能和编排结构焊死,得能随环境切换;有状态容器的数据,必须独立于容器存活,不能容器一删就灰飞烟灭;容器之间怎么组网、哪些端口对外、哪些只对内,都要明确设计。depends_on 那一行,只够到了这一整套关系里最浅的一层;真正会在部署时反复咬你的,全是它够不到的那几层。
所以做好 Compose 编排,真正的工程量不在"把容器写进 yaml、连上 depends_on"那几行上。那几行,谁都会写。真正的工程量,在于你要承认"编排管的是一组容器协同工作的全部关系,而不只是启动顺序",并据此把每一类关系都安排妥当:依赖方要等的是"就绪"不是"启动",你就给被依赖方配 healthcheck、用 service_healthy 去等;配置不能写死,你就把它抽进 .env、让编排文件保持纯净;数据不能随容器消失,你就给有状态容器挂上命名卷;容器要互相找到对方,你就用服务名走内部网络、按需暴露端口;容器会崩、会泄漏,你就配上重启策略和资源上限。这篇文章的几节,其实就是顺着这条线展开的:先想清楚"写了 depends_on 就够了"为什么错,再讲 healthcheck 怎么等就绪、配置怎么外置、数据怎么持久化、网络端口怎么设计,最后是重启、资源、多环境这几个把编排守扎实的工程细节。
你会发现,Docker Compose 编排,和现实里"装修一套房子时,统筹各个工种进场"完全相通。一个粗心的包工头会怎么做?他排了个进场顺序——先水电、后木工、再油漆,就以为万事大吉(这就是只写了 depends_on)。可"水电工进场了"根本不等于"水电做完了"——木工按表进了场,却发现水电还在埋管、墙都没法上钉,只能干等着,活全乱套(这就是"启动 ≠ 就绪")。而一个讲究的包工头怎么做?他每个工种做完,都安排一次验收,验收通过了,下一个工种才进场(这就是 healthcheck 和 service_healthy);他把装修的尺寸、用料这些会随每套房变化的东西,统一记在一份图纸上,而不是直接写死在墙上,换一套房子,只换图纸,施工章法不变(这就是配置外置和多环境 override);他分得清什么是"装修期间的临时脚手架"——拆了就拆了,什么是"房子的承重墙和已铺好的地砖"——这些绝不能因为某个工种返工就砸掉重来(这就是容器的可写层与持久化数据卷);他还让工人之间靠工地内部的对讲机沟通,只有大门是对外开放的,绝不会把每个房间的窗户都拆开冲着马路(这就是服务名内部互连与按需暴露端口)。同样是张罗一套房子的装修,可粗心的包工头让工期一拖再拖、还留下一堆隐患,讲究的包工头让每个工种都严丝合缝地咬合上——差别不在"排不排顺序"这件事本身,只在他认不认"统筹施工管的是所有工种协同作业的全部关系,而不只是排个进场表"这件事。
最后想说,编排做没做对,差距永远不会在"本地开发、镜像早拉好了、一切都是热的"时暴露——本地容器起得飞快,"启动到就绪"的缝隙窄到你撞不进去,数据卷没挂你也一时半会儿发现不了,配置写死在文件里反正就你一个人改,你会觉得"写进 yaml、连上 depends_on"已经够用。它只在真实的、要在一台干净机器上从零部署、要反复重启、要在多个环境之间切换的时候才显形。那时候它会用最难堪的方式给你结账:做不好,你会因为应用抢在数据库就绪之前启动而每次部署都崩一次,会因为没挂数据卷而在一次例行的容器重建里弄丢全部数据,甚至因为密码写死在 compose 里而让它随仓库泄露出去;而做对了,你的整套服务在任何一台干净机器上 compose up 一敲就稳稳起来,依赖严丝合缝地按就绪顺序咬合,数据在容器一次次重建中安然无恙,换个环境只需换一份配置。所以别等"一次干净部署把整套服务搞崩"那一刻找上门,在你写下每一个 service、连下每一行 depends_on 的时候就该想清楚:这套编排等的是"就绪"还是只是"启动"、配置外置了吗、有状态容器的数据挂卷了吗、容器间怎么组网、端口暴露得克制吗,这一道道工序,我是不是都替它想过了?这些问题有了答案,你交付的才不只是一份"本地能 up 起来"的 compose 文件,而是一套能在任意环境一键拉起、经得起反复重启和迁移考验的可靠编排。
—— 别看了 · 2026