4. Docker Compose 入门:docker-compose.yml 编写与多容器编排

单个容器能跑一个服务,但质谱数据处理流程没那么简单。你可能需要同时运行数据预处理、峰检测、数据库写入三个任务。一个一个敲 docker run?太累了。这时候 Docker Compose 就派上用场了。

我个人习惯把 Compose 看作是一个项目的“说明书”。你写一个 YAML 文件,把服务、网络、存储卷都定义清楚。然后一条命令,全部启动。今天我们就来吃透这个 docker-compose.yml 该怎么写。

4.1 为什么需要多容器编排?

在真实项目中,我常常需要把流程拆成多个独立的模块。比如质谱数据经过预处理后,要送到另一个容器做谱库检索,最后结果存到数据库。每个模块都是独立的容器,但它们之间需要通信。

你想想看,如果全靠命令行启动容器,要记 IP 地址,要手动建网络,还要控制启动顺序。稍微改个配置就得重来。这不是在写代码,这是在给自己找麻烦。

Docker Compose 解决了这些痛点:

  • 服务定义在一个文件里,版本可控
  • 自动创建网络,服务间用服务名通信
  • 可以指定依赖关系,控制启动顺序
  • 卷和端口映射统一管理
我的经验:刚开始接触 Compose 时,我觉得它就是个“一键启动”工具。后来用多了才发现,它真正的价值在于把环境配置和业务流程绑定在一起。换台机器,拷贝一个目录,docker compose up 就完事了。

4.2 docker-compose.yml 核心结构

一个典型的 Compose 文件长这样。我挑几个重点部分来剖析:

version: '3'

services:
  preprocess:
    image: ms-tools/preprocess:1.0
    volumes:
      - ./data:/data
    networks:
      - ms-net

  database:
    image: postgres:13
    environment:
      POSTGRES_DB: mslib
      POSTGRES_USER: msuser
      POSTGRES_PASSWORD: mspass
    volumes:
      - pgdata:/var/lib/postgresql/data
    networks:
      - ms-net

  web:
    image: ms-tools/webui:1.0
    ports:
      - "8080:80"
    depends_on:
      - database
    networks:
      - ms-net

volumes:
  pgdata:

networks:
  ms-net:

4.3 版本声明

文件第一行就是version: '3'。很多人以为这是 Compose 工具的版本,其实不是。它指定的是 Compose 文件格式的版本。我建议直接用 '3',原因后面说。

不同版本支持的语法略有差异。比如版本 2 不支持 depends_on 的条件等待,版本 3 引入了 deploy 块用于 Swarm 模式。日常开发用版本 3 最稳。

4.4 服务定义:把流程拆成块

services 下面每个键就是一个服务。服务名最好起得一看就懂——preprocess、database、web。名字会作为主机名在容器间通信使用。

每个服务都要告诉 Compose 用哪个镜像。常用的方式有两种:

方式 适用场景 示例
直接指定 image 使用现成的镜像,比如 PostgreSQL image: postgres:13
指定 build 上下文 需要现场构建镜像 build: ./app

我这里举的例子都用的 image。如果你有一个自定义的质谱处理程序,可以写成这样:

services:
  peak-detector:
    build: ./peak-detector
    volumes:
      - ./raw_data:/input
      - ./results:/output

这时候 Compose 会先执行 docker build,再启动容器。嗯,这里要注意:如果 build 和 image 同时存在,Compose 会用 build 构建的镜像,但标签打的是 image 指定的名字。

4.5 卷挂载:数据怎么传递?

质谱数据可能几百兆甚至几个 G,不能放在镜像里。卷挂载是必学的技能。

挂载有两种形式:

  • 绑定挂载:直接映射宿主机路径,比如 ./data:/data
  • 命名卷:用卷名管理,比如 pgdata:/var/lib/postgresql/data

我个人更推荐命名卷来存放持久化数据。为什么呢?因为绑定挂载如果路径写错,数据可能写到意料之外的地方。命名卷由 Docker 管理,安全多了。

如果你要在多个容器间共享数据,可以用同一个命名卷:

volumes:
  shared-data:

services:
  converter:
    image: ms-tools/converter
    volumes:
      - shared-data:/work

  analyzer:
    image: ms-tools/analyzer
    volumes:
      - shared-data:/work

这样 converter 写完文件,analyzer 就能读到。顺序呢?用 depends_on 控制。

4.6 网络配置:服务间怎么通信?

Compose 会自动创建一个默认网络,所有服务都在里面。所以默认情况下,服务间用服务名就能 Ping 通。但有些场景下你需要自定义网络。

核心:在同一网络下,服务名就是主机名。比如 database 服务,其他服务访问它时,连接字符串写host=database就行了。不用查 IP,不用记端口。

