尧图精选

从 docker run 到 Docker Compose:多容器编排实战与排障指南

🕒 发布时间:2026/9/19 1:45:46 📁 来源:尧图网络
我见过有人把项目里所有容器的启动命令一句句堆进 shell 脚本每加一个服务就往里追加一行 docker run最后脚本长到没人敢动也见过同事在新环境部署时对着聊天记录一条条找参数敲到第三条才发现端口映射写错了。多容器编排听起来玄乎其实核心就一句话别再手动重复敲 docker run交给 Docker Compose 用一份声明式配置来统一管理。这篇文章不聊高深的调度原理就聊怎么把三条 docker run变成一份 docker-compose.yml再顺带把日志查看、健康检查、数据卷、网络这些多容器场景必然遇到的问题讲透。适合刚学完 docker run、正在被多容器互相踩坑折磨的新手也适合想把项目部署方式整理得更规范、更好复现的开发者。1. 从三条 docker run 说起多容器编排到底难在哪在讲 Compose 之前先还原一个最典型的场景你会发现很多项目就是从这个场景开始失控的。1.1 一个典型项目的真实场景假设你有一个博客应用架构不复杂Nginx 反代前端、后端服务、Redis 做缓存、MySQL 存数据。为了省事先只算三个核心容器后端、Redis、MySQL。按照官方文档和能用就行的原则你会敲出类似这样的命令docker run -d --name redis-cache -p 6379:6379 redis:7-alpine docker run -d --name mysql-db -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDsecret \ -e MYSQL_DATABASEblog \ mysql:8.0 docker run -d --name blog-app -p 8080:8080 \ --link redis-cache --link mysql-db \ -e REDIS_ADDRredis-cache:6379 \ -e MYSQL_ADDRmysql-db:3306 \ blog-app:v1每条命令单独看都不复杂-d后台运行--name给容器起名-p映射端口-e注入环境变量--link让容器之间能访问。但三条命令放在一起问题就来了。--link其实已经是被官方标记为遗留的机制它靠往容器的/etc/hosts里写条目来解析对方地址而且只在容器启动时生效。你在这个例子中还能用是因为你手动控制了启动顺序。一旦服务多起来--link的依赖关系会变成一团乱麻。真正稳定可靠的方案是让所有容器加入同一个用户自定义网络用服务名做 DNS 解析——这正是 Compose 默认做的事情后面实操部分会展开。1.2 手工命令的三个致命问题第一启动顺序全凭记忆。MySQL 容器起来不等于 MySQL 进程已经准备好接受连接。在--link方案下后端容器如果比 MySQL 先启动或者 MySQL 还没初始化完成后端就会连接失败。你可能见过很多应用带重试逻辑但并不是所有程序都有于是先启动谁、后启动谁这件事就成了部署环节最大的隐性负担。第二参数分散且不可复现。三条命令里的端口、密码、网络配置、镜像版本散落在不同的地方。你今天在本机敲一遍没问题三个星期后在新服务器上再部署一遍可能就发现密码忘了、端口换过了、镜像 tag 拉不到了。你没法保证两次部署用的是同一套配置这才是比敲命令麻烦更致命的问题——部署结果不可控。第三修改配置必须删除重建。docker run的语义是创建并启动一个新的容器它不允许你修改已存在容器的端口映射或环境变量。也就是说每次调整参数你得先docker rm -f把旧容器干掉再重新敲一遍完整命令。配置稍微复杂一点这条命令就会变成一长串没法看的文本复制粘贴时漏一个参数排查半天。1.3 那些容易被忽略的 docker run 细节除了上面三个大问题docker run本身的不少细节也会在实战中埋雷。--rm参数就很有迷惑性。它的作用是容器退出时自动删除容器文件系统适合临时跑任务比如拉一个测试镜像执行完就走。但如果某个服务本身需要长期驻留你误加了--rm一旦进程异常退出容器连同日志一起被清掉现场都没得看。我见过有人在调试时用docker run --rm -d --name hermes hermes-image以为这样既能后台跑又能自动清理结果容器一崩日志全没了调试变成盲人摸象。容器名冲突也是高频坑。同一个宿主机上容器名是唯一的。你要是之前已经跑过一个hermes下次再执行docker run -d --name hermes ...会直接报Conflict. The container name /hermes is already in use。解决办法只有docker rm或者换名字多容器多项目混在一起时起名和清理就变成了负担。再就是日志。docker run前台运行时日志直接刷在终端上所以你第一次体验觉得很直观。一旦加上-d后台运行终端什么都看不到想看日志只能docker logs 容器名。三个容器你就得开三个终端窗口分别看日志一多定位问题全靠肉眼比对时间戳。后面我会讲docker compose logs是怎么把这件事变简单的。2. Docker Compose 的设计思路把命令变成声明式配置Docker Compose 不是什么玄乎的调度平台它本质上做了一件看起来很笨但极其有效的事把docker run的参数原封不动地写进一个 YAML 文件然后由它来负责创建网络、按依赖启动容器、管理生命周期。2.1 从命令参数到配置字段的映射只要你熟悉docker run的常用参数写 Compose 文件基本没有学习成本因为就是一一对应docker run 参数Compose 字段作用-d无需配置up -d本身即后台后台运行--namecontainer_name指定容器名-pports端口映射-eenvironment环境变量-vvolumes数据卷挂载--networknetworks自定义网络--restartrestart重启策略--link不需要服务名自动解析容器间访问这个映射关系非常重要。很多人看到 Compose 文件第一反应是又要学一套新语法实际你只要把它当成用 YAML 写 docker run 参数心理包袱就卸掉一半。剩下的只是 Compose 帮你多做了一些约定俗成的事情。最典型的一个约定是同一个 Compose 文件里的所有服务默认会被放进同一个自定义网络。在这个网络里每个服务都可以直接用服务名作为主机名访问其他服务不需要知道对方的 IP。这就彻底替代了--link的活而且网络是动态创建的容器重建后 IP 变了也不影响互相访问。2.2 docker-compose.yml 的核心结构现代版本的 Docker ComposeV2 已集成到 docker CLI直接执行docker compose即可推荐使用不带version字段的 Compose Specification 格式。一个最小可用的文件长这样services: redis: image: redis:7-alpine app: image: my-app:v1 ports: - 8080:8080 environment: REDIS_ADDR: redis:6379顶层只有services下面每个键就是一个服务。services.redis声明了一个名为 redis 的服务字段和docker run参数一一对应。两个服务在同一个 Compose 网络里所以app访问redis只需要把地址写成redis:6379而不是localhost:6379更不需要查 IP。除了services还有两个顶层字段你会经常用到networks和volumes。它们的作用是声明式地定义需要的网络和数据卷。不用的时候 Compose 会使用默认网络但如果你有多个 Compose 项目需要互通或者需要挂载命名卷做持久化就会用到这两个字段。命名卷的声明方式也很简单volumes: mysql-data:声明之后服务里就可以引用它services: mysql: image: mysql:8.0 volumes: - mysql-data:/var/lib/mysql这里mysql-data:/var/lib/mysql的语义是把名为mysql-data的卷挂载到容器的/var/lib/mysql目录。卷不存在时 Compose 会自动创建docker compose down也不会删除它数据就安全了。2.3 depends_on 的真实效果与限制depends_on是 Compose 里最容易被误解的字段新手几乎都会在这里踩坑。它的字面意思是依赖某个服务于是很多人以为加上它就能保证被依赖的服务已经完全就绪。实际上默认的depends_on只控制启动顺序不控制就绪状态。比如services: app: depends_on: - mysqlCompose 的保证只是先启动 mysql再启动 app。但 mysql 容器启动成功只代表容器进程起来了不代表 mysqld 已经初始化完成、可以接受连接。app 容器可能仍然在 mysql 还没就绪时就尝试连接然后报错退出。标准解法是给被依赖的服务加上healthcheck然后让depends_on带条件services: mysql: image: mysql:8.0 healthcheck: test: [CMD, mysqladmin, ping, -h, localhost, -pmysql123] interval: 5s timeout: 3s retries: 10 app: depends_on: mysql: condition: service_healthy这样 Compose 会在 mysql 通过健康检查后才启动 app。这个细节值得反复强调只要是用 Compose 编排互相依赖的服务就要养成healthcheck condition的习惯否则depends_on就是一个看起来有用、实战中经常害你的假保证。3. 实操一个 Web 应用 Redis MySQL 的完整编排理论讲完直接进入实战。这次我们把开头那三条docker run重写成一份 Compose 文件顺便把网络、卷、健康检查这些该有的东西都补齐。3.1 目标与目录结构假设项目是一个后端应用blog-app依赖 Redis 和 MySQL。目录结构如下my-blog/ ├── docker-compose.yml └── .env.env文件用来放密码这类敏感配置避免直接写死在 YAML 里。Compose 会自动读取同目录下的.env并用${变量名}的语法在 YAML 中引用。这样一份配置文件可以轻松适配多套环境开发、测试、生产只靠换.env即可。3.2 编写 docker-compose.yml下面是一份可以直接落地的配置我逐段解释关键点services: redis: image: redis:7-alpine container_name: blog-redis restart: unless-stopped command: redis-server --requirepass ${REDIS_PASSWORD} volumes: - redis-data:/data healthcheck: test: [CMD, redis-cli, -a, ${REDIS_PASSWORD}, ping] interval: 5s timeout: 3s retries: 5 mysql: image: mysql:8.0 container_name: blog-mysql restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} MYSQL_DATABASE: blog volumes: - mysql-data:/var/lib/mysql healthcheck: test: [CMD, mysqladmin, ping, -h, localhost, -p${MYSQL_ROOT_PASSWORD}] interval: 5s timeout: 3s retries: 10 app: build: . image: blog-app:v1 container_name: blog-app restart: unless-stopped ports: - 8080:8080 environment: REDIS_ADDR: redis:6379 REDIS_PASSWORD: ${REDIS_PASSWORD} MYSQL_ADDR: mysql:3306 MYSQL_USER: root MYSQL_PASSWORD: ${MYSQL_ROOT_PASSWORD} depends_on: mysql: condition: service_healthy redis: condition: service_healthy volumes: redis-data: mysql-data:Redis 这段用了command给 redis-server 加了访问密码。注意redis-cli -a ${REDIS_PASSWORD} ping里的密码来自.env健康检查就是向 Redis 发一个 ping能收到 PONG 就算健康。MySQL 这段配置了 root 密码和初始数据库。MYSQL_DATABASE: blog会在容器首次初始化时自动创建名为 blog 的数据库。mysqladmin ping -h localhost -p${MYSQL_ROOT_PASSWORD}是判断 MySQL 是否就绪的标准做法注意-p和密码之间没有空格。app 服务的关键在environment里REDIS_ADDR写的是redis:6379MYSQL_ADDR写的是mysql:3306直接用服务名访问而不是用localhost。因为在 Compose 网络里localhost指的是容器自己而不是其他服务。很多新手在这卡半天原因就是没理解容器网络和宿主机网络的差异。最后用depends_on加上condition: service_healthy保证 app 只在 Redis 和 MySQL 都健康后才启动。这份配置已经把开头三条命令的所有功能覆盖了还额外解决了顺序、就绪探测、密码集中管理的问题。3.3 启动、验证与常用命令在my-blog目录下执行docker compose up -d-d表示后台运行等价于给每条docker run加-d。首次执行会先拉取镜像之后每次启动基本秒级完成。验证状态用docker compose ps这个命令会列出所有服务显示容器名、状态和端口映射。配合健康检查你会看到类似healthy的状态一眼就知道三个容器是否正常。查看日志用docker compose logs -f app-f是 follow 模式效果类似tail -f可以实时盯着某个服务的输出。后面第 4 节还会详细展开日志这块。需要进入容器排查时用docker compose exec app sh这条命令等价于docker exec -it blog-app sh但通过服务名定位容器不用记容器名。停止并删除所有容器用docker compose down注意down默认只删除容器和默认网络不会删除数据卷。这是好事Redis 和 MySQL 的数据都还在。如果你想连数据一起清掉才需要docker compose down -v这个命令很危险生产环境务必慎用。3.4 网络与数据卷的细节这份配置只把 app 的 8080 端口暴露给了宿主机Redis 和 MySQL 都没有ports映射。这是有意为之外部只能访问到应用数据库和缓存待在 Compose 内部网络里只有 app 能访问它们。相比三条docker run把 Redis、MySQL 端口全都暴露出去的写法攻击面小得多。数据持久化靠的是两个命名卷redis-data和mysql-data。它们由 Compose 管理存放路径在宿主机的 Docker 数据目录下。你不需要关心具体位置只要知道容器删了、重建了数据还在。这是生产环境最基础的数据安全保证。如果你需要把宿主机的配置文件传给容器可以用 bind mount写法是./config:/app/config冒号左边是宿主机路径右边是容器路径。开发和调试阶段 bind mount 很实用改完代码直接生效不用重新构建镜像。但生产环境我一般建议优先用命名卷迁移和备份都更省心。4. 日志、健康检查与真实排障经验到了这一节聊聊实战中最消耗时间的东西日志排查和稳定性。多容器场景下日志和健康检查不是可选项而是必需品。4.1 用 docker compose logs 管理多容器日志单容器时代docker logs 容器名凑合能用。多容器时代三个容器三个终端窗口日志互相穿插定位问题全靠肉眼。Compose 把这件事收敛成了一条命令docker compose logs -f --tail100这条命令把当前项目所有服务的日志实时打印出来并且每种服务会自动标注前缀比如app-1 |、mysql-1 |一眼就能看出日志属于谁。--tail100表示只显示每个服务最后 100 行避免刷屏。只想看某个服务时docker compose logs -f app日志里关键字太多可以先过滤再找docker compose logs app | grep ERROR这个组合在线上排障时几乎是标准操作。先 grep 出错误行再根据时间戳回到对应日志上下文定位速度比开三个窗口人工比对快得多。这里给新手一个建议如果docker compose logs app什么都看不到先确认容器是不是根本没起来。执行docker compose ps看状态如果是Exit 1再用docker compose logs app看启动错误。很多日志为空的情况本质是容器在启动阶段就崩了根本没走到写日志那一步。4.2 资源限制与健康检查的重要性Compose 支持用deploy.resources.limits给容器设置资源上限虽然它在单机docker compose up下的语义和在 Swarm 模式下略有差异但内存、CPU 限制在单机也有效services: app: deploy: resources: limits: cpus: 0.5 memory: 512M这个配置把 app 容器的 CPU 限制在半核、内存限制在 512MB。资源限制的意义在于某个容器发生内存泄漏时不会拖垮宿主机上的其他容器。我在实际项目里就遇到过 redis 缓存被大量冷数据打满、内存上涨后把整台机器拖挂的情况后来给每个容器都加了上限问题立刻隔离。健康检查前面已经提过这里再强调一下它是多容器编排的基石。容器存活不代表服务可用。MySQL 容器在启动、初始化、恢复数据期间进程虽然活着但还不能接受业务请求。如果不做健康检查下游服务会在这段时间内反复连接失败。healthcheck的四个参数很好记test探测命令在容器内执行退出码为 0 即健康。interval每隔多久探测一次。timeout单次探测超时时间。retries连续失败多少次判定为不健康。配合restart: unless-stopped容器在异常退出或探测失败后会自动重启很多偶发问题可以被系统自动消化掉不用人半夜爬起来处理。4.3 常见问题速查表整理了我在项目里最常遇到的几个问题按现象 → 原因 → 解法列成表格方便直接对号入座。问题现象可能原因解决办法app 连接 MySQL 报 Connection refusedMySQL 容器未就绪给 MySQL 加 healthcheckdepends_on加condition: service_healthyapp 连不上 Redis地址用的是 localhost容器内 localhost 指向自身改成服务名如redis:6379端口被占用容器启动失败宿主机端口已被其他进程占用修改ports映射或用lsof -i :8080查占用容器名冲突报 Conflict同名容器已存在docker rm -f name或去掉container_name改了 YAML 配置但容器不更新Compose 未强制重建使用docker compose up -d --force-recreate数据突然消失误执行了down -v卷已删除无法找回务必慎用-v容器总是自动重启,状态是 restarting启动即崩溃或者健康检查持续失败看docker compose logs修复后手动restart镜像版本不对行为诡异用了latesttag 拉到新版镜像固定镜像 tag如mysql:8.0.36这张表基本覆盖了我接触过的多数 Compose 事故。其中容器名冲突和漏了-v导致数据删除是最高频的两类前者影响心情后者影响职业生涯务必小心。4.4 真实项目里攒下的避坑清单除了速查表还有一些经验是文档里不会专门写的单独列出来供参考。不要把所有服务的端口都映射到宿主机。容器之间通过 Compose 网络可以直接访问端口只需要暴露给真正需要访问的外部调用方。数据库、缓存、消息队列这些内部服务映射出去就是给自己添堵。环境变量里的密码不要写死在 YAML 文件里。用.env文件配合${VAR}引用同时把.env加进.gitignore。如果项目用 git 管理一定不要把这个文件提交到仓库。真不小心提交了赶紧轮换密码别抱侥幸心理。镜像 tag 尽量固定不要用latest。latest的含义是最新今天能跑不代表三个月后还能跑。一旦上游发布新版本你重新部署时可能拉到不兼容的镜像服务就莫名挂了。固定 tag 虽然麻烦一点但部署结果可复现出了事能对版本负责。写配置前先用docker compose config验证一下。这个命令会解析你的 YAML 和.env把最终展开的配置打印出来。语法错误、变量没定义、端口格式不对它都会警告你。在up之前先跑一下它等于给配置做一次静态检查。还有一个关于docker run --rm的使用习惯我一般只在临时调试和跑一次性任务时用它比如docker run --rm -v $(pwd):/data alpine ls /data。凡是需要长期运行的容器一律走 Compose 并用restart策略管理绝不混用。这两种模式混在一起最容易出现容器不见了但数据还在/数据没了的认知混乱。5. Compose 不只是玩具真实项目的编排范本Compose 的能力边界经常被低估。很多人以为它只是开发环境的小工具实际上大量开源项目的生产级部署都直接使用 Compose它撑起了单机部署的半壁江山。5.1 很多开源项目都自带现成的 Compose 配置与其自己从零琢磨怎么编排不如多看看成熟项目的 Compose 文件这是学习速度最快的路径。以私有镜像仓库 Harbor 为例官方提供的部署方式就是下载一个包含docker-compose.yml的安装包改一下配置文件里的域名、密码和存储路径然后docker compose up -d。一个完整的镜像仓库服务内部包含了 Web 管理端、数据库、Redis、日志收集等多个组件全都由一套 Compose 文件编排起来。你用docker compose ps能清楚看到每个组件的状态用docker compose logs能看到统一日志。这就是多容器编排在真实生产环境中的典型形态——你不需要知道每个组件是怎么互相通信的Compose 文件已经把这一切声明好了。媒体服务器 Jellyfin 也一样官方文档直接给出 Compose 配置把媒体目录、配置目录、端口映射写得清清楚楚。你复制过去改一下路径就能跑起来。这类项目的好处是文件经过大量用户验证你可以直接把它当作最佳实践模板来学习别人是怎么处理卷挂载的、怎么配置环境的、怎么声明健康检查的。还有一类值得关注的是近年流行的各类向量数据库服务比如 Qdrant 的 standalone 模式官方示例通常就是让你用docker run -d先跑一个单机实例做验证但真正用于生产部署时官方同样提供了 Compose 文件。你会发现一个规律任何需要多个组件配合的服务最终都会收敛到 Compose 或者更高层的编排工具上。单容器用docker run足够多容器必须靠编排。5.2 Compose 的边界什么时候该上更重的平台Compose 不是万能的它的定位是单机多容器编排。当你的服务需要部署到多台机器、需要自动扩容、需要滚动更新而不中断服务时Compose 就力不从心了。那时候该考虑的是 Kubernetes 或 Docker Swarm 这类容器编排平台。我个人的判断标准很简单项目容器数量在几个到十几个跑在单台服务器或少量服务器上用 Compose 就足够如果应用需要根据流量自动伸缩、需要跨多台机器的服务发现和负载均衡再考虑上 K8s 这种重型平台。强行在小项目里上 K8s运维成本反而高过收益。最后分享一个我自己的习惯无论项目多小只要容器数量超过一个我一定会停下来先写一份docker-compose.yml哪怕只是临时调试。原因很简单手动docker run是一次性思维这次敲完下次还得重来写成 Compose 文件才是资产化配置、网络、依赖关系全部固化成代码能提交、能审查、能复现。等你三个月后再回来看这个项目那份 compose 文件比任何聊天记录和 shell 脚本都靠谱。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →