使用 Docker 部署 Cromwell

说实话,Docker 部署 Cromwell 是我在实际项目中最常用的一种方式。为什么?因为它省心。你想想看,Cromwell 本身依赖 Java,还有各种数据库驱动、Centaur 组件等等。直接用 Docker 镜像,这些东西全部被打包好了,拿到手就能跑。

我曾经在一个客户现场部署 Cromwell,那环境真叫一个干净——连 Java 都没有。用 Docker 部署,五分钟搞定了所有环境问题。下面我把具体的步骤和踩过的坑都写出来。

Docker 部署的三种方式

根据团队习惯和环境条件,我通常推荐三种方式。你可以根据实际情况选一种。

部署方式 适合场景 优点 缺点
docker run 快速验证、开发测试 简单直接,一条命令搞定 不方便管理和持久化
docker-compose 混合云部署、多服务编排 支持 MySQL、数据库持久化 需要额外编写 YAML 文件
容器编排(K8s) 生产环境、大规模集群 高可用、自动扩展 学习成本高,配置复杂

准备工作:拉取镜像

我习惯先确认一下镜像版本。Cromwell 官方在 Docker Hub 上维护了镜像,你也可以自己 build。但说真的,用官方镜像更稳妥。

# 拉取最新稳定版
docker pull broadinstitute/cromwell:latest

# 查看本地已有镜像
docker images | grep cromwell

小经验:我个人习惯把镜像版本号固定下来,比如 broadinstitute/cromwell:85。用 latest 在开发环境没问题,但生产环境一旦镜像更新,行为可能变化,容易出问题。

方法一:直接 docker run

这是最快的验证方式。你不用管数据库、不用管配置,Cromwell 默认用内存数据库。说白了,就是启动一个单机版服务,适合本地调试。

# 启动 Cromwell 服务
docker run --name cromwell-server \
    -p 8000:8000 \
    broadinstitute/cromwell:85 \
    server

跑起来之后,你可以用 curl 测试一下是否正常:

curl http://localhost:8000/swagger

如果能返回 Swagger 接口文档页面,嗯,恭喜你,部署成功了。

注意:这种方法启动的服务不会持久化数据。容器一删,所有 workflow 记录都没了。我只建议在「尝鲜」和「写 Demo」的时候用。

方法二:docker-compose 生产化部署

如果要在多平台迁移的场景下用,我强烈推荐 docker-compose。它可以把 Cromwell、MySQL、数据库初始化脚本全部编排在一起,一步到位。

下面是我项目中常用的 docker-compose.yml 配置文件。我自己做了简化,主要去掉了冗余的卷映射,保留了最关键的部分。

version: "3.8"

services:
  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: cromwell_root
      MYSQL_DATABASE: cromwell_db
    volumes:
      - cromwell_mysql_data:/var/lib/mysql
    ports:
      - "3306:3306"

  cromwell:
    image: broadinstitute/cromwell:85
    ports:
      - "8000:8000"
    environment:
      - CROMWELL_DB_URL=jdbc:mysql://mysql:3306/cromwell_db?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
      - CROMWELL_DB_USER=root
      - CROMWELL_DB_PASS=cromwell_root
    depends_on:
      - mysql
    volumes:
      - ./cromwell.conf:/cromwell/cromwell.conf

volumes:
  cromwell_mysql_data:

这里要注意,Cromwell 的数据库连接信息需要通过环境变量传入。我踩过一个大坑:MySQL 8.0 以上版本默认使用 caching_sha2_password 加密方式,Cromwell 的 JDBC 驱动可能不兼容。解决方法是在连接串上加 allowPublicKeyRetrieval=true。

配置 Cromwell 数据库连接

我们还需要一个 cromwell.conf 配置文件。这个文件告诉 Cromwell 怎么连数据库。它与上面的 docker-compose.yml 在同一个目录下。