看一个实际的质谱流程例子:

services:
  preprocess:
    image: ms-tools/preprocess
    networks:
      - frontend
      - backend

  database:
    image: postgres:13
    networks:
      - backend

networks:
  frontend:
  backend:

preprocess 服务同时接入两个网络,可以接收来自前端的请求,也能访问后端的数据库。而数据库只在 backend 网络里,前台模块无法直接接触数据库。我曾经在项目中就用这个思路做安全隔离,效果很好。

4.7 依赖与启动顺序

depends_on 是编排的关键。它告诉 Compose:先启动哪个服务,再启动哪个。比如:

services:
  database:
    image: postgres:13

  webui:
    image: ms-tools/webui
    depends_on:
      - database

这样 webui 会在 database 之后启动。但注意,depends_on 只保证容器启动了,不保证数据库准备好接受连接。如果 webui 启动太快,数据库还没就绪怎么办?

我曾经踩过这个坑。后来我在应用代码里加了个重试机制,效果不错。也可以写个等待脚本:

services:
  webui:
    image: ms-tools/webui
    depends_on:
      - database
    command: >
      sh -c "while ! nc -z database 5432; do sleep 1; done; exec myapp"

这相当于在容器启动后先等数据库端口可用,再启动主程序。稳妥。

4.8 环境变量与配置

环境变量可以用 environment 块写死,也可以通过 env_file 引入文件。我推荐把敏感信息放文件里,不要明文写在 Compose 文件中。

services:
  database:
    image: postgres:13
    env_file:
      - ./db.env

db.env 文件的内容:

POSTGRES_DB=mslib
POSTGRES_USER=msuser
POSTGRES_PASSWORD=mysecretpassword

这样 Compose 文件可以提交到版本控制,但 .env 文件要加入 .gitignore。

4.9 常用 Compose 命令一览

命令 作用 常用场景
docker compose up -d 后台启动所有服务 日常启动流程
docker compose down 停止并删除容器和网络 清理环境
docker compose logs -f 实时查看所有服务的日志 调试和排错
docker compose ps 查看服务运行状态 快速检查
docker compose exec 在运行中的容器执行命令 进入容器排查问题

4.10 一套完整的质谱流程 Compose 配置

最后,我展示一个接近实际项目的配置。包含预处理、峰检测、谱库检索、数据库四个服务,还加了 healthcheck 确保数据库就绪:

version: '3'

services:
  preprocessing:
    image: ms-tools/preprocess:2.1
    volumes:
      - ./raw_mzml:/data/raw
      - ./preprocessed:/data/out
    networks:
      - ms-pipeline

  peak-picking:
    image: ms-tools/peak:1.4
    depends_on:
      preprocessing:
        condition: service_started
    volumes:
      - ./preprocessed:/data/in
      - ./peaks:/data/out
    networks:
      - ms-pipeline

  library-search:
    image: ms-tools/library:0.9
    depends_on:
      - peak-picking
    volumes:
      - ./peaks:/data/in
      - ./results:/data/out
    networks:
      - ms-pipeline

  db-writer:
    image: ms-tools/dbwriter:1.0
    depends_on:
      database:
        condition: service_healthy
    volumes:
      - ./results:/data/in
    networks:
      - ms-pipeline

  database:
    image: postgres:13
    environment:
      POSTGRES_DB: massspec
      POSTGRES_USER: msadmin
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - msdata:/var/lib/postgresql/data
    networks:
      - ms-pipeline
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U msadmin -d massspec"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  msdata:

networks:
  ms-pipeline:

注意 database 服务加了 healthcheck,db-writer 的 depends_on 等待条件设置为 service_healthy。这样能确保写入数据时数据库已经可用,不会报连接失败。

避坑指南:我曾经在生产环境里没写 healthcheck,结果 db-writer 启动后直接连接失败,写了满满一屏幕的错误日志。后来把 healthcheck 加上,问题就解决了。数据库类服务一定要加健康检查。

4.11 本章知识结构一览

下面这张图总结了 docker-compose.yml 的核心要素和它们之间的关系。你看一眼就能抓住主线:

Docker Compose 核心结构 docker-compose.yml Services 服务定义 Networks 网络隔离 Volumes 数据持久化 image / build depends_on 服务间通信 数据共享 一句话概括 Compose 把多个容器的镜像、网络、数据卷、依赖关系 写在一个 YAML 文件里,一条命令统一管理。

从中心节点向外,三根线分别指向 Services、Networks、Volumes。这三个部分就是 Compose 文件的骨架。每个服务定义镜像来源、依赖关系,再挂载卷、接入网络,整个流水线就串起来了。