ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用Docker Compose告别手动docker run:多容器编排实战指南

用Docker Compose告别手动docker run:多容器编排实战指南 每次看到有人在那敲好几遍docker run再手动建网络、再手动关联容器我都想说一句这不是勤奋这是给自己挖坑。今天这篇就围绕Docker Compose多容器编排展开目标是帮你把手动敲三遍docker run的日常彻底换掉。文章会从“为什么手动方式不可靠”讲起再到安装、语法、生产环境细节和故障排查适合刚接触容器编排的开发者也适合已经用docker run跑了好一阵子、想理顺项目结构的人。如果你现在本地跑一个前后端项目大概长这样第一遍启动数据库容器第二遍启动后端接口容器第三遍启动前端 Nginx 容器还要记得加--network、传环境变量、设置重启策略。一旦哪个参数忘了写前面的容器可能白启动整个排查过程能让人原地暴躁。这篇文章讲清楚怎么用一份compose.yml把这些事情全部管起来顺便把从老方案迁移过去时容易踩的坑一起填上。1. 为什么“三遍 docker run”会让你后期加倍付账1.1 手动命令的最大问题不是“手累”是状态不可复制单看一次启动docker run并没什么不好。问题在于当这个项目有多个容器、多个环境要求时手动命令就变成了一堆“潜规则”。数据库要指定端口映射后端要读数据库的地址前端要反向代理到后端端口三个容器还要处于同一个自定义网络里才能用服务名互相访问。这些信息全都散落在操作记录里可能在某条历史命令里可能在某个同事的聊天记录里也可能就只在你脑子里。我自己有过一次印象很深的经历本地跑得挺好换一台新电脑打算按记忆把环境重新搭起来。结果docker run敲到后端容器时忘了加-e DB_HOSTmysql后端一直报连接数据库失败。排查了半天发现不是代码问题就是少传了一个环境变量。这类问题手动模式下特别容易犯因为容器的参数没有统一存档每次重建都是一次“对着回忆抄命令”的过程。手动模式第二个隐患是启动顺序。数据库还没起来后端容器就先启动了应用进程初始化时连不上数据库直接崩溃退出退出了又没人管后端服务就一直不在线。你可能会说“那我把数据库先启动再启动后端就行了”没错但这句话本身就是一条需要人为遵守的规则。只要哪天顺序敲反了或者某个容器启动时间变慢了整个环境就处于一种“看起来都执行了、实际服务不可用”的模糊状态。1.2 一个具体槽点漏掉 --network 导致容器之间不通手动模式最常见的翻车现场是容器间网络不通。比如数据库容器和后端容器都启动了后端在代码里把数据库地址配成了localhost后端容器内部访问的其实是容器自己不是宿主机上的数据库。要解决这个问题要么让端口暴露到宿主机再用宿主机 IP要么把两个容器放进同一个自定义网络让后端通过服务名访问数据库。后一种方案明显更干净但你需要手动执行网络创建还要在每个docker run里带上--network my-app-net。命令一多总有漏网之鱼。漏掉之后的表现很直接后端日志里报connect ECONNREFUSED你检查数据库容器明明活着端口也是通的就是代码里连不上。这个时候你才意识到少写了一个网络参数。而这还只是多容器协作的最基础场景换成消息队列、Redis、Nginx、多个后端副本手动命令的维护成本会成倍上升。1.3 单容器设计思路与一组服务编排的差异docker run本身是为“启动一个容器”设计的不是为一个完整服务组设计的。这就像是单个工具箱和一条完整流水线的区别工具箱适合临时修个东西流水线则需要把每个工序的依赖、顺序、参数都固化下来。容器编排要解决的问题正是“多个容器如何作为一个整体被定义、启动、更新和清理”。Docker Compose解决的就是这个层面的问题把一组容器的镜像、端口、环境变量、网络、卷、依赖关系全部写进一个文件一条命令拉起一条命令停掉环境描述变成可版本管理的资产。这也是为什么很多开源项目现在都直接用docker-compose.yml交付部署说明而不是贴一串docker run命令。因为一份文件本身就带有“文档即配置”的效果别人拿到仓库后不用问你“到底要先跑哪条命令”直接docker compose up -d就能复现环境。2. 先把工具装对Compose 安装与 daemon 连接报错2.1 搞清楚你用的是 docker-compose 还是 docker compose开始写配置之前先确认一件容易混淆的事现在有两个 Compose 入口老式的是独立命令docker-compose新式的是 Docker 命令行插件docker compose中间没有横杠。大多数新版 Docker 桌面版、Linux 上的 docker-ce 包都默认带插件你直接在终端敲docker compose version就能验证。这两个入口的底层实现经历了从 Python 脚本到 Go 二进制的演进新版本语法更接近但网上教程鱼龙混杂老教程可能让你下载独立二进制新教程让你装插件。所以我给你的建议是新环境优先用docker compose插件形式它的更新跟随 Docker 主程序命令习惯也更统一。如果服务器上只能装老式docker-compose也能用但注意别把两个混着写进同一个脚本免得运维同事接手时一脸问号。验证是否安装成功的标准动作是执行docker compose version如果输出类似Docker Compose version v2.x.x说明插件可用。如果提示command not found进入下一步安装。2.2 Linux 服务器安装CentOS 与 Ubuntu 的差异在 CentOS 系列服务器上如果 docker 是通过yum install docker-ce docker-ce-cli containerd.io安装的通常可以直接补装docker-compose-plugin包yum install -y docker-compose-pluginUbuntu / Debian 系则用 aptapt-get update apt-get install -y docker-compose-plugin装完之后再次敲docker compose version确认。如果包管理源里的版本偏旧或者你想指定版本最稳妥的办法是从 Docker 官方 GitHub 发布页面下载二进制文件放到~/.docker/cli-plugins/目录并命名为docker-compose注意需要加上执行权限mkdir -p ~/.docker/cli-plugins curl -SL https://github.com/docker/compose/releases/latest/download/docker-compose-linux-x86_64 -o ~/.docker/cli-plugins/docker-compose chmod x ~/.docker/cli-plugins/docker-compose这里有个经常被忽略的点插件路径不能随意放。Docker 默认扫描~/.docker/cli-plugins和/usr/local/lib/docker/cli-plugins之类的系统目录放错位置docker compose还是提示找不到命令。放在用户目录下的插件只对当前用户有效如果部署脚本会切到其他用户执行建议放到系统目录。2.3 最经典的报错cannot connect to the docker daemon at unix:///var/run/docker.sock安装完 Compose 之后很多人会立刻碰到这个经典报错Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?这个报错几乎每天都会出现在各种群里。我先说结论它和 Compose 本身没关系是 Docker 守护进程没起来或者当前用户没有访问 Docker 的权限。按下面的顺序排查通常都能解决。第一步先用docker version看客户端能不能连上服务端。注意这个命令会输出两部分Client 和 Server。如果你只看到 Client 信息Server 部分是报错说明守护进程确实没在运行。此时执行systemctl status docker如果状态是 inactive 或 failed就启动它systemctl start docker systemctl enable docker第二步如果服务端正常但普通用户执行报错而加上sudo之后正常那问题出在用户权限。Docker 的 socket 文件/var/run/docker.sock默认属于 root 组和 docker 组普通用户需要加入 docker 组才能直接访问sudo usermod -aG docker $USER newgrp docker注意newgrp docker是让当前终端会话立即生效如果重新登录直接生效。改完组之后再用普通用户执行docker ps确认。第三步检查是否有DOCKER_HOST环境变量干扰。假如你在.bashrc或系统配置里设置了DOCKER_HOST指向一个代理地址或远程地址本地docker compose也会跟着去连那个地址自然连不上本机 socket。确认方法echo $DOCKER_HOST如果有值可以先unset DOCKER_HOST再重试。我排过不少类似问题其中一大半都不是什么深奥原因就是服务没启动或者用户没加入 docker 组。先把这两步做完再考虑更底层的网络、SELinux 的问题。3. 一份 compose.yml 替换三遍命令语法映射与最小案例3.1 先看“三遍 docker run”和一份 compose 的直观对比假设我们要启动一个包含 Nginx、后端 Node 服务和 Redis 的最小项目手动版本大致长这样docker network create myapp docker run -d --name redis \ --network myapp \ -p 6379:6379 \ redis:7-alpine docker run -d --name backend \ --network myapp \ -p 3000:3000 \ -e REDIS_HOSTredis \ -e DB_HOSTmysql \ myapp-backend:latest docker run -d --name frontend \ --network myapp \ -p 8080:80 \ -v ./html:/usr/share/nginx/html:ro \ nginx:1.27-alpine写进compose.yml同一套东西就变成services: redis: image: redis:7-alpine restart: always networks: [myapp] backend: image: myapp-backend:latest restart: always environment: REDIS_HOST: redis DB_HOST: mysql depends_on: - redis networks: [myapp] frontend: image: nginx:1.27-alpine restart: always ports: - 8080:80 volumes: - ./html:/usr/share/nginx/html:ro depends_on: - backend networks: [myapp] networks: myapp:上面的 yaml 已经把网络定义抽出来了networks: [myapp]表示所有服务都在同一个自定义网络里服务之间直接通过服务名访问。启动时执行docker compose up -d停掉整个环境docker compose down手动模式下漏掉--network导致的容器互通问题在这里基本不存在了。而且这份文件可以放进 Git 仓库换机器、换同事接手只要项目配置一致拉起的就是一套完全相同的环境。3.2 docker run 常用选项与 compose 字段的映射关系学习 Compose 最快的方式不是从头背字段而是把你已有的docker run命令逐项翻译成 yaml。下面这张对照表是我实际开发中用到最多的映射关系docker run 选项compose 字段说明-d不需要配合up -d不在 yaml 里写守护标志部署时用-d控制--namecontainer_name不推荐滥用服务名本身就够用-p 8080:80ports: [8080:80]多个端口就写多行-v /host:/containervolumes: [/host:/container]相对路径基于 compose 文件目录-e KEYvalueenvironment: KEY: value也从env_file读文件--network mynetnetworks: [mynet]网络在文件底部声明--restart alwaysrestart: always生产环境常用--link redis:redisdepends_on或直接用服务名--link已过时不推荐--env-file .envenv_file: .env适合存敏感配置--volume-fromvolumes_from很少用知道概念即可这里我想提醒一点很多人会习惯性地写container_name但对于多副本、扩展的场景固定容器名反而会造成冲突。Compose 默认用“项目名服务名序号”的方式命名容器比如myapp_backend_1好处是同一套 compose 文件可以同时跑多份只要项目名不同就行。所以除非有特殊脚本依赖固定的容器名否则不建议手动指定container_name。3.3 depends_on 的真实作用与局限depends_on是最容易被误会的字段。它只控制容器的启动顺序不保证依赖的容器已经“真正可用”。举个例子后端依赖 Redis你写了depends_on: [redis]Compose 会先启动 Redis 容器然后立刻启动后端容器。但如果 Redis 容器只是进程起来了还没完成内部初始化后端连接 Redis 一样会失败。要在 Compose 里实现真正的“等依赖就绪”需要给依赖服务配上健康检查再用长格式的depends_onservices: redis: image: redis:7-alpine healthcheck: test: [CMD, redis-cli, ping] interval: 5s timeout: 3s retries: 10 backend: image: myapp-backend:latest depends_on: redis: condition: service_healthycondition: service_healthy表示要等到 Redis 的健康检查通过才启动后端。这种写法才是生产环境里需要的行为单纯写depends_on: [redis]只是保证了先后顺序并不能保证可用性。4. 生产环境 Redis 编排持久化、健康检查与依赖顺序4.1 为什么中间件容器要额外关注持久化很多人刚开始用 Compose 跑 Redis、MySQL 这类存储型服务时容易犯一个错误容器起来能存取数据就觉得完事了完全不考虑容器删除后的数据去向。容器的文件系统是临时的一旦docker compose down加上-v参数匿名卷也会被清理数据说没就没。生产环境的 Redis 即使只当缓存用往往也期望重启后还能保留一部分恢复数据所以持久化必须显式配置。Redis 的持久化有两种方式RDB 快照和 AOF 日志。Compose 文件里不需要写具体的持久化策略参数但要保证数据目录挂到了宿主机。Redis 官方镜像里容器内数据目录是/data所以最基础的挂载长这样services: redis: image: redis:7-alpine restart: always command: [redis-server, --appendonly, yes] volumes: - redis-data:/data ports: - 6379:6379 volumes: redis-data:redis-data是命名卷数据由 Docker 管理不会因为容器重建就消失。生产环境如果 Redis 只允许内网访问我建议不要暴露6379到宿主机甚至不暴露端口只让其他容器通过服务名访问。上面例子里的ports属于开发调试方便生产环境可以去掉。另外给 Redis 设置密码是底线操作。即使容器只在内部网络里被访问也不建议裸奔。可以通过 command 传参或者配置文件方式设置services: redis: image: redis:7-alpine command: [redis-server, --appendonly, yes, --requirepass, your-strong-password]出于安全习惯密码不要直接写死在 compose 文件里用环境变量引用更合适等下在 .env 部分会讲。4.2 给 Redis 加健康检查让后端不要抢跑生产环境中Redis 本身故障时后端服务最好能快速感知而不是无限等待。Compose 里给 Redis 配置健康检查既是给depends_on提供判断依据也能让运维通过docker compose ps一眼看到服务健康状态。Redis 官方镜像自带了redis-cli所以健康检查最常写的是services: redis: image: redis:7-alpine command: [redis-server, --appendonly, yes] healthcheck: test: [CMD, redis-cli, ping] interval: 10s timeout: 5s retries: 5 start_period: 5sredis-cli ping正常会返回PONG退出码为 0Docker 认为容器健康。设置start_period是给容器留出初始化时间这段时间内的健康检查失败不会计入retries。后端业务服务在依赖 Redis 时就可以写成services: backend: image: myapp-backend:latest environment: REDIS_HOST: redis REDIS_PORT: 6379 depends_on: redis: condition: service_healthy这样每次docker compose up -d都会等 Redis 真正可用了再启动后端启动完很少再看到应用日志里密集的“Redis连接失败”。4.3 资源限制与日志配置生产形态和本地开发不一样生产环境和本地开发的另一个差别在于资源管控。本地跑 Redis 不需要限制 CPU 内存生产服务器上如果不设限某个容器异常占用内存会波及同宿主机的其他容器。Compose 支持deploy.resources字段但在非 Swarm 模式下docker compose up对deploy.resources.limits的解析方式需要依赖 Docker 主进程的支持。更通用的做法是运行时加--memory或者直接写在 compose 里尝试启用services: redis: image: redis:7-alpine mem_limit: 512m cpus: 0.5mem_limit和cpus是 Compose 规范里比较通用的字段对单机部署来说比deploy.resources更直观。用内存限制时要注意Redis 的maxmemory参数要低于容器内存限制否则容器进程可能在触发 OOM 后被系统杀掉。日志方面容器日志默认会写到 JSON 文件中时间久了会占磁盘。生产服务器上最好配置 log rotation两种方式第一种是在/etc/docker/daemon.json里做全局配置第二种是每个服务单独配置logging。局部配置示例如下services: backend: image: myapp-backend:latest logging: driver: json-file options: max-size: 20m max-file: 5这样日志文件单份不超过 20M最多保留 5 份。对大多数业务来说这个配置就算够用了。5. 常见故障排查实战从端口占用到容器反复退出5.1 端口占用排查宿主机端口被占时 Compose 的表现用docker compose up -d时端口已经被占的情况非常典型。报错大概是Error response from daemon: driver failed programming external connectivity on endpoint myapp_redis_1: Bind for 0.0.0.0:6379 failed: port is already allocated看到这个报错先不要急着删容器。用下面的命令确认是哪个进程占用了端口ss -lntp | grep 6379 lsof -i :6379常见原因有几种宿主机的 Redis 服务没停、另一个容器占用了相同端口、Nginx 之类的服务占用了 80 端口。解决方式也简单要么停掉冲突进程要么把 compose 文件里的宿主机端口改掉。需要注意的是容器内部的端口不能随便改比如 Redis 默认监听 6379对应的是容器内端口宿主机端口只是映射入口改成 16379 完全没问题。5.2 容器启动后立即退出先看日志再猜原因我自己排障时最忌讳上来就怀疑 compose 写错了。先执行docker compose ps看状态如果发现容器状态是Exited立刻看日志docker compose logs 服务名日志通常能直接给出线索比如Error: /bin/sh: 1: mysql: not found说明镜像里没有你 command 里写的那个程序再比如Error response from daemon: OCI runtime create failed: unable to start container process: exec: bash: executable file not found in $PATH说明基础镜像可能用的是精简版根本没有 bash只有 sh。还有一个常见原因是挂载目录和容器内启动脚本冲突。某个服务的 Dockerfile 里把初始数据放在了镜像内的目录同时 compose 又用宿主机目录覆盖了这个路径容器启动时读不到预期文件直接退出。这种情况日志里一般会有明确的文件找不到错误重点关注挂载路径。5.3 docker compose config配置校验的保命命令不管你是刚写完 compose 文件还是从网上复制了一段配置我都建议先执行docker compose config这条命令会做几件事校验 yaml 语法、把环境变量替换成实际值、展示最终生效的完整配置。如果 yaml 里有缩进错误、字段拼写错误它会在执行up之前就暴露出来。比如你少写了一个空格docker compose up会直接报解析错误而docker compose config也会报同样的错但因为这条命令不会启动任何服务用来调试更安全。另外新版本的 Compose 已经不再推荐写version: 3.8这一行了因为 Compose 规范本身已经独立演进version字段只对老工具的兼容有意义。看到网上老配置里带version可以顺手删掉不影响运行。5.4 故障排查表按症状快速定位为了让你以后遇到问题时不慌我把最常遇到的几种情况整理成一张表方便对照排查症状可能原因先试命令解决方向Cannot connect to the Docker daemonDocker 服务未启动 / 用户权限不足systemctl status docker、docker ps启动服务、加入 docker 组端口已被占用宿主机端口冲突ss -lntp | grep 端口调整宿主机端口或停掉占用进程容器启动后立即退出启动命令错误 / 环境变量缺失docker compose logs 服务名看日志定位修正 command 或 environment服务间无法通过服务名访问网络配置不一致 / 没在同一个网络docker compose ps确认所有服务都声明了同一个 networks数据容器重启后数据丢失未挂载持久化卷docker volume ls添加 volumes 挂载或命名卷compose 配置语法报错yaml 缩进 / 字段拼写错误docker compose config用该命令定位精确行号这张表覆盖了新手阶段 90% 的问题。遇到别的问题时也记住一个思路先docker compose config确认配置再docker compose ps看状态最后docker compose logs看日志按这个链路走大多数问题都能在一个小时之内解决。6. 把老项目从 docker run 迁移到 Compose 的经验总结6.1 迁移第一步用 docker inspect 把你现有容器“翻译”成配置如果你手头已经有一堆用docker run跑着的容器先别急着手动删掉重写最稳妥的路径是把现有容器参数导出来作为 compose 文件编写的依据。对某个运行中的容器执行docker inspect 容器名输出是 JSON 格式重点看这几部分Config.Env是环境变量HostConfig.PortBindings是端口映射HostConfig.Binds是卷挂载HostConfig.RestartPolicy是重启策略NetworkSettings.Networks是所属网络。把这些信息对应到 compose 字段上基本上就能写出一份可用的 yaml。我的建议是在迁移期间不要马上把原容器删掉可以先docker stop停掉旧容器然后用docker compose up -d启动新环境等新环境验证通过后再清理旧容器。这样万一 compose 文件里有遗漏还能随时把旧容器启动起来继续用不用承担“一锤子买卖”的风险。6.2 项目化组织目录、.env 与 README迁移到 Compose 之后我会建议把整个环境按项目维度组织成一个目录而不是把每个容器的命令散落在不同的 shell 脚本里。常见结构如下myapp/ ├── compose.yml ├── .env ├── .env.example ├── data/ │ ├── redis/ │ └── mysql/ └── README.mdcompose.yml是核心编排文件.env保存每个环境的差异化变量.env.example是提交到 Git 的模板data目录用来挂载持久化数据README.md写下启动命令和注意事项。团队协作时新同事只需要把.env.example复制成.env改几个关键值然后执行docker compose up -d就能把整套环境拉起来。env_file的使用方式很简单在服务里声明services: backend: image: myapp-backend:latest env_file: - .env这样 MySQL 密码、Redis 密码这类敏感信息就不需要写死在 compose 文件里了。记住一个原则compose.yml尽量写成“与环境无关”的模板所有按环境变化的值都从.env或 shell 环境变量读取。6.3 常用 Compose 命令的搭配思路迁移完成后日常命令也会比手动docker run干净很多。我最常用的组合是docker compose up -d首次启动或配置变更后同步容器状态-d表示后台运行。docker compose logs -f backend跟踪某个服务日志看启动是否正常、有没有报错。docker compose exec backend sh进入容器内部调试比docker exec多了一层服务名映射不用记容器完整名称。docker compose down停止并移除整个项目的容器和网络默认保留卷数据。如果确认要连数据也清掉才加-v。这个参数要谨慎生产环境误操作一次就是事故。新版本的docker compose有个很好的特性修改 compose 文件后不需要先down再up直接执行docker compose up -d就能自动识别配置变化重建需要变化的容器保持其余容器不动。这在迭代频繁的开发阶段能省下大量无意义的全量重启时间。6.4 一点迁移体会我在多个项目里经历过从docker run脚本到 Compose 的切换最大的感受是这不只是把命令换了一种写法而是把“环境如何搭建”这件事从个人经验变成了项目资产。以前换电脑或者带新人总要讲一遍“先启动数据库再启动后端前端最后起网络记得加”现在只需要说一句“看 READMEdocker compose up -d”。当然也不是所有场景都要 Compose。只是临时跑一个一次性容器做验证docker run依然是最快的选择。但只要你发现自己开始给同一个项目敲第二条docker run或者意识到多个容器之间还要建网络、设置环境变量、保持重启策略的时候那就到了该写compose.yml的时机了。多花十分钟把这个配置沉淀下来后面省下的是几小时甚至一天的排障时间。
返回列表