第4章:Docker Compose与多容器管理——用docker-compose编排多容器应用
这一章,我们来聊聊多容器管理。说白了,当你的分析流程从“一个容器搞定”变成“多个服务协同”时,手动docker run就变得很难受。我记得第一次接手一个需要同时跑数据库、Web服务和计算引擎的项目,光是记启动顺序和端口映射就花了一整个下午,还老是搞混。后来我学乖了,老老实实用docker-compose。
Docker Compose就是帮你用一个YAML文件定义、运行多个容器的工具。你写一个docker-compose.yml,一个命令docker compose up,所有服务就按照你规划的依赖关系和网络配置启动起来。在生物信息学的可重复性实践中,这个能力特别重要——你的分析环境不该只是“一个镜像”,而是一整套“服务组合”,包括数据库、队列、文件存储、计算节点等等。
为什么需要Docker Compose?
我遇到过不少团队,项目初期只用单个容器做分析,后面发现结果存不下来、计算资源没法隔离、环境变量分散在多个shell脚本里。这就是典型的“容器碎片化”。Docker Compose解决的正是这个问题:它把服务定义、网络、卷、环境变量全部写进一个配置文件,版本控制起来,其他成员拉下来直接跑。
我们来看一个核心观点:Dockerfile定义的是单个容器的“是什么”,而docker-compose.yml定义的是多个容器的“怎么配”。一个管构建,一个管编排。
| 比较维度 | docker run(单容器) | docker-compose(多容器) |
|---|---|---|
| 启动方式 | 命令行参数一串 | YAML配置 + 一条命令 |
| 网络配置 | 手动--link或创建网络 | 自动创建网络,服务名即主机名 |
| 依赖管理 | 手动控制启动顺序 | depends_on声明依赖 |
| 可重复性 | 依赖人的记忆 | 配置文件纳入版本控制 |
核心概念:服务、网络、卷
Compose里的三大件:services、networks、volumes。简单记住:服务是跑什么程序,网络是怎么连通,卷是数据存哪里。
我们以生物信息学中一个典型场景为例——Nextflow的数据库后端加上文件存储。你想想看,Nextflow运行时会需要一个PostgreSQL来记录任务状态,也需要一个MinIO兼容S3接口来存中间结果和输出。这俩服务必须彼此连通,而且数据要持久化。
version: '3.8'
services:
postgres:
image: postgres:14
container_name: nf-postgres
environment:
POSTGRES_DB: nextflow
POSTGRES_USER: nf_user
POSTGRES_PASSWORD: nf_pass
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- nf-net
healthcheck:
test: ["CMD-SHELL", "pg_isready -U nf_user -d nextflow"]
interval: 10s
timeout: 5s
retries: 5
minio:
image: minio/minio:latest
container_name: nf-minio
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin
volumes:
- miniodata:/data
networks:
- nf-net
ports:
- "9000:9000"
- "9001:9001"
volumes:
pgdata:
miniodata:
networks:
nf-net:
driver: bridge
这个配置文件里,depends_on还没出现,你可能好奇为什么。因为我个人习惯在关键服务上用healthcheck来替代简单的depends_on,后者只保证容器启动了,不保证服务就绪了。你想想看,PostgreSQL容器启动了,但数据库初始化还没完成,Nextflow一连接就报错。所以我倾向于用健康检查来替代。
服务编排与依赖控制
依赖关系是编排的关键。很多新手直接写depends_on: - postgres,然后发现Nextflow无法连接。为什么?因为depends_on只保证容器进程开始运行,不等数据库的TCP端口可访问。
我曾经踩过一个坑:把Nextflow、PostgreSQL、MinIO一起启动,结果Nextflow连着三次重试都失败,日志里全是“connection refused”。后来我给PostgreSQL和MinIO都加了健康检查,然后在Nextflow的启动脚本里增加了condition: service_healthy。
services:
nextflow:
image: nextflow/nextflow:latest
container_name: nf-executor
depends_on:
postgres:
condition: service_healthy
minio:
condition: service_healthy
environment:
NXF_EXECUTOR: local
NXF_WORK: s3://nf-work/
AWS_ACCESS_KEY_ID: minioadmin
AWS_SECRET_ACCESS_KEY: minioadmin
AWS_ENDPOINT_URL: http://minio:9000
networks:
- nf-net
volumes:
- ./workflows:/workflows
command: run /workflows/main.nf
这里的关键是condition: service_healthy。这样Nextflow只有在PostgreSQL和MinIO都通过健康检查后才开始启动。这是一种稳健的编排方式,尤其在自动化部署环境中非常实用。
网络模式与服务发现
在Compose默认的网络里,服务名就是主机名。你的Nextflow容器里用http://minio:9000就能访问MinIO,用postgres:5432就能连PostgreSQL。这一点非常方便,你不需要自己去维护DNS或者IP映射。
我习惯在每个项目中创建一个专门的网络,而不是用默认的default网络,这样可以隔离不同项目组的服务,避免端口冲突。
networks:
nf-net:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
注意,如果你指定了子网,一定要确保跟其他网络不重叠。我有一回在服务器上同时跑了两个Compose项目,都用了172.20.0.0/16,结果网络完全乱掉,容器之间互相ping不通。后来全部改成不同子网才解决。
环境变量管理的两种做法
环境变量在Compose里可以用两种方式传:直接写在environment下面,或者用env_file引用外部文件。我个人建议把敏感信息和通用配置写到env_file里,把项目特定的快捷配置写在environment里。这样env_file可以加入.gitignore,避免密码泄露。
services:
postgres:
image: postgres:14
env_file:
- ./config/postgres.env
environment:
POSTGRES_DB: nextflow
postgres.env文件内容:
POSTGRES_USER=nf_user
POSTGRES_PASSWORD=nf_pass
讲到这里,你可能会问:如果既写了env_file又写了environment,哪个优先级高?答案是environment会覆盖env_file里的同名变量。所以POSTGRES_DB最终会是nextflow,而POSTGRES_USER和POSTGRES_PASSWORD来自env_file。
多容器场景下的数据持久化
生物信息学分析通常会产生大量中间数据和结果文件。Compose里用volumes来管理持久化存储。你可以用命名卷,也可以用绑定挂载。
我通常在开发阶段用绑定挂载,方便直接看文件;在生产部署上用命名卷,因为命名卷由Docker管理,性能更好,也不容易因为路径改动而出问题。
命名卷 vs 绑定挂载:
- 命名卷:
volumes: - pgdata:/var/lib/postgresql/data—— 由Docker管理,适合生产 - 绑定挂载:
volumes: - ./data:/var/lib/postgresql/data—— 直接映射到宿主目录,适合开发和调试
如果你在团队协作中需要确保所有人使用同样的数据目录结构,建议统一使用命名卷,并在项目文档里写明卷的用途。
一个完整的实战模板
接下来,我们组合一下上面所有知识点,写一个完整的、可直接复用的Nextflow执行环境Compose配置。
version: '3.8'
services:
postgres:
image: postgres:14
container_name: nf-postgres
restart: unless-stopped
env_file: ./config/postgres.env
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- nf-net
healthcheck:
test: ["CMD-SHELL", "pg_isready -U nf_user -d nextflow"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
minio:
image: minio/minio:latest
container_name: nf-minio
command: server /data --console-address ":9001"
restart: unless-stopped
env_file: ./config/minio.env
volumes:
- miniodata:/data
networks:
- nf-net
ports:
- "9000:9000"
- "9001:9001"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 15s
timeout: 5s
retries: 3
start_period: 20s
nextflow:
image: nextflow/nextflow:latest
container_name: nf-executor
depends_on:
postgres:
condition: service_healthy
minio:
condition: service_healthy
env_file: ./config/nextflow.env
networks:
- nf-net
volumes:
- ./workflows:/workflows
- ./results:/results
working_dir: /workflows
command: run main.nf -with-report report.html -with-trace trace.tsv
volumes:
pgdata:
miniodata:
networks:
nf-net:
driver: bridge
这个配置把数据库、对象存储、执行器三个服务编排在一起。当你执行docker compose up -d时,Docker会按依赖顺序启动服务,只有健康检查通过后Nextflow才会启动。所有配置集中管理,其他人拉下来后只需要调整./config/目录下的环境文件即可。
我建议你在开发阶段用docker compose up --build强制重建镜像(如果Compose文件引用了本地Dockerfile),但在生产部署时用docker compose up -d --no-build只拉取已构建好的镜像,避免意外覆盖。
一张图理清编排逻辑
这张图展示了docker-compose.yml作为编排中心,向下连接三个核心服务。实线表示配置定义,虚线表示启动依赖。所有服务共享一个网络,通过命名卷持久化数据。健康检查保证了实际可用性,而不是“容器运行中”这种虚假的就绪状态。
特别注意:不要把生产环境里所有服务都塞进一个Compose文件。我见过有人把分析流程、前端、后端、数据库、缓存、定时任务全写在一个文件里,结果每次更新都要重启全部服务,非常痛苦。建议按“功能域”拆分成多个Compose文件,比如基础设施一组、分析流程一组。用-f参数可以合并加载:docker compose -f infra.yml -f pipeline.yml up。
小技巧:在开发过程中,用docker compose config命令可以查看最终的合并配置。这个命令会解析所有引用、环境变量、扩展字段,输出一个完整的YAML。我每次写复杂Compose文件后都会先跑一下docker compose config确认没有语法错误或逻辑冲突。
常见坑与避坑指南
- 端口冲突:多个Compose项目绑定同一个宿主端口会导致启动失败。解决方法是给每个项目分配不同的端口段,或者用反向代理统一管理。
- 依赖不健康:我见过有人只用
depends_on不加健康检查,服务启动后依赖容器还没准备好。应该学我们上面那样,用condition: service_healthy。 - 环境变量泄露:在
environment里写死密码,然后不小心提交到Git。解决方法是使用env_file,并将该文件加入.gitignore。 - 卷权限问题:PostgreSQL容器内的uid和宿主不一致,导致数据目录无法读写。可以在Compose里指定
user,或者在Dockerfile里提前设置好uid。
讲到这里,你对Docker Compose的核心用法应该比较清晰了。在接下来的课程中,我们会把这些多容器编排的能力延续到Nextflow的配置集成中。记住一条原则:你今天写下的每一个编排配置,都是在为明天的重现性投资。你的同事、未来的你,都会感谢这份清晰的编排文件。