第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:

这个配置文件做了几件事:

  1. 定义了三个服务:web、app、db
  2. web 暴露 8080 端口到宿主机
  3. app 依赖于 db,确保 db 先启动
  4. 两个网络隔离了前后端流量
  5. 一个命名卷 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 核心逻辑 docker-compose.yml Services image / build ports / environment depends_on / restart command / healthcheck Volumes 命名卷:name:path 绑定挂载:host:container 持久化数据库数据 开发热重载 Networks bridge 默认网络 自定义隔离网络 服务名替代 IP 多网络接入 docker compose up -d 三要素缺一不可 多容器协同工作

你可以把 docker-compose.yml 想象成一个“指挥官”。它告诉 Docker:
“我要启动三个服务,它们各自用什么镜像、暴露哪些端口、挂载哪些卷、连接到哪些网络。你们按照我说的来执行。”

说白了,Compose 让容器从单打独斗变成团队作战。学会它,才算真正入门了容器编排。