ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Docker Compose多容器编排:YML替代docker run

2026/9/29 6:11:07 拓冰建站 浏览量
Docker Compose多容器编排:YML替代docker run 把docker run敲到第三十遍的时候我基本就认命了——这活不该人干。参数长得像一串乱码端口、挂载、环境变量、网络别名改一个忘三个第二天想复现昨晚调通的那套环境得翻半天 shell 历史记录还不一定翻得着。所以当我把 Docker Compose 的 YML 真正写顺手之后回头再看那些手敲的命令行感觉就像用记事本写代码和用 IDE 写代码的区别。这篇就聊透一件事怎么用一份 YML 把整套服务的创建和启动一次搞定。Docker Compose 解决的核心问题不是少敲几个字而是把一次性的手工操作变成可版本管理、可复现、可交接的基础设施描述文件。你把它提交进 Git同事 clone 下来一条命令就能跑出跟你一模一样的环境这才是它真正的价值所在。这篇适合已经会写单条docker run、但还没把多容器编排理顺的人如果你连 Docker 都还没装建议先把基础的镜像、容器、网络、数据卷这几个概念过一遍再回来不然下面的内容会有点跳。1. 从 docker run 到 YML多服务编排到底省了什么1.1 手敲命令的四个真实痛点先别急着夸 Compose我们先看看不用它的时候到底有多难受。第一个痛点是参数漂移。生产上跑的 MySQL 你挂载了/data/mysql:/var/lib/mysql本地测试的时候手一抖写成了/data/mysql:/var/lib/mysql/多加了个斜杠容器能起来但数据实际写到了容器层一删容器数据全没了。这种错误在命令行里极难自查因为命令执行完就没了没有留下任何可供对比的痕迹。第二个痛点是启动顺序靠人肉记忆。后端起服务的时候要连数据库数据库没起来应用就崩你得先docker runmysql等十秒再docker runredis再等最后跑应用。顺序记错了就重来一遍。第三个痛点是网络配置重复劳动。每个容器都得--network mynet忘了加就连不通排查半天才发现是网络没指定。第四个痛点是交接成本。你把一套环境调好了要让同事复现只能把几条命令截图发过去对方复制粘贴一通中间漏了个-e参数环境就不一致了然后开始互相甩锅。这四个痛点指向同一个根因命令行的运行态是瞬时的、不可版本化的。你没法对一条敲过的命令做 code review没法 diff 出这次和上次到底哪里不一样。而 YML 文件是文本、是文件、是可以进 Git 的它天然解决了可追溯和可复现两个问题。这就是为什么一旦服务数量超过两个我就再也不手敲 run 了。1.2 一份 YML 到底描述了哪些东西很多人第一次看 docker-compose.yml 会觉得字段太多了记不住其实它的结构非常对称理解了三层就全通了服务services、网络networks、数据卷volumes。这三者恰好对应了容器运行需要的三个维度——跑什么、怎么互通、数据放哪。服务层描述的是每个容器长什么样包括用什么镜像、暴露哪些端口、挂载哪些目录、注入什么环境变量、依赖谁先启动。网络层描述的是这些容器之间怎么互相找到对方。数据卷层描述的是持久化的数据存在宿主机的哪里。你用docker run的时候这三件事是混在一坨参数里的拆成 YML 之后各归各位代码可读性直接上了一个台阶。这里有个容易被忽略的认知点Compose 不是运行时它只是运行时的描述器和调度器。底层调用的一切还是 Docker Engine 那套 APICompose 干的事就是把你写的 YML 翻译成一堆 API 调用按依赖顺序依次执行。理解了这一点你就能明白为什么 Compose 出问题的时候排查思路往往要退回到docker ps、docker logs、docker network inspect这些底层命令上——症状在 Compose病根可能在 Docker 本身。1.3 什么场景该上 Compose什么场景别硬上Compose 最适合的场景是单机多容器开发环境、测试环境、小型生产部署、个人项目、家用服务器上的自建服务集群。像自建私有镜像仓库、跑一套 Git 服务、搭个媒体库、部署一套带数据库和缓存的后端这些都是 Compose 的舒适区。一台机器一份 YML一条命令拉起全套运维心智负担极低。但有两种情况我会明确建议你别硬用 Compose。一是跨多台机器的集群编排这时候该考虑的是 Kubernetes 那一类东西Compose 只管单机它没有调度、没有副本管理、没有自愈能力容器挂了不会自动漂移到别的机器上。二是需要精细滚动更新和流量治理的线上核心业务Compose 的更新方式是停旧起新会有短暂中断做不到平滑的灰度切流。当然如果你的业务对几秒钟的中断不敏感用 Compose 跑生产是完全可以的很多中小型团队就是这么干的稳定运行几年没问题。判断标准很简单你的服务能不能接受重启期间短暂不可用能接受Compose 就够用。提示Compose 有两个世代Python 写的docker-composev1命令带横杠和 Go 重写的docker composev2命令带空格作为 Docker CLI 插件。v1 已经停止维护新环境一律用 v2。判断方法很简单敲docker compose version能出版本号就是 v2。2. Compose 文件结构与核心字段逐个拆开讲2.1 顶层字段写什么、不写什么一份标准的 Compose 文件顶层就三个核心块外加一个可选的配置块。services 是必填的所有容器定义都在里面networks 和 volumes 是选填的只有当你需要自定义网络名或者手动声明数据卷的时候才写不写的话 Compose 会自动创建默认网络和匿名卷。这里有个很多教程还在教、但现在已经过时的东西顶层的version:字段。老版本的 Compose 文件必须写version: 3.8这种否则报错。但从 Compose Spec 统一之后v2 会直接忽略这个字段你写了不报错但会打印一行警告说它已经被废弃。我现在的习惯是干脆不写文件更干净也不会有警告干扰日志输出。如果你接手的是老项目还带着 version 字段不用急着删不影响功能等哪天顺手改文件的时候一起去掉就行。还有一个顶层字段值得单独提一下name。它用来指定这个 Compose 项目的名称默认情况下项目名取自你所在目录的文件夹名。这个默认行为埋着一个坑——如果你把项目文件夹重命名了或者在不同目录下放了同名文件Compose 会认为是两个不同的项目于是数据卷和网络都会重新创建一份之前的数据就消失了其实还在只是挂在旧项目名下面。生产环境我强烈建议显式写上name把它钉死避免误操作。name: my-stack services: # 各个服务定义 networks: backend: volumes: mysql-data:2.2 服务定义里的关键字段哪些必须写哪些别乱写服务块是整份文件的肉字段最多也最容易写错。我按使用频率和踩坑概率排一下序。image 或 build二选一。用现成镜像就写 image要从 Dockerfile 构建就写 build可以同时写这时候 build 负责构建image 负责给构建出来的镜像打标签。生产上我倾向构建和运行分离YML 里只写 image镜像由 CI 流程提前构建好推送到私有仓库部署机器只负责拉取。这样部署机器不需要源码也不需要构建缓存干净利落。ports 和 expose 的区别是新手最容易搞混的。ports是把容器端口映射到宿主机格式是宿主机端口:容器端口比如8080:80外部才能访问。expose只是声明这个容器在内部网络里开放哪些端口不做宿主机映射主要用于文档化和让同一网络内的其他容器能访问。绝大多数时候数据库、缓存这类只需要被内部服务访问的容器根本不需要写 ports写了反而是把 3306 直接暴露到公网安全性直接掉一个档次。这一点我在实际项目里反复强调因为它是个纯粹的安全问题不写 ports 数据库照样能被同网络的容器连上。environment 和 env_file都用来注入环境变量。少量变量直接写 environment 挺方便的但只要涉及密码、密钥、Token我一律走 env_file把敏感值放在.env或者单独的文件里然后把那个文件加进.gitignore。把密码硬编码在 YML 里提交到仓库是运维事故的高发区尤其是一些内部项目仓库权限管理松等于把数据库密码公开发布了。volumes的写法有个语法细节短格式- ./data:/var/lib/mysql用的是相对路径相对的是 Compose 文件所在目录长格式可以指定更多选项。这里我个人有个偏好——业务数据用命名卷配置文件用绑定挂载。命名卷由 Docker 管理性能好、迁移方便配置文件用绑定挂载方便直接编辑宿主机上的文件。但要注意绑定挂载的路径权限问题容器内的进程如果以非 root 用户运行宿主机目录的属主 ID 对不上就会报权限错误这个坑后面细说。restart字段决定了容器退出后的行为。可选值里我常用unless-stopped意思是除非手动停止否则总是重启。这个值比always好用的地方在于你手动docker compose stop之后重启宿主机它不会自作主张地把容器拉起来而always会。开发环境我一般用no避免改代码后容器无限重启刷日志。healthcheck是判断容器真的可用而不是进程还在的关键也是后面讲依赖顺序的基础单独开一节细说。2.3 变量替换与 .env别把配置写死在文件里Compose 支持在 YML 里用${变量名}做文本替换替换的值来源于三个地方shell 环境变量、Compose 文件同目录下的.env文件、--env-file指定的文件。优先级上 shell 环境变量最高.env次之。这个机制最大的价值是同一份 YML 适配多套环境。比如开发、测试、预发三套环境只有镜像 tag、数据库地址、端口这些不一样其他全一样那就可以只维护一份 YML用三份不同的.env文件切换。启动的时候docker compose --env-file .env.test up -d一下就切过去了。services: api: image: ${REGISTRY}/my-api:${TAG} environment: DB_HOST: ${DB_HOST} DB_PASSWORD: ${DB_PASSWORD}# .env 文件内容示例 REGISTRYregistry.internal.example.com TAG1.4.2 DB_HOSTmysql DB_PASSWORDchange-me-in-real-env注意.env文件是明文别把它当保险箱用。真正的生产密钥应该走密钥管理服务或者运行时注入.env只适合放非敏感的配置项。另外有个易错点YML 里如果要用字面量的美元符号得写成$$否则会被当成变量引用解析然后报变量未设置的警告。3. 一份可直接抄作业的多服务编排实例3.1 需求拆解先把架构画清楚再动手写光讲字段没意思我们直接上一套完整的东西。假设你要部署一个典型的小型后端服务组件包括一个对外提供 HTTP 接口的应用服务、一个 MySQL 8.0 做数据存储、一个 Redis 做缓存和会话、一个 Nginx 做反向代理和静态资源分发。四个容器一台机器。先把关系理清楚Nginx 暴露到宿主机 80 和 443 端口它把/api的请求转发给应用服务的 8080 端口应用服务连 MySQL 的 3306 和 Redis 的 6379这两个端口不对外暴露MySQL 的数据必须持久化Redis 我开了 AOF 也持久化四个容器在同一个自定义 bridge 网络里通过服务名互相访问。这个关系图在脑子里过一遍YML 基本就成型了因为 Compose 的每个字段都在回答我需不需要这个问题。这里多提一句服务命名的重要性。Compose 会把服务名自动注册为网络内的 DNS 别名所以在应用配置里数据库地址直接写mysql就行不用写 IP。这是 Compose 里我最喜欢的设计它让配置文件彻底摆脱了硬编码 IP 的泥潭。你在本地跑通了把文件丢到服务器上照样跑通因为服务名没变。3.2 完整 YML 逐段注解下面这份文件我加了详细注释可以直接拿走改。注意我用的是 Compose Spec 格式不写 version。name: demo-stack services: nginx: image: nginx:1.25-alpine container_name: demo-nginx ports: - 80:80 - 443:443 volumes: # 配置文件走绑定挂载方便直接改 - ./nginx/conf.d:/etc/nginx/conf.d:ro - ./nginx/certs:/etc/nginx/certs:ro depends_on: api: condition: service_healthy networks: - frontend - backend restart: unless-stopped api: image: registry.internal.example.com/demo/api:1.4.2 container_name: demo-api # 不写 ports只走内网 environment: SPRING_PROFILES_ACTIVE: prod DB_HOST: mysql DB_PORT: 3306 DB_NAME: demo DB_USER: demo DB_PASSWORD: ${DB_PASSWORD} REDIS_HOST: redis REDIS_PORT: 6379 depends_on: mysql: condition: service_healthy redis: condition: service_healthy healthcheck: test: [CMD, wget, -qO-, http://localhost:8080/actuator/health] interval: 10s timeout: 3s retries: 6 start_period: 40s networks: - backend restart: unless-stopped mysql: image: mysql:8.0 container_name: demo-mysql command: - --character-set-serverutf8mb4 - --collation-serverutf8mb4_unicode_ci - --default-time-zone08:00 environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} MYSQL_DATABASE: demo MYSQL_USER: demo MYSQL_PASSWORD: ${DB_PASSWORD} volumes: - mysql-data:/var/lib/mysql # 首次初始化时执行的建表脚本 - ./mysql/init:/docker-entrypoint-initdb.d:ro healthcheck: test: [CMD, mysqladmin, ping, -h, 127.0.0.1, -u, root, -p${MYSQL_ROOT_PASSWORD}] interval: 10s timeout: 5s retries: 10 start_period: 60s networks: - backend restart: unless-stopped redis: image: redis:7-alpine container_name: demo-redis command: - redis-server - --appendonly - yes - --requirepass - ${REDIS_PASSWORD} volumes: - redis-data:/data healthcheck: test: [CMD, redis-cli, -a, ${REDIS_PASSWORD}, ping] interval: 10s timeout: 3s retries: 6 start_period: 20s networks: - backend restart: unless-stopped networks: frontend: backend: volumes: mysql-data: redis-data:逐段说一下设计考量。Nginx 同时挂在 frontend 和 backend 两个网络上因为它是唯一需要同时面对外网和后端服务的组件。api、mysql、redis 只挂 backend彼此能通但不会跟任何其他项目意外互通。网络分层这个习惯值得养成它相当于给容器做了一次逻辑隔离比全部塞一个默认网络里要清晰得多。MySQL 的command里我改了字符集和时区。这两项如果不改中文乱码和时间差八小时的问题迟早找上门而且是在业务跑了一段时间之后才暴露排查起来很烦。一次性在编排文件里定死比事后改数据库参数要省事得多。它挂载的docker-entrypoint-initdb.d目录是个约定官方镜像会在数据目录为空时执行这个目录下的.sql、.sh文件用来做首次初始化。注意数据目录为空这个前提也就是说这个脚本只会在第一次启动、且数据卷为空的时候跑后面重启不会再执行——这是设计如此不是 bug。Redis 我开了 AOF 持久化并设了密码。带密码的 Redis 才敢放在任何能连通的网络里这是底线。健康检查里用redis-cli -a 密码 ping返回 PONG 才算健康。3.3 网络与数据卷的设计取舍上面那份文件里网络和数据卷都是声明式的只写了个名字没有加任何配置。这种写法下 Compose 会自动创建名为项目名_网络名的 bridge 网络。之所以还要显式声明是为了控制名字和后续的复用。有个细节值得展开数据卷用命名卷还是绑定挂载本质上是在可移植性和可控制性之间做选择。命名卷由 Docker 管理路径在/var/lib/docker/volumes/下面你不太方便直接拿工具去改里面的文件但它的好处是备份和迁移有标准做法跨机器搬运也简单。绑定挂载的优点是文件位置你自己说了算可以直接用编辑器改备份就用普通的文件同步工具缺点是路径、权限、SELinux 上下文这些麻烦事全得你自己处理。我自己的分界线是这样的数据库、对象存储这类数据是核心资产的用命名卷因为它们的写入模式由数据库自己管你也不该手动去改里面的文件配置文件、证书、静态资源这类人需要经常维护的用绑定挂载改起来方便。另外绑定挂载记得加:ro只读标记除非容器真的需要写入只读能挡住不少误操作。提示如果你之前用文件夹名跑过这套服务后来改了name字段Docker 会把新项目当成全新项目命名卷也会重新创建旧数据还在旧卷里。这种情况不用慌用docker volume ls找到旧卷然后docker run --rm -v 旧卷名:/from -v 新卷名:/to alpine cp -a /from/. /to/就能迁移这是我在实际项目里用过好几次的办法。3.4 启动、验证与日常运维命令文件写好了接下来是启动。核心命令就一条docker compose up -d-d是 detached后台运行。如果你想看完整的启动日志就先不加-d看它跑起来、确认没有报错再 CtrlC 停掉改回-d不过更推荐的做法是加-d然后用docker compose logs -f 服务名看指定服务的日志这样不会被其他服务的输出淹没。启动之后必须验证不能看到Started就以为成了。我一般分三步走。第一步docker compose ps看四个容器的状态是不是都是 Up重点是健康检查那一列有没有哪个是starting或者unhealthy——starting说明还在等待期unhealthy说明检查连续失败得去看日志。第二步docker compose logs --tail50 mysql这种情况就不用多说了日志永远是最好的朋友。第三步实际打一下接口curl -I http://localhost/以及curl http://localhost/api/health确认整条链路真的通了。日常运维用到的命令就那么几条我把高频的整理成表命令用途备注docker compose up -d创建并后台启动所有服务首次会自动构建、拉镜像、建网络和卷docker compose up -d --build强制重新构建镜像再启动改了 Dockerfile 时用docker compose ps查看服务状态和健康状态排障第一站docker compose logs -f api实时跟踪某服务日志加--tail200限定行数docker compose restart api重启单个服务改完配置但不想重建容器时用docker compose down停止并删除容器和网络默认保留数据卷docker compose down -v连数据卷一起删危险会丢数据慎用docker compose config渲染出最终配置检查变量替换、语法是否正确docker compose config这条命令我强烈建议大家养成习惯。它会把所有变量替换后的最终 YAML 打印出来同时做语法校验。当你觉得我明明改了配置怎么没生效的时候先跑这个命令八成能发现问题——要么是变量没读到要么是改错了文件。4. 启动顺序与依赖depends_on 到底管不管用4.1 depends_on 的真实语义边界depends_on是新手最容易误解的字段。它的语义是控制启动顺序不控制就绪状态。也就是说写了depends_on: mysqlCompose 会先启动 mysql 容器再启动 api 容器但它只保证mysql 容器的进程已经创建不保证 MySQL 已经初始化完成、可以接受连接了。MySQL 容器从启动到真正能接受连接中间要经历初始化数据目录、启动服务、加载权限表好几秒甚至几十秒这个空档期里 api 启动就会连不上数据库然后崩溃退出。所以经常有人问我明明写了 depends_on 为什么还是连不上数据库答案就是这个。在不带condition的短语法下depends_on 只是一个启动排序器不是就绪等待器。它解决的是A 必须在 B 之后启动这种纯粹的顺序问题比如你先要建网络、再启动依赖它的服务这类场景它能胜任。要真正做到等数据库就绪再启动应用必须用长语法配合condition: service_healthy这也是我在上面那份 YML 里的写法。它要求被依赖的服务必须定义了healthcheck然后 Compose 会等到健康检查通过才启动下游服务。这个组合才是真正意义上的依赖就绪等待。4.2 healthcheck 怎么写才靠谱健康检查写得好不好直接决定了启动顺序机制能不能用。写健康检查有三个关键点。第一检查命令必须是容器内真实存在的。常见错误是写了个容器里没装的可执行文件比如在某些精简镜像里用curl但镜像里只有wget结果健康检查永远失败容器一直处于 unhealthy下游服务永远等不到。我一般会先docker compose exec 服务名 bash进去确认一下有哪些命令可用再决定用哪个。上面 YML 里 MySQL 用mysqladmin pingRedis 用redis-cli ping应用用wget都是对应镜像里自带的。第二start_period 必须给足。这个参数的意思是容器启动后的宽限期这段时间内健康检查失败不记为失败。它专门用来对付那些启动慢的服务。MySQL 首次启动要初始化数据目录慢的时候能到一分钟所以我把 start_period 设成 60s。如果这个值设得太小会出现一种很诡异的现象容器明明在正常初始化健康检查却已经判定它失败并重启它重启后又开始初始化陷入死循环。这类问题看起来像服务本身有问题实际上是编排参数没配好。第三interval 和 retries 要配合业务容忍度。interval 是检查间隔retries 是连续失败多少次算 unhealthy。两个值相乘大致就是服务出问题后多久被发现。开发环境可以设得激进一点10 秒一次、3 次失败快速发现问题生产上我会稍微放宽避免因为一次瞬时抖动导致容器被重启。healthcheck: test: [CMD-SHELL, pg_isready -U ${DB_USER} -d ${DB_NAME}] interval: 15s timeout: 5s retries: 5 start_period: 30s注意test的两种写法行为不同。CMD形式不会经过 shell不支持管道和重定向CMD-SHELL会走 shell可以用|、这类语法但也会触发变量替换。需要组合命令的时候用CMD-SHELL简单命令用CMD更稳。4.3 应用侧的重试机制才是最后一道防线即便健康检查配得再好我依然坚持应用必须有自己的重试逻辑。原因很实在健康检查有间隔比如每 10 秒查一次那理论上最坏情况下应用会在数据库还没完全就绪的窗口里启动而且分布式的世界里服务中途重启是常态依赖方的重连能力比启动顺序重要得多。具体做法因技术栈而异。用连接池的话大多数连接池都支持配置初始连接重试比如设置最大重试次数和重试间隔用消息队列的客户端一般都有自动重连参数自己写的服务就更简单了启动时连不上就等几秒重试重试若干次再退出。这个机制平时看不出价值等到某天数据库因为磁盘满被自动重启时你就会庆幸应用能自己扛过去。我记得有一次做迁移数据库要重建应用重启了三次都失败最后发现是连接池的初始重试次数配得太少改大之后就顺利起来了。那次之后我就把重试能力当成服务的必备项而不是可选项。5. 常见报错与排查实录5.1 启动阶段的高频问题速查Compose 的报错信息不算友好很多问题需要退到底层去看。我把这些年遇到的高频问题整理成表方便按症状对号入座。报错或症状大概率原因处理办法port is already allocated宿主机端口被占用ss -lntp | grep 端口找占用进程或改映射端口no configuration file provided当前目录没有 YML 文件确认文件名是docker-compose.yml或compose.yaml或用-f指定容器起来立刻退出启动命令报错或环境变量缺失docker compose logs 服务名看真实报错permission denied挂载目录宿主机目录属主 ID 与容器内用户不匹配调整宿主机目录属主或指定容器运行用户变量没替换日志里出现空值.env不在 Compose 文件同目录或变量名拼错用docker compose config检查渲染结果下游服务连不上上游用了短语法 depends_on没等就绪改用condition: service_healthy并配健康检查健康检查一直 unhealthy检查命令容器里不存在进容器确认可用命令换掉检查命令数据消失了项目名变了挂到了新卷上确认name字段用docker volume ls找旧卷这张表里我觉得最值得多说的是权限那条。用绑定挂载配置目录的时候如果容器内的进程以 UID 1000 运行而你宿主机上的目录属主是 root容器读不了这个目录就会报权限错误。解决办法有两个把宿主机目录chown成对应的 UID或者在 YML 里用user:字段显式指定容器以哪个用户运行。我个人的偏好是把配置文件目录的属主统一成一个固定 ID团队成员本地都是同一个 ID避免每个人的环境都不一样。5.2 容器都起来了但服务不通的排查路径这是最头疼的一类问题因为docker compose ps显示全是 Up健康检查也是 healthy但接口就是 502 或者超时。这种情况我有一套固定的排查路径按顺序走基本能定位。第一步确认容器之间能不能通。进到应用容器里docker compose exec api sh然后试试ping mysql或者nc -zv mysql 3306。能通说明网络层没问题问题在应用配置不通就是网络配置有问题检查两个服务是不是在同一个网络里服务名有没有写错。注意nc不一定有wget或者telnet也行。第二步确认端口对不对。应用的配置里是不是连的 3306数据库有没有改过监听端口容器内部端口和宿主机映射端口别搞混了。曾经有个同事排查了两小时最后发现是他在应用配置里填了宿主机映射出去的端口号而实际上容器间通信应该用容器端口。这个错误特别常见因为宿主机端口和容器端口不一致的时候很容易记混。第三步看 Nginx 的转发配置。502 通常意味着 Nginx 找到了 upstream 但连不上。检查proxy_pass里写的服务名和端口是否正确注意 Nginx 配置里的服务名解析是在 Nginx 启动时做一次还是每次请求做一次——如果 Nginx 比应用先启动它启动时解析的 IP 可能是旧的应用重建之后 IP 变了Nginx 还在往老 IP 发请求就会 502。这个问题在容器重建之后特别容易出现。提示上面那个 DNS 缓存问题有两个解法。一是把 Nginx 的depends_on配上应用的condition: service_healthy保证顺序二是用 Docker 内置的解析器resolver 127.0.0.11 valid10s;配合变量形式的proxy_pass让 Nginx 每次请求都重新解析。第二种更彻底我在生产环境用的就是这个。5.3 几个不报错但很坑的配置陷阱有些配置问题不会让你看到任何红色报错容器也跑得好好的但隐患就在那儿。第一种是日志无限增长。容器默认的日志驱动是 json-file并且没有任何大小限制一个跑得久、输出又多的服务日志文件能涨到几十 GB把磁盘吃满。解决办法是在服务里配日志轮转services: api: logging: driver: json-file options: max-size: 50m max-file: 5这一项我建议写进所有服务的默认模板里成本极低能省掉一次磁盘告警。第二种是没有限制资源。容器默认能用宿主机的全部 CPU 和内存某个服务内存泄漏的时候会把整台机器拖死其他服务跟着遭殃。生产环境我会用deploy.resources.limits给每个服务加上内存上限超了就 OOM 重启至少不会波及邻居。第三种是用 latest 标签。image: mysql:latest看起来很省事实际上埋了颗雷——哪天你重新拉镜像版本变了数据目录格式或者配置项可能不兼容服务起不来。生产环境的镜像标签一律钉死到具体版本升级要走显式的变更流程。6. 关于这套编排方式我自己的一些实操体会6.1 生产环境里我坚持的几条保守原则用 Compose 跑生产这几年我慢慢形成了几个不太会破例的习惯。第一条是永远不用docker compose down清理生产环境因为它会删掉网络下次 up 的时候重建期间如果有容器引用了旧网络会造成短暂的连通问题。要停止服务就用stop要更新单个服务就用up -d 服务名Compose 会只重建那一个其他服务不动。第二条是配置文件和镜像分离。YML 里不写任何跟环境强相关的值全部走.env服务器上单独维护一份.env权限设成 600。这样镜像可以在任何环境跑配置随环境走出了问题也容易定位到底是镜像问题还是配置问题。第三条是变更前先docker compose config一次。这条命令能提前发现变量缺失和语法问题比在线上 up 失败了再回滚要主动得多。我一般还会在变更前把当前的 YML 备份一份万一新配置有问题把文件换回来再 up 一次就恢复原状了回滚成本极低这也是 Compose 相比复杂编排系统的一个明显优势。第四条是数据库的升级永远单独走流程。千万别指望改一下 YML 里的镜像版本然后 up 一下就完事。MySQL 大版本升级涉及数据字典变更一旦升上去基本回不来。我的做法是先把数据全量备份出来然后在测试环境完整走一遍升级加验证确认没问题了再操作生产并且保留旧容器和数据卷一段时间随时准备切回去。6.2 几个提高效率的小技巧最后分享几个用得顺手的小技巧。第一个是给服务加 profile。有些服务只在开发环境需要比如数据库的图形管理界面、日志查看工具这些不该出现在生产环境。Compose 的 profiles 功能正好干这个services: adminer: image: adminer:latest profiles: - dev ports: - 8081:8080这样默认docker compose up -d不会启动它只有加上--profile dev才会拉起来一份文件适配两套场景。第二个是用docker compose watch需要较新版本做开发时的自动同步。改本地代码容器里自动更新比每次重建镜像快得多。开发后端服务的时候这个体验提升是肉眼可见的。第三个是把常用的组合命令写成 Makefile 或者脚本。像备份数据库然后重启服务这种操作敲一遍命令要三行写成脚本就是一次调用还能避免手滑打错。这类自动化在团队协作里价值更高因为它把操作规范固化下来了新人不用记命令照着脚本跑就行。第四个是关于container_name的取舍。我上面 YML 里给每个服务都写了 container_name好处是容器名字干净好认docker logs demo-api就能看不用敲一长串带项目前缀的名字。但代价是失去了横向扩展能力——同一个服务不能起多个副本因为名字会冲突。如果你的场景需要临时起多个副本压测那就别写 container_name用默认的自动命名。这个取舍得看你的实际需求我自己是单副本为主所以基本都写。这套东西我用了几年从最早的一份 YML 管三个容器到后来管十几个服务文件的复杂度上去了但核心逻辑一直没变把运行环境当成代码来管理。每次写 YML 的时候多想一步这个东西换台机器还能跑吗答案如果是不能那就说明还有硬编码没抽干净。好的编排文件从开发机搬到生产机除了.env里的几个值其他一个字都不用改。