第4章 Docker Compose:多容器编排、服务定义、网络与卷的使用
上一章我们跑通了一个容器。但在真实项目中,一个服务往往需要好几个容器配合。
举个例子,一个Web应用,至少得有个Nginx反向代理、一个Python后端、一个Redis缓存、一个Postgres数据库。你手动一个个 docker run,参数写到手软,还容易漏掉网络配置。
这时候就该 Docker Compose 上场了。
4.1 Compose 到底解决什么问题
说白了,Compose 就是一个“多容器编排工具”。它用一个 YAML 文件把多个服务、网络、卷的定义写在一起,一条命令就能全部启动。
我个人习惯把 docker-compose.yml 放在项目的根目录下。这样不管是新同事还是 CI 流水线,一看这个文件就知道整套环境怎么跑。
核心价值:用声明式配置替代繁琐的命令行,实现“一份配置,到处运行”。
它的工作流程是这样的:
- 你在 YAML 里定义好各个服务
- 定义它们之间的网络连接
- 定义需要持久化的数据卷
- 一条命令让所有容器按依赖顺序启动
一个小技巧:Compose 会自动创建一个默认网络,所有服务都在这个网络里,通过服务名就能互相访问。
4.2 docker-compose.yml 长什么样
先看一个最基础的例子。
version: '3.8'
services:
web:
image: nginx:alpine
ports:
- "8080:80"
networks:
- frontnet
app:
build: ./app
depends_on:
- db
networks:
- frontnet
- backnet
db:
image: postgres:13
environment:
POSTGRES_PASSWORD: secret
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- backnet
volumes:
pgdata:
networks:
frontnet:
backnet:
这个配置文件做了几件事:
- 定义了三个服务:web、app、db
- web 暴露 8080 端口到宿主机
- app 依赖于 db,确保 db 先启动
- 两个网络隔离了前后端流量
- 一个命名卷 pgdata 保存数据库数据
我在项目中遇到过不少次这样的问题:新同事把数据库连接写成了 localhost,结果连不上。其实一旦用了 Compose,服务名就是主机名——app 要连 db,直接写 db:5432 就行。
4.3 服务定义:不仅仅是镜像
services 块里每个服务都能定义很多细节。除了 image 和 build,常用的还有这些:
| 字段 | 作用 | 我踩过的坑 |
|---|---|---|
| image | 指定基础镜像 | 能定版本就别用 latest |
| build | 指定 Dockerfile 路径 | 相对路径容易搞混,用 . 或 ./xxx |
| ports | 端口映射 host:container | 小心端口冲突,特别是 80 和 443 |
| environment | 设置环境变量 | 密码别写死,用 .env 文件 |
| depends_on | 启动顺序控制 | 它只管启动,不等就绪,需要额外处理 |
| restart | 重启策略 | 生产环境我一般用 unless-stopped |
| command | 覆盖容器默认命令 | 和 CMD 一起写容易混淆 |
举个例子,重启策略有这么几种:
services:
app:
restart: "no" # 从不重启
restart: always # 总是重启
restart: on-failure # 出错才重启
restart: unless-stopped # 除非手动停止,否则总重启
我建议:生产环境里的服务,如果不希望它挂掉就再也不起来,用 unless-stopped。开发环境用 on-failure 就够了。
4.4 网络:容器之间的高速公路
没有网络,容器就是孤岛。Compose 的网络模型非常灵活。
默认情况下,Compose 会创建一个与项目名同名的网络。所有在这个网络里的容器,都能通过服务名互相访问。
但有时候你需要更精细的控制。比如让 Web 层对外暴露,让后端只对内通信。这时候就需要自定义网络了。
注意:
- 不同网络之间的容器默认是不能通信的
- 一个容器可以加入多个网络
- 网络模式有 bridge(默认)、host、none
我举个例子,为什么需要多个网络?
曾经有个项目,我把所有服务都放在同一个网络里。结果安全审计说 Web 服务能直接访问数据库,这是安全隐患。后来我改成前端服务接前端网络,后端服务接后端网络,数据库只接后端网络。这样 Web 容器无论如何都连不上数据库了。
配置方式很简单:
networks:
frontend:
driver: bridge
backend:
driver: bridge
services:
nginx:
networks:
- frontend
api:
networks:
- frontend
- backend
db:
networks:
- backend
你看,nginx 只能访问 api,api 既能被 nginx 访问,又能访问 db,但 nginx 永远访问不了 db。这种网络隔离比写防火墙规则省心多了。
4.5 卷:数据持久化的正确姿势
容器一旦删除,里面的数据就没了。数据库、日志、上传的文件该怎么办?
答案是用卷(volume)。
Compose 支持两种卷:
- 命名卷:由 Docker 管理,存在宿主机特定目录下
- 绑定挂载:直接把宿主机的某个目录映射到容器里
它们的区别很明显:
| 类型 | 配置方式 | 适用场景 |
|---|---|---|
| 命名卷 | volumes: - pgdata:/var/lib/postgresql/data | 数据库数据、持久化存储 |
| 绑定挂载 | volumes: - ./data:/app/data | 开发热重载、调试 |
我用命名卷比较多,因为不用担心路径问题。而且 docker volume prune 清理未使用的卷,挺方便的。
如果你在开发调试,绑定挂载就特别有用:
services:
app:
build: .
volumes:
- .:/app
- /app/node_modules
这样你在本地改了代码,容器里立刻就能看到。不过要注意把 node_modules 这类依赖目录排除掉,否则本地和容器里的依赖会打架。这也是我踩过的坑之一。
4.6 常用命令一览
写好了 docker-compose.yml,用这些命令来操作:
# 启动所有服务(后台运行)
docker compose up -d
# 查看运行状态
docker compose ps
# 查看日志
docker compose logs -f
# 重新构建并启动(改了 Dockerfile 时用)
docker compose up -d --build
# 停止并删除容器、网络
docker compose down
# 停止并删除卷(慎用,数据会丢)
docker compose down -v
# 进入某个容器
docker compose exec app bash
我最常用的组合是 docker compose up -d --build。改了代码或者 Dockerfile 之后,一条命令就全搞定,不用手动 docker build 再 docker run。
日常开发小技巧:把 docker compose up -d 写成 alias,比如 alias dcup="docker compose up -d"。能省不少打字时间。
4.7 本章知识体系
下面这张图展示了 Docker Compose 的核心逻辑:用一份 YAML 文件把服务、网络、卷这三个要素组织起来,实现多容器的协同工作。
你可以把 docker-compose.yml 想象成一个“指挥官”。它告诉 Docker:
“我要启动三个服务,它们各自用什么镜像、暴露哪些端口、挂载哪些卷、连接到哪些网络。你们按照我说的来执行。”
说白了,Compose 让容器从单打独斗变成团队作战。学会它,才算真正入门了容器编排。