ARTICLE DETAIL

资讯详情

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

docker-compose文件从入门到实战:多容器部署的编排之道

docker-compose文件从入门到实战:多容器部署的编排之道 1. 先搞清楚docker-compose 文件到底是干什么用的我第一次意识到必须认真对待 docker-compose 文件是在一个深夜上线现场。当时要部署的是一个内部工具站Nginx 反向代理、后端服务、MySQL、Redis四个容器各司其职。我手动敲了十几条 docker run每条都是一长串参数端口、数据卷、环境变量、网络模式全挤在一起窗口一乱就漏配置漏一个就白折腾半小时。后来把整套部署状态整理成一份 docker-compose.yml一条docker compose up -d全部起来一条docker compose down全部清理再也没在启动顺序上翻过车。docker-compose 文件本质上是一个用 YAML 写成的“部署总纲”。它声明了一个多容器应用应该长什么样跑哪些镜像、每个容器开哪些端口、挂哪些数据卷、依赖谁、要不要做健康检查、掉线后怎么重启。Docker Compose 工具读到这份文件会把里面描述的所有资源一次性做好并启动起来。对于用过 docker run 的人理解成本几乎为零因为 compose 文件里的字段和 docker run 的参数是一一对应的只是换成了更好读、更可复用的结构化形式。那它到底解决了什么问题最直观的一点是告别手打命令。容器一多靠记忆输入命令根本不现实而且没法提交到 Git 里做版本管理。第二点是依赖顺序后端要等数据库起来Nginx 要等后端起来手写脚本去处理这些等待逻辑非常脆弱compose 文件里用 depends_on 和 healthcheck 一套组合拳就解决了。第三点是网络和数据卷的复用compose 会自动创建项目专属网络容器之间用服务名互相访问数据卷也可以声明成顶层资源销毁容器时数据还在。如果你正打算把一个单体应用拆成容器化部署或者团队里需要一个统一、可共享的部署描述方式那 docker-compose 文件就是你绕不开的第一张牌。下面我按文件结构、实战编写、下载安装、避坑排查四个方向把整套东西讲透。2. 逐段拆解一个 docker-compose 文件的骨架2.1 YAML 不是玄学但格式真会咬人docker-compose 文件用的是 YAML 语法玩过 Ansible、Kubernetes 清单文件、GitHub Actions 的人不会陌生。没有接触过的话只需要记住三条规则缩进用两个空格不要用 Tab键值对用key: value的写法冒号后面必须有空格数组项用-开头后面也要空格。就这三条能避开绝大多数解析报错。一个最小的 compose 文件长这样version: 3.8 services: web: image: nginx:1.25-alpine ports: - 8080:80如果你是从 Compose V1 时代过来的习惯在文件头写version: 3.8这没有错。到了 Compose V2 插件时代version 字段基本上被忽略但保留它也不会有问题还能照顾老旧环境的兼容性。我的做法是新项目直接写version: 3.8因为团队里总有老手在旧机器上维护多一行不碍事真遇到不认识该字段的新版本工具也只是忽略而已。这里要解释一个新手常有的疑问为什么是 services、volumes、networks 这几个顶层键因为 Compose 的模型认为一个应用由服务、持久化数据、自定义网络三类资源组成服务是容器数据卷给容器提供持久化存储网络让容器之间可以通信。理解了这个模型写出来的文件才不是死记硬背。2.2 services 段每个容器都有它的位置services 是整份文件的核心下面每一个一级子项就是一个容器服务。常用字段各自有讲究image指定镜像及标签比如mysql:8.0。这是最常用的方式适合直接使用官方镜像或团队私有仓库里的现成镜像。build指定 Dockerfile 的路径比如build: ./app。Compose 会在启动前先构建镜像。image和build可以同时存在构建完后就以 image 字段的名字打标签方便复用。container_name自定义容器名不写的话 Compose 会按照“项目名_服务名_序号”自动生成。restart重启策略后面专门讲。command覆盖镜像默认的启动命令。environment向容器内注入环境变量适合传入密码、配置开关。ports端口映射把宿主机端口映射到容器端口。端口映射这块我多说一句写法很灵活但容易迷惑ports: - 80:80 # 宿主机所有网卡的 80 都映射到容器 80 - 127.0.0.1:80:80 # 只允许本机访问外部无法通过该端口访问 - 8080:80 # 宿主机 8080 映射到容器 80适合本地开发错开端口冲突第三种写法在本地开发时最常用。第二种则适合部署数据库、管理后台这类不想对外暴露的服务只让本机或内网访问安全性能好不少这也是很多人在生产环境忽略的一点以为端口映射出去没事实际上把 MySQL 的 3306 直接绑到0.0.0.0上等于把数据库裸奔在外网。2.3 数据卷与网络容器可以删数据不能丢容器本身是临时资源内部写的文件在容器删除后会跟着消失。所以需要数据卷来解决持久化问题。compose 文件里大致有三种挂载方式挂载类型声明方式特点推荐场景命名卷mysql_data:/var/lib/mysql顶层声明volumesDocker 自己管理目录权限稳定可跨容器复用数据库、日志等需要长期保留的数据绑定挂载./config:/etc/nginx/conf.d宿主机目录直接挂进去改文件立即生效配置文件、开发时的源码热更新tmpfs 挂载tmpfs: /run只写入内存重启即失临时缓存、敏感信息存储命名卷是 Composer 文件里最值得优先用的方案。比如 MySQL 的数据目录我从来不用绑定挂载因为 Docker 里的 MySQL 容器启动时会对数据目录权限做校验绑定挂载的目录经常出现权限不匹配我遇到过不止一次容器起来又立刻退出日志里全是Permission denied。命名卷由 Docker 自己创建和管理权限结构天然对得上省心太多。网络方面Compose 会自动为每个项目创建一个默认网络所有服务挂在同一个网络上彼此用服务名就能互通。所以在上面的最小例子里web 和 api 两个服务即使没有显式声明网络api 里也可以直接访问http://web:80。这种“服务名即 DNS 名”的设计非常方便容器重启 IP 变了也不怕因为访问的是名字不是 IP。如果你有隔离需求可以在顶层自定义网络networks: frontend: driver: bridge backend: driver: bridge services: nginx: networks: - frontend - backend api: networks: - backend mysql: networks: - backend这样可以做到 nginx 既接前端流量又访问后端 API但 API 和 MySQL 只在 backend 内部互通外部网络无法直连。对于多网段隔离的内网部署这个模式非常实用。2.4 依赖顺序与健康检查启动不是顺序问题是就绪问题初学者最容易踩的坑就是把 depends_on 理解为“等前一个服务完全可用再启动”。实际上 depends_on 默认只等待容器进入运行状态不等待容器内部的服务真正就绪。MySQL 容器起来了但 3306 端口可能还要初始化几秒后端服务这时候去连数据库大概率拿到 connection refused。Compose V2 里提供了一个相对优雅的组合healthcheck 加 condition。services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} healthcheck: test: [CMD, mysqladmin, ping, -h, 127.0.0.1, -u, root, -p${MYSQL_ROOT_PASSWORD}] interval: 10s timeout: 5s retries: 5 api: build: ./app depends_on: mysql: condition: service_healthyhealthcheck 里的 test 命令要返回 0 才代表健康。MySQL 官方镜像自带mysqladmin ping是最省事的探测方式。interval 是多久检查一次timeout 是单次检查的超时时间retries 是连续失败几次后判定为不健康。service_healthy条件会让 Compose 等 MySQL 真正就绪后才启动 api。restart 策略同样重要它决定了容器退出后 Docker 的反应策略行为适用场景no不自动重启调试、临时任务on-failure非零退出码时重启任务型容器always无论退出原因都重启常驻服务比如 Web 服务unless-stopped手动停止外都重启服务端部署手动 stop 后不会被打扰生产环境里的常驻服务我基本无脑选restart: unless-stopped。它和always的差别在于如果管理员手动 docker compose stop 了某个服务unless-stopped会在下次启动或重启 Docker 后尊重手动停止的状态不强行拉起这对日常维护真的很友好。3. 实战一份可以直接抄的 docker-compose 文件3.1 场景设定一个内部任务管理系统光讲字段和概念有点干我拿一个实际项目来走一遍完整流程。假设要给团队搭一个内部任务管理系统架构是 Nginx 做反向代理后端是一个 Spring Boot 或 Node.js 应用数据落在 MySQL缓存用 Redis。目录结构如下task-manager/ ├── docker-compose.yml ├── .env.example ├── .env # 不入库只在本机存在 ├── app/ # 后端源码与 Dockerfile │ └── Dockerfile └── nginx/ └── conf.d/ └── default.conf这套结构的关键点在于代码只留在宿主机环境变量从 .env 注入配置文件用绑定挂载进去数据全部交给命名卷。抛弃了容器内编辑、宿主机直接改容器文件这些坏习惯才能保证高可用。3.2 完整文件与逐段解读下面是完整的 docker-compose.yml我按生产可用的标准写的version: 3.8 services: mysql: image: mysql:8.0 container_name: task-mysql restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} MYSQL_DATABASE: ${MYSQL_DATABASE} MYSQL_USER: ${MYSQL_USER} MYSQL_PASSWORD: ${MYSQL_PASSWORD} volumes: - mysql_data:/var/lib/mysql healthcheck: test: [CMD, mysqladmin, ping, -h, 127.0.0.1, -u, root, -p${MYSQL_ROOT_PASSWORD}] interval: 10s timeout: 5s retries: 5 networks: - backend redis: image: redis:7-alpine container_name: task-redis restart: unless-stopped command: [redis-server, --appendonly, yes] volumes: - redis_data:/data networks: - backend app: build: ./app image: task-app:latest restart: unless-stopped container_name: task-app environment: SPRING_PROFILES_ACTIVE: prod DB_HOST: mysql DB_PORT: 3306 DB_NAME: ${MYSQL_DATABASE} DB_USER: ${MYSQL_USER} DB_PASSWORD: ${MYSQL_PASSWORD} REDIS_HOST: redis REDIS_PORT: 6379 depends_on: mysql: condition: service_healthy redis: condition: service_started networks: - backend nginx: image: nginx:1.25-alpine container_name: task-nginx restart: unless-stopped ports: - 80:80 volumes: - ./nginx/conf.d:/etc/nginx/conf.d:ro depends_on: - app networks: - frontend - backend networks: frontend: driver: bridge backend: driver: bridge volumes: mysql_data: redis_data:逐个服务说一下我为什么这么写。mysql 服务没有暴露宿主机端口因为它只服务内部后端应用通过服务名 mysql 访问不需要让外部碰 3306。如果需要命令行临时连库可以用docker compose exec mysql mysql -u root -p进入容器操作安全得多。redis 同理不对外监听只在内网服务。app 服务做了构建build: ./app会先构建镜像再用 image 字段打上task-app:latest的标签这样下次只改代码不重新 build 的话可以直接跑旧镜像做回滚。nginx 是唯一对外暴露端口的服务映射宿主机 80。local 配置里我建议你调整成127.0.0.1:8080:80这样外部访问不到只能通过本机端口转发。配置目录用:ro做了只读挂载避免容器里产生修改配置的权限问题也防止宿主机文件被容器进程误写。3.3 .env 文件把密码从版本库里请出去compose 文件里出现了${MYSQL_ROOT_PASSWORD}这样的写法这就是 Compose 的变量替换机制。Compose 在解析文件时会读取同目录下的 .env 文件把对应的变量值填进来。因此 .env 文件的格式非常简单MYSQL_ROOT_PASSWORDChangeMeRoot123 MYSQL_DATABASEtask_manager MYSQL_USERtask_app MYSQL_PASSWORDChangeMeApp456两个细节很重要。第一.env一定不要提交到 Git要在 .gitignore 里明确排除仓库里放一个.env.example里面写变量名和示例值队友拉代码后复制成.env再自己填密码。第二不要在 compose 文件里同时出现明文密码和${VAR}两者选一种我强烈建议用变量替换因为开发环境、测试环境、生产环境密码不同混在一起早晚要出事。还有一个容易搞混的点environment里的变量是传给容器内部的.env文件里的变量是给 Compose 解析用的。比如DB_HOST: mysql传给后端容器后端代码读DB_HOST就能拿到数据库地址而${MYSQL_PASSWORD}则是在宿主机上先被 Compose 替换成真实密码再传入容器。理解这层关系你就不会再问“为什么容器里看不到 .env 的值”了。3.4 启动、验证与日常命令写完文件先不急着 up先跑一遍校验docker compose config这个命令会把最终解析结果打印出来所有变量替换后的真实值一目了然。如果 .env 里漏了变量它这里会直接报错提示是部署前最值得养成的习惯。确认无误后再启动# 构建并后台启动全部服务 docker compose up -d --build # 查看服务状态 docker compose ps # 跟踪某个服务的日志 docker compose logs -f app # 进入容器内操作 docker compose exec app sh # 停止全部 docker compose down这里必须提醒一句docker compose down默认不会删除命名卷数据还在。但加上-v参数会把所有命名卷一并清掉down -v之后 MySQL 数据就真的没了。日常调试可以放心用down生产环节千万别手滑带-v除非你确定要重置全部数据。4. docker-compose 下载与安装V1 和 V2 别用混了4.1 Compose V1老牌命令行工具如果你在维护旧项目或者团队脚本里用的是docker-compose这个带横杠的命令恭喜你还在用 Compose V1。它本质上是一个独立二进制从 GitHub 官方 releases 下载即可# 下载对应平台版本到 /usr/local/binuname 自动识别系统和架构 curl -L https://github.com/docker/compose/releases/download/v2.24.7/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose # 给可执行权限 chmod x /usr/local/bin/docker-compose # 验证版本 docker-compose --version下载下来的是一整个可执行文件不需要额外依赖放到/usr/local/bin后就能全局使用。校验是个好习惯官方 releases 页面会同步给出 sha256 校验值下载完后可以用sha256sum /usr/local/bin/docker-compose比对一下防止下载文件不完整。我第一次装的时候没等下载完就执行结果报错Exec format error就是文件残缺导致的白排查了半天。4.2 Compose V2Docker 现在的默认形态Docker 官方从 2023 年起逐步把 Compose V2 做成了 Docker CLI 插件也就是说不再需要独立的docker-compose命令而是直接集成进docker compose。安装方式也很清晰# 创建 CLI 插件目录 mkdir -p /usr/local/lib/docker/cli-plugins/ # 下载 v2 插件到该目录 curl -SL https://github.com/docker/compose/releases/download/v2.24.7/docker-compose-linux-x86_64 -o /usr/local/lib/docker/cli-plugins/docker-compose # 加权限 chmod x /usr/local/lib/docker/cli-plugins/docker-compose # 验证 docker compose versionV1 和 V2 的核心区别在于V1 是独立二进制命令是docker-composeV2 是插件形态命令是docker compose且参数和配置文件兼容性更好部分命令输出更友好。在你自己的机器上两者可以共存但项目级文档里我建议统一只写一种命令否则团队里有人用 V1 有人用 V2部署行为容易出差异。新项目一律用docker compose老项目如果脚本已经稳定跑着别急着切先并行用一段时间再迁移。4.3 包管理器安装适合批量初始化的节点如果不想手动下载二进制主流的包管理器也能直接装。Debian/Ubuntu 上如果你已经配置了 Docker 官方 apt 仓库一条命令就能装插件sudo apt update sudo apt install docker-compose-pluginRedHat/CentOS 等用 dnf 的发行版类似sudo dnf install docker-compose-pluginmacOS 上则可以直接brew install docker-compose用包管理器装的优点在于卸载方便、升级跟着系统走适合一次性批量初始化多台测试机。缺点是版本往往比官方 releases 滞后新特性要等仓库维护者更新。我个人在开发机上的习惯是本机用官方二进制方式装最新版服务器上则用包管理器装稳定版两边职责分开互不干扰。4.4 装完一定要验证这些坑我全踩过装完之后第一件事就是验证版本命令期望输出特征验证目的docker compose version能打印出Docker Compose version v2.x.x确认 V2 插件生效docker-compose --version能打印出版本号确认 V1 二进制可用docker infoPlugins 部分能看到 compose确认 Docker 识别到插件常见问题是二进制明明下载了执行docker compose却提示找不到命令。大概率是插件没放到 Docker 扫描的目录默认路径是~/.docker/cli-plugins和/usr/local/lib/docker/cli-plugins放错了位置 Docker 根本看不见。还有一种是权限不对文件没加chmod x执行时直接报 permission denied这种一般发生在用普通用户而不是 root 下载的场景加完权限基本能解决。5. 高频坑位与排查速查5.1 depends_on 没等 MySQL 初始化完这是我强调过但值得再次强调的坑。现象后端容器启动后立刻失败日志里全是UnknownHostException或connection refused。原因就是 depends_on 只等待容器启动不等待服务就绪。解决方式就是上文说的 healthcheck 加condition: service_healthy。如果你用的是 Compose V2 但 condition 语法不生效检查一下 compose 版本是否太老V1 的旧版本对这个特性的支持很有限。5.2 数据卷权限导致容器反复重启另一个高发问题MySQL 或 PostgreSQL 容器启动几秒后退出日志显示无法写入数据目录。如果用绑定挂载把宿主机目录挂进数据库容器宿主机目录的属主和容器内用户不一致就会触发权限校验失败。这类问题用命名卷基本能根治。如果必须用绑定挂载那就确认好目录属主比如 MySQL 镜像里数据目录属主是 uid 999可以用chown -R 999:999 ./mysql_data把宿主机目录所有权调整过来。5.3 端口被占用容器死活起不来现象是docker compose up -d时报告port is already allocated或bind: address already in use。排查思路很简单sudo lsof -i :8080看哪个进程占了端口或者docker ps -a看看是不是旧容器还挂着没清理干净。如果是旧容器残留docker rm -f删掉即可如果是系统里其他进程占用改 compose 文件里的宿主机端口映射不要和现有服务硬碰。5.4 Compose 版本升级后的兼容性问题升级 Docker 或 Compose 后可能遇到unsupported config option这类错误。常见原因是老 compose 文件里用了旧版本专属字段比如version: 2下的某些写法在 V2 里已经不支持。排查时先执行docker compose config它会告诉你具体哪个字段有问题。没有特殊需求的话建议直接把 compose 文件统一到 3.8 语法规范字段精简、兼容性也最好。5.5 环境变量没生效配置全是空的现象容器里打印环境变量发现是空字符串或者 Compose 解析时报variable is not set。大概率是 .env 文件命名或位置不对。Compose 默认读取的是当前目录下的.env文件不是 compose 文件所在目录容易搞混。还有一种是 .env 里用了export VARxx的写法Compose 的解析器不认 export直接写VARxx才能被识别。再一个隐蔽坑environment 里的值如果加了双引号Compose 可能把引号也当作值传进去所以你看到容器里值是password而不是password就是引号没处理干净。5.6 问题排查速查表现象可能原因排查命令与解决方向容器启动后立即退出启动命令报错、入口脚本缺失、权限不足docker compose logs 服务名定位报错服务间访问不通网络配置错误、服务名拼写不一致docker compose exec 服务名 ping 目标服务名端口映射不生效宿主机端口被占用、防火墙拦截lsof -i :端口、检查防火墙规则数据一重启就丢没挂卷或挂的是 tmpfs检查 volumes 配置改为命名卷或绑定挂载image 拉取超时网络波动、镜像仓库响应慢重试、检查 DNS、更新 Docker 版本我在实际项目里养成的习惯是每次改动 compose 文件先docker compose config校验再docker compose up -d --build最后docker compose logs --tail100快速扫一眼启动日志。这套流程跑顺之后部署多容器应用的思考方式会发生很大变化——你不再盯着一个个容器去救火而是把整份 docker-compose 文件当成可以随时推倒重来的环境蓝图甚至可以直接把同一份文件复制到测试环境跑一遍验证。这点体会只有真正用 compose 文件接管过一整个项目的人才能懂。
返回列表