database {
  profile = "slick.jdbc.MySQLProfile$"
  db {
    driver = "com.mysql.cj.jdbc.Driver"
    url = ${CROMWELL_DB_URL}
    user = ${CROMWELL_DB_USER}
    password = ${CROMWELL_DB_PASS}
    connectionTimeout = 5000
  }
}

restart {
  strategy = "fixedDelay"
  delay = 30
}

你看,这里用了 ${CROMWELL_DB_URL} 这种变量引用方式。Cromwell 会从环境变量中读取这些值。这样做的好处是——配置文件和环境解耦,不同环境只需要改环境变量就行。

启动并验证

一切准备好之后,启动服务:

# 后台启动所有服务
docker-compose up -d

# 查看启动日志,确认没有报错
docker-compose logs -f cromwell

# 检查服务状态
docker-compose ps

看到服务状态都是 Up 之后,再验证 API:

curl -X POST http://localhost:8000/api/workflows/v1 \
    -H "Content-Type: application/json" \
    -d '{
        "workflowSource": "task hello { command { echo hello } }",
        "workflowInputs": {}
    }'

避坑指南:我曾经遇到一个情况——Cromwell 启动成功了,但提交 workflow 一直报 500 错误。查了半天,发现是 MySQL 的时区问题。Cromwell 要求数据库的 time_zone 设置为 UTC,不然会有一些说不清的时间转换异常。解决方法是在 MySQL 服务配置里加上 default-time-zone='+00:00'。

Docker 环境常见问题排查清单

我整理了一张排查清单,这些都是在实际项目中频繁出现的问题:

  • 端口映射冲突:8000 端口被占用。改用 -p 18000:8000 映射不同的宿主机端口。
  • 容器重启失败:检查 MySQL 服务是否先于 Cromwell 启动。可以通过 docker-compose logs 看启动时序。
  • 数据丢失:记得把 MySQL 数据卷映射到宿主机路径,否则容器删除后数据全部丢失。
  • Java 版本不兼容:官方镜像内部已经带好了 Java,如果自己构建镜像,务必要用 Java 11 或 17。

Docker 日志与容器管理

日常运维中,常用的命令如下。我习惯把常用的几个记成小本本:

# 查看实时日志
docker logs -f cromwell-server

# 进入容器内部
docker exec -it cromwell-server bash

# 拷贝配置文件到容器
docker cp ./cromwell.conf cromwell-server:/cromwell/

# 停止并删除容器
docker-compose down

# 清理所有未使用的数据卷(谨慎使用)
docker volume prune

这里我要提醒一下——docker volume prune 会把没有被使用的数据卷全部删掉。如果你用 docker-compose down -v 也会一起删掉数据卷。数据没了,哭都来不及。所以,生产环境一定要把数据卷映射到宿主机持久目录。

部署流程概览

我把整个部署流程总结成了下面这张图,你可以照着这个顺序一步一步来:

Cromwell Docker 部署流程 1. 拉取镜像 2. 编写配置 3. 启动服务 4. 验证 API MySQL 数据库(数据持久化) Cromwell 后端服务 用户通过 HTTP API 提交 WDL 工作流 / 查询状态 返回 API 响应 端口: 8000

几点额外提醒

嗯,内容基本说完了。但基于我在多个项目中折腾 Docker 的经验,再补充几条:

  • 日志清理:Cromwell 日志会随时间膨胀。尤其是 workflow 执行失败时,堆栈信息特别长。建议在 docker-compose.yml 中加入日志轮转配置,或者用 logrotate 定时清理。
  • 健康检查:我用 docker-compose 的时候,会在 Cromwell 服务中加入 healthcheck 小节,定期调用 /api/swagger 端点。这样能第一时间知道服务是否还活着。
  • 资源限制:Cromwell 默认会吃不少内存。我建议在 docker-compose.yml 里加上 mem_limit: 4g,防止它把宿主机撑爆。