资讯动态

Docker Compose实战:从docker run到微服务编排的完整指南

发布时间:2026/10/16 12:51:54 来源:尧图企业网站定制
Docker Compose 这东西说实话刚开始接触 Docker 的时候我根本看不上的。觉得不就是写个 YAML 文件把 docker run 的参数搬进去嘛能有多神奇。直到后来真的开始折腾微服务架构一条一条 docker run 命令敲到头大才发现 Compose 其实才是 Docker 生态里最容易被低估的宝贝。它的定位很明确把多个 Docker 容器的定义、依赖关系、网络通信、存储挂载统一收进一个声明式的 YAML 文件里。你不需要记住复杂的 docker run 参数顺序不需要手工创建网络不需要担心先启动哪个容器一条 docker-compose up 就能把整套环境拉起来。这篇内容就围绕这个核心展开我会从基础概念讲起然后完整拆解一个接近真实的微服务项目包含前端、后端网关、业务服务、数据库、缓存这些角色让你看完能直接照猫画虎落地自己的项目。1. 为什么微服务项目越来越离不开 Compose1.1 从一条条 docker run 到单一编排文件早期我没用 Compose 的时候想在本地跑一套环境得先手动 docker network create再算好每个容器的端口映射还要确保数据库容器先起来、业务容器后起来不然业务容器一启动就疯狂重试连不上数据库。这些工作偶尔做一次还好但团队成员一多、环境一复杂问题立刻发酵。新同事拉完代码第一件事就是对着文档敲十分钟命令还经常因为漏了一个环境变量让整个后端挂掉。Compose 解决的不是某一个命令的问题而是整个容器生命周期的管理问题。它把所有容器的定义集中到一个 docker-compose.yml 里每个容器抽象成一个服务每个服务下面写镜像、端口、环境变量、卷、网络、依赖关系而且支持 YAML 的语法特性。这意味着环境定义可以被版本化、被评审、被复用团队里任何一个人拉下来代码只需要 docker compose up就能得到一个跟生产环境高度一致的本地开发环境。1.2 本地开发、测试、演示三合一的场景价值很多人不自觉地会把 Compose 归类为“开发工具”但它的使用场景其实很宽泛。本地开发确实是最常见的使用场景尤其是需要同时启动多个有依赖关系的服务时Compose 能让你只用一条命令进入工作状态。而在 CI/CD 流水线里测试环境想要快速拉起一套隔离的依赖服务池用 Compose 也能省掉大量脚本维护成本。我记得有段时间团队要做一个对外演示环境需要在不同分支上分别展示新老功能直接用 Compose 配合不同的 YAML 覆盖文件就搞定了演示完一键销毁干净利落。1.3 Compose 版本与命令行工具的关系这里要插一句版本问题因为很多人容易搞混。Compose 早期是 Python 写的独立工具通过 docker-compose 命令调用后来 Docker 把它重构成了 Go 语言插件集成进 Docker CLI使用 docker compose 子命令。目前主流环境基本都切换到后者了而且 Compose 规范本身已经独立演进官方推出了 Compose Specification不再跟 Docker Engine 的版本绑定。如果你用的是新版 Docker Desktop 或者 Linux 发行版自带的 Docker 引擎直接输入 docker compose version 看看有没有结果就知道了。这个变化的意义在于不同实现比如有些云平台也支持 Compose 规范只要遵循同一份规范配置就能通用这是写 YAML 文件时最大的信心来源。2. 核心概念拆解服务、网络、存储与依赖2.1 服务是你的第一抽象单元Compose 里最基础的概念就是服务。一个服务对应一个容器镜像以及这份镜像启动时需要的所有配置。你可以把服务理解成“一台逻辑服务器”nginx 是一个服务mysql 是一个服务你的 Spring Boot 应用是一个服务。服务之间的通信直接在 Compose 定义的默认网络里通过服务名互相访问不需要知道容器的 IP 地址也不需要配置 IP 映射。我之前带过一个小白同事他写服务配置时总想着在 environment 里把其他服务的 IP 写死被网络变化搞得很痛苦。其实 Compose 自带了一个默认网络所有没有显式指定网络的服务都会被加入进去。在这个网络里服务名就是可解析的域名比如 db、redis 这种名字直接能被容器访问到。等于说你只用记住“服务名叫什么”比记 IP 靠谱多了。一个标准的服务定义一般长这样services: web: image: nginx:1.27-alpine ports: - 8080:80 volumes: - ./html:/usr/share/nginx/html这里的 image 指定镜像ports 负责宿主机到容器的端口映射volumes 把本地目录挂载进容器。看起来跟 docker run 很像但优势在于这些是声明式的并且能精确控制哪些配置对哪些服务生效。2.2 网络模型与自定义网络的取舍虽然 Compose 默认网络够用但我还是会建自定义网络原因很直接按职责分离流量。比如一个电商系统里前端流量要走独立网络不让前端容器直接访问数据库后端服务之间走内部网络数据库只暴露给后端服务。通过自定义网络可以实现这种隔离。Compose 的网络声明非常简单顶层一个 networks 节点networks: frontend: driver: bridge backend: driver: bridge services: api: networks: - backend # 注意api 没挂到 frontend web: networks: - frontend这样 web 容器就没法和 api 容器通信了除非你在两个网络里都挂上 api。这种网络设计在微服务架构里特别重要因为它是一个“逻辑防火墙”即使某个服务被攻破攻击者能接触到的内网范围也是受限的。没有网络隔离意识的话所有服务堆在一个网络里安全处置起来非常被动。2.3 数据卷的持久化策略容器是无状态的重启之后容器内的文件系统会还原到镜像初始状态所以数据库这类有状态服务必须做持久化。Compose 里支持两种形式的卷一种是具名卷一种是绑定挂载。具名卷用起来很简单services: mysql: image: mysql:8.4 volumes: - mysql_data:/var/lib/mysql volumes: mysql_data:这种卷由 Docker 管理数据实际存放在 Docker 的数据目录里不用担心容器删除导致数据丢失。绑定挂载则是把宿主机某个目录映射进容器services: web: image: nginx:1.27-alpine volumes: - ./nginx.conf:/etc/nginx/nginx.conf:ro绑定挂载在开发阶段特别方便改完代码直接生效不用重新构建镜像。这里我强烈建议挂配置文件时加个 :ro 只读权限一方面防止容器内部误改宿主机文件另一方面也让配置挂载的意图更明确。2.4 depends_on 不等于等就绪depends_on 是 Compose 里最常用的依赖控制字段但它的语义极其容易造成误解。它只保证服务启动顺序不保证依赖服务已经可用。比如你让 app 依赖 dbCompose 会先启动 db 容器再启动 app 容器但 db 容器内部的数据库引擎可能还需要几十秒才能接受连接。app 启动时去连数据库照样失败。这个问题处理起来有两条路线。一是在应用代码里加健康检查和重试机制二是借助 Compose 的 healthcheck condition 组合。官方推荐后者因为它把“服务就绪”的判断真实内置进了容器状态管理。具体写法是这样services: db: image: postgres:16 healthcheck: test: [CMD-SHELL, pg_isready -U postgres] interval: 5s timeout: 5s retries: 10 app: image: my-app:latest depends_on: db: condition: service_healthy这样 app 会真正等到 db 健康后才启动比盲目 sleep 优雅得多。不过在分布式系统里完全不依赖启动顺序才是更稳健的方案应用自身要有重连退避能力这两种手段不冲突建议都做。3. 从零拆解一个真实微服务项目3.1 架构设计与服务拆分思路为了让你更好地把 Compose 用起来我设计了一个相对完整但又不至于过度复杂的模拟项目用于演示一个电商系统中最典型的服务拆分方式。项目里包含五个角色第一个是前端服务用 Nginx 承载静态页面并反向代理后端接口。第二个是 BFF 网关层也就是 Backend for Frontend负责统一鉴权、路由分发。第三个是用户服务处理用户的注册登录提供用户信息接口。第四个是订单服务负责创建订单、查询订单列表它以用户服务提供的用户信息为数据基础。第五个是基础设施层包含 MySQL 作为业务主存储Redis 作为缓存与会话存储。服务之间通信依赖服务名前端通过 Nginx 反代到网关网关根据路径把请求分发到对应的微服务。这个架构的重点在于让你看到 Compose 如何把这些相互依赖的组件无缝粘合在一起而不是单纯教你五个独立的 Hello World。3.2 完整的 docker-compose.yml 解析先看完整文件再看拆解version: 3.8 services: frontend: image: nginx:1.27-alpine ports: - 8080:80 volumes: - ./frontend/nginx.conf:/etc/nginx/nginx.conf:ro - ./frontend/dist:/usr/share/nginx/html:ro networks: - frontend depends_on: - bff bff: build: ./bff ports: - 8081:8080 environment: USER_SERVICE_URL: http://user-service:8080 ORDER_SERVICE_URL: http://order-service:8080 networks: - frontend - backend depends_on: user-service: condition: service_healthy order-service: condition: service_started user-service: build: ./user-service environment: DB_HOST: mysql DB_PORT: 3306 DB_NAME: mall DB_USER: order DB_PASSWORD: order_pass networks: - backend healthcheck: test: [CMD-SHELL, curl -f http://localhost:8080/actuator/health || exit 1] interval: 10s timeout: 5s retries: 5 depends_on: mysql: condition: service_healthy order-service: build: ./order-service environment: DB_HOST: mysql DB_PORT: 3306 DB_NAME: mall DB_USER: order DB_PASSWORD: order_pass REDIS_HOST: redis REDIS_PORT: 6379 networks: - backend depends_on: mysql: condition: service_healthy redis: condition: service_healthy mysql: image: mysql:8.4 environment: MYSQL_ROOT_PASSWORD: root_pass MYSQL_DATABASE: mall MYSQL_USER: order MYSQL_PASSWORD: order_pass volumes: - mysql_data:/var/lib/mysql - ./mysql/init.sql:/docker-entrypoint-initdb.d/init.sql:ro networks: - backend healthcheck: test: [CMD-SHELL, mysqladmin ping -h localhost -uroot -proot_pass] interval: 5s timeout: 5s retries: 10 redis: image: redis:7-alpine command: [redis-server, --appendonly, yes] volumes: - redis_data:/data networks: - backend healthcheck: test: [CMD-SHELL, redis-cli ping] interval: 5s timeout: 3s retries: 10 networks: frontend: driver: bridge backend: driver: bridge volumes: mysql_data: redis_data:3.3 关键配置背后的设计逻辑看这份 YAML我按点击几个核心设计点解释为什么我要这么配。frontend 只暴露端口只进 frontend 网络。它的作用是承载静态资源并把 API 请求转发到网关。这里 frontend 和 bff 都在 frontend 网络所以 nginx 反代时可以直接写 http://bff:8080不需要关心 bff 容器在宿主机上的端口。Nginx 配置里只要写 upstream 或者 proxy_pass 到服务名就行。这种机制带来的体验提升是巨大的你不需要维护一份 IP 清单服务网格会自动完成负载均衡和 DNS 解析容器重恢复 IP 变化也不会影响配置。bff 桥接两个网络。我让网关同时挂载到 frontend 和 backend等于说它是前端网络和后端网络之间的唯一桥梁这也是“Backend for Frontend”模式在 Compose 下的实现方式。前端 Nginx 只认 bffbff 向后转发到 user-service 或 order-serviceID 划分得一清二楚不会出现前端容器直连数据库的风险。健康检查的类型区别。数据库服务和缓存服务用的是进程级命令mysqladmin ping 和 redis-cli ping简洁可靠。Java 服务则用 HTTP 请求检查应用自己的健康端点这样容器虽然是活的但应用没起来也能被发现。这里有个细节值得注意curl 在容器里不一定存在如果你的服务镜像基于一个很精简的基础镜像要么在 Dockerfile 里装上 curl要么改用 Java 应用的 TCP 检查方式总之别默认 curl 一定可用。初始化 SQL 的挂载方式比较特殊。mysql 镜像首次启动时有个机制会自动执行 /docker-entrypoint-initdb.d 目录下的 .sql 脚本所以我用绑定挂载把本地的数据库初始化脚本放进去。这个机制只在数据卷首次初始化为空时生效如果数据卷已经有数据这个脚本不会再次执行。想重新初始化就把对应的具名卷删掉再来一遍这个坑我犯过好几次。3.4 构建与启动的完整操作流程假设代码已经放到本地对应目录了实际操作流程是docker compose config这一步是最该养成习惯的步骤。它会渲染解析后的最终配置同时校验 YAML 语法和配置格式是否有误。我见过太多人跳过这步写错缩进或者把环境变量写成了字符串结果一启动就爆错。config 命令能把问题拦在启动前省掉的排查时间不可估量。确认配置没错后开始构建镜像docker compose build三个服务需要本地构建bff、user-service、order-service。frontend、mysql、redis 直接拉取公共镜像这一步只需要几秒。构建完成后执行docker compose up -d-d 表示后台运行启动期间可以加上 --wait 参数等待所有带健康检查的服务变成 healthy 状态再返回。不过 --wait 对只设置了 started 条件的服务可能不生效具体逻辑不同版本有差异。启动完成后验证状态docker compose ps如果一切正常看到每个服务对应的状态栏都是 Up 并且后面跟个 healthy 或具体的运行时间。你还可以查看每个容器的实时日志docker compose logs -f --tail100 bff-f 是持续跟随日志输出--tail100 表示从末尾 100 行开始。多服务同时看日志的话也可以直接 docker compose logs -f 全部推送。3.5 后端服务的代码视角光有 Compose 文件还不够服务本身也得能适配这种容器网络环境。我特意做了一个常见的 Java 服务配置样例用来展示环境变量如何进入 Spring Boot 应用spring: datasource: url: jdbc:mysql://${DB_HOST:localhost}:${DB_PORT:3306}/${DB_NAME:mall}?useSSLfalseserverTimezoneAsia/Shanghai username: ${DB_USER:root} password: ${DB_PASSWORD:root} data: redis: host: ${REDIS_HOST:localhost} port: ${REDIS_PORT:6379}这里的关键点在于默认值。你在本地没有环境变量时它会自动退回 localhost 和默认端口开发者直接连本地数据库就能跑。在 Compose 环境里环境变量被注入后数据库连接就自动指向 mysql 服务名。同一套代码既能在本地跑也能在容器里跑这比硬编码地址要优雅太多。我见过不少团队写代码时直接用 jdbc:mysql://localhost:3306/mall本地能跑一进容器连不上了。容器里的 localhost 指的是容器自己而不是数据库容器这个认知差异是新手必踩的坑。用环境变量加默认值的方式才能做到“本地容器两开花”。4. 真实项目里的扩展方案scale、profiles 与外部配置4.1 横向扩容与负载均衡微服务落地到 Compose 之后最自然的扩展方式就是横向扩容。你可以直接指定某服务扩容到多个实例docker compose up -d --scale user-service3这个命令会让 user-service 的容器数量变成 3Compose 会自动分配三个容器的网络 IP并让它们共享同一个服务名。此时前面配置的 Nginx 反代对服务的访问会自动在这三个实例之间做轮询负载均衡。这是 Docker 内置 DNS 的机制带来的能力不需要额外引入注册中心就能做基础的水平伸缩。但注意横向扩容对目标服务也有要求。服务必须是无状态的或者状态集中存储在外部组件中。如果一个服务把会话状态存在自己内存里扩容之后用户的请求被分配到不同的实例上就有可能导致会话丢失。所以在设计阶段就要把这种扩展性考虑进去任何业务服务都尽量不要在自己的文件系统或内存里保存关键数据。4.2 多环境管理用 profiles 和覆盖文件Compose 3.x 开始支持 profiles 特性可以针对不同场景触发不同的服务集合。例如services: mysql: image: mysql:8.4 profiles: [dev, test] redis: image: redis:7-alpine profiles: [dev]启动的时候用 --profile dev 或者设置 COMPOSE_PROFILES 环境变量即可。只选 dev profile 的话redis 会启动mysql 也会启动但其他标记了其他 profile 的服务就被排除在外。这让同一份 compose 文件在不同环境间保持一致性和可维护性的同时提供按需组合的能力。另一个更基础的做法是使用多个 YAML 文件docker compose -f docker-compose.yml -f docker-compose.prod.yml up后面的文件自动覆盖前面的同名配置。比如开发环境不启用资源限制生产环境的覆盖文件里加上 deploy.resources.limits。这种组合方式对运维人员特别友好避免一套配置到处复制的困境。4.3 敏感信息与外部配置的注入方式环境变量里直接写密码放在版本库里显然不安全。很多团队刚开始用 Compose 时都会犯这个错配置文件里挂着明文密码到处传。正确的做法是用 .env 文件存放变量让 Compose 在解析时动态替换同时把 .env 文件加入 .gitignore不进入版本控制。MYSQL_ROOT_PASSWORDlocaltest_root DB_USERorder DB_PASSWORDlocaltest_order在 docker-compose.yml 里就能直接用 ${MYSQL_ROOT_PASSWORD} 引用这些值。更进一步可以引入 docker secrets把密码文件挂载到 /run/secrets/ 目录应用从文件读取敏感信息。这对生产环境尤其重要但复杂度相对更高本地开发用 .env 就足够顺滑了。4.4 资源限制与日志滚动的必要设置真实项目中如果不设置资源上限一个内存泄漏的服务可能把整台机器拖垮。Compose 支持在服务下声明 resource 限制services: order-service: build: ./order-service deploy: resources: limits: cpus: 1.0 memory: 512M注意 deploy 块的定义来自 Swarm 模式的配置规范在 Compose 单机模式下虽然不会强制但 docker compose up 时会尝试识别并应用部分字段。如果你用 docker compose 时发现 deploy 配置没有生效可能跟版本有关最稳妥的办法是在服务定义里直接设置 mem_limit 和 cpus 这类传统字段或者干脆依赖 Docker 引擎的容器默认限制。日志也要做滚动控制否则运行久了磁盘会被日志撑爆。默认情况下容器日志文件是无限制累积的需要加上services: user-service: logging: driver: json-file options: max-size: 10m max-file: 3这样单个日志文件超过 10MB 就会自动切割超过 3 个文件就会删除最旧的日志配合合理的日志聚合方案能有效控制宿主机磁盘占用。5. 常见问题与排查技巧实录5.1 端口冲突与服务启动失败启动时如果提示 bind: address already in use基本可以断定宿主机端口已经被占用。排查手段是先确认端口被什么进程占用lsof -i :8080 netstat -tulpn | grep 8080解决方式有两种思路一是停掉占用端口的进程二是修改 Compose 里的端口映射比如把 8081:8080 改成 8091:8080。还有一种常见情况是你上一个 Compose 项目还没停止同样的端口映射又被新的项目尝试使用这时候 docker compose ps 检查一下所有项目即可。我个人的习惯是启动前先 docker compose ps 看看当前项目状态避免新老容器同占端口导致反复失败。5.2 容器内连接不上其他服务这个问题绝大多数时候不是因为网络断了而是服务之间根本不在同一个网络里。用自定义网络后容易被忽略的场景是某个服务没声明 networks 或者声明错了网络结果在另一个网络里的服务访问它的时候DNS 解析得到空地址。服务名在这个网络里不存在当然连接失败。一个很实用的排查技巧进入容器内部手动解析一下服务名。docker compose exec bff sh ping user-service如果不能 ping 通检查自己的 YAML 里是否都挂在同一个网络。另一个坑是防火墙策略或 Docker 网络插件导致的 DNS 解析异常不过本地环境出现的概率很低。最隐蔽的情况是用 localhost 访问对方服务容器看到的 localhost 永远是自己必须改成目标服务名。5.3 数据库启动后初始化脚本不执行刚才已经提到MySQL 镜像只会首次创建空数据卷时执行 init.sql。如果你已经启动过然后改了 init.sql想让新表结构生效必须把旧数据卷删掉docker compose down docker volume rm project_mysql_data docker compose up -d这个操作会把数据库数据全部清空只能用于开发环境。生产环境如果有表结构变更应该走迁移工具而不是依赖容器初始化脚本。我强烈建议本地开发时用一个可销毁的开发数据卷不要跟真实数据混在一起避免手一抖全没了。5.4 容器内时区与宿主不一致容器默认通常是 UTC 时间日志记录的时区跟本地差 8 个小时排查线上问题时容易产生误导。官方镜像很多都支持通过环境变量 TZ 指定时区environment: - TZAsia/Shanghai但对于基于精简系统镜像的容器直接设置环境变量可能不生效需要在 Dockerfile 里安装 tzdata 包并设置时区符号链接。Java 应用本身还可以通过 JVM 参数传递时区设置。这个问题表面看不大但在多语言服务并存的项目里特别容易埋坑我建议所有自定义镜像在构建期就把时区问题解决掉。5.5 频繁重启循环与健康检查误判服务容器不断重启docker compose ps 显示 Restarting 状态最常见的原因是应用进程启动即崩溃。先用 docker compose logs 查看具体日志。比如 Java 服务堆积了大量 JVM 参数导致启动失败数据库连接配置错误导致启动时初始化失败这些都能从日志里明显看到。另一类情况是健康检查配置得太严苛服务实际已经起来但健康检查命令需要的某个工具不存在导致一直 reporting unhealthy。这时需要进入容器手动执行一次 healthcheck 里的命令比如docker compose exec user-service curl -f http://localhost:8080/actuator/health返回非零状态或者命令不存在问题就出在这里。调整 healthcheck 的 test 指令换成容器里确实存在的命令问题就能解决。5.6 容器内文件权限导致挂载卷无法访问Linux 下绑定挂载的目录其所有者和权限由宿主机决定。如果容器内进程应该以某个特定用户运行比如 nginx 的 worker 进程用 nginx 用户运行而宿主机挂载进来的文件权限是 644 或者所有者不对容器内可能无法写入文件。排查方式包括查看挂载目录的 linux 权限、understand Docker 的账号映射原理。解决方式很多常见的手段是调整宿主机的目录属主或者在镜像内自定义 user 的属组 ID让它们保持一致。如果不方便改基础镜像也可以临时在 Compose 里用 user 字段直接指定进程运行用户不过这会绕过镜像默认安全策略需要权衡。6. 把 Compose 作为团队协作的基石6.1 环境一致性带来的效率提升微服务团队最头疼的问题之一就是环境不一致。有人用 Windows 开发有人用 macOS有人用 Linux依赖软件的版本也参差不齐唯一能在所有人机器上保持一致的就是 Docker 容器本身。Compose 文件一提交所有团队成员拉下来直接 up数据库版本错了、中间件缺失、依赖不一致这些问题基本绝迹。我自己的体会是这套体验不仅能提升本地开发效率还能让新人的上手成本显著下降。A 同学入职当天不需要翻一小时 wiki只需要装好 Docker复制一份仓库docker compose up整个开发环境就在眼前了。环境问题一旦让新人纠缠太久对士气的影响远比想象中大。6.2 测试环境按需拉起与销毁除了本地开发Compose 还有一个容易被低估的用法是测试环境治理。团队每个迭代需要验证不同分支的兼容性你可以为每个 PR 拉一套独立的 Compose 环境挂上不同的端口和网络别名跑完自动化测试后直接 docker compose down。这套模式在资源有限的团队里特别实用不需要维护庞大的常驻测试集群按需拉起、用完即焚成本极低。6.3 向 Kubernetes 迁移前的过渡方案不少人纠结到底要不要直接上 Kubernetes。现实情况是很多团队的体量根本用不到 Kubernetes 的全部能力而维护成本却翻了几倍。Compose 在单机和轻量多机场景下有天然优势它足够简单Git 里维护一套配置就能支撑整个项目从开发到预发布的全过程。即使将来迁移到 KubernetesCompose 里的服务定义、环境变量、健康检查、卷设计这些思路依然可以映射到 Kubernetes 的 Deployment 与 Service 上。我见过不少团队先拿 Compose 把微服务的领域边界梳理清楚确认好服务之间的 API 契约和数据依赖再平滑过渡到编排平台。换句话说Compose 不只是玩具它完全可以作为微服务架构演进的起手式。7. 一条命令一套体系Compose 这条命令看似简单背后支撑的却是一整套容器编排的思路。它帮你把“启动顺序、网络拓扑、数据持久化、健康检测”这些运维知识下沉到一份可读的文件里让开发人员不用在环境问题上反复挣扎。读到这里我猜很多人已经摩拳擦掌想拿自己的项目试试了。最后分享一个我的个人习惯无论项目多小我都会在 init 阶段就把 Compose 配好哪怕是只有一个数据库和一个应用服务的简单结构。因为环境定义得越早后面迭代的成本越低。等你的项目真的长到十几二十个服务时再回头处理统一的环境配置那就不是一两天能解决的事了。Docker Compose 的优雅恰好在于那份提前做好的规划让后来的一切都变得顺理成章。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