容器化技术基础:Docker 与 Singularity

说到容器化,很多人第一反应就是“轻量级虚拟机”。其实没那么复杂。容器就是给进程造一个独立的小隔间,让它以为自己独占整个操作系统。我在做单细胞分析时遇到过不少环境冲突的坑——Python 版本对不上、R 包装不上、某个 C 库版本不对……折腾一整天是常有的事。后来用容器,这些问题基本上一劳永逸地解决了。

你想想看,Nextflow 本身就能对接多种容器引擎。我们做单细胞流程,最常用的两个就是 Docker 和 Singularity。为什么是两个?说白了,Docker 适合本地开发和测试,Singularity 更受 HPC(高性能计算)集群的青睐。我自己的习惯是:笔记本上跑 Docker,上超算就切 Singularity。

2.1 Docker:开发者的瑞士军刀

Docker 的核心概念就三个:镜像(Image)、容器(Container)、仓库(Registry)。

  • 镜像:一个只读的模板,包含完整的运行环境。比如你想跑 Seurat v4,就把 R 4.1、Seurat 4.0、所有依赖都装进一个镜像。
  • 容器:镜像的运行实例。你可以开一个、停一个、删一个,不影响镜像本身。
  • 仓库:存放镜像的地方。Docker Hub 是最常用的公开仓库,机构内部也可以用私有仓库。

我用个实际例子说明 Dockerfile 怎么写。这是我们单细胞项目中常用的一个基础镜像:

FROM rocker/r-ver:4.2.0
RUN apt-get update && apt-get install -y \
    libhdf5-dev \
    libcurl4-openssl-dev \
    libssl-dev \
    libxml2-dev
RUN R -e "install.packages(c('Seurat','dplyr','ggplot2'), repos='https://cloud.r-project.org')"
ENV PATH /usr/local/bin:$PATH
WORKDIR /workspace

这段 Dockerfile 干了三件事:基于官方 R 镜像,装系统依赖,装 R 包。最后设置工作目录。嗯,这里要注意:WORKDIR 后面要跟你的分析目录,否则容器启动后默认在根目录下,找文件很麻烦。

构建镜像只需要一行命令:

docker build -t sc-rna-seurat:1.0 .

运行容器时,把数据目录挂载进去:

docker run --rm -v /path/to/data:/workspace sc-rna-seurat:1.0 Rscript analysis.R

-v 参数把宿主机的 /path/to/data 挂载到容器内的 /workspace。这样容器就能读写你的数据文件了。我一开始总忘记挂载目录,结果跑出来的结果全丢了,后来就写成一个模板脚本,每次都复用。

小技巧: Docker 镜像层是缓存友好的。把经常变化的安装命令放在 Dockerfile 靠后的位置,这样改代码时不会重新安装前面的依赖,构建速度快很多。

2.2 Singularity:HPC 上的标配

Docker 虽然好用,但在超算上经常碰壁——权限问题、安全策略限制、用户没有 root 权限。Singularity 就是为 HPC 设计的。它最大的特点是:不需要 daemon 进程,以非 root 用户身份运行,天然支持共享文件系统。

我曾经在某个超算中心提交作业时,管理员直接说“我们这里只跑 Singularity,Docker 不行”。那时候我手头只有 Docker 镜像,怎么办呢?Singularity 可以直接从 Docker 镜像构建:

singularity build sc-rna-seurat.sif docker://rocker/r-ver:4.2.0

这就把一个 Docker 镜像转换成了 Singularity 的 .sif 文件。sif 是一个单文件镜像,方便在集群间移动。你想想看,一个文件就装了整个环境,复制到 /scratch 或者 lustre 文件系统上,都很快。

Singularity 的容器定义文件叫 .def,类似 Dockerfile:

Bootstrap: docker
From: rocker/r-ver:4.2.0

%post
    apt-get update && apt-get install -y \
        libhdf5-dev \
        libcurl4-openssl-dev \
        libssl-dev \
        libxml2-dev
    R -e "install.packages(c('Seurat','dplyr','ggplot2'), repos='https://cloud.r-project.org')"

%environment
    export PATH=/usr/local/bin:$PATH

%runscript
    exec Rscript "$1"

构建命令:

singularity build sc-rna-seurat.sif Singularity.def

运行方式和 Docker 类似,但不需要 -v 挂载——Singularity 默认就把宿主机的文件系统暴露给容器了。

注意: Singularity 默认不允许动态链接库的路径覆盖,如果你遇到“library not found”,可以在运行前设置 SINGULARITY_BINDPATH 环境变量来强制绑定目录。我曾经因为这个踩过坑,容器内读不到我的 conda 环境里的 libssl.so,折腾了大半天。

2.3 核心差异对比

为了让你快速决策,我把两个引擎的关键差异列成表格:

特性 Docker Singularity
运行模式 Client-Daemon 架构,需 root 权限启动 daemon 用户态直接运行,无需 daemon
镜像格式 分层 tar 归档(registry 或本地存储) 单文件 .sif(只读 squashfs)
文件系统挂载 需显式 -v 或 --mount 默认挂载宿主机所有路径,可限制
GPU 支持 需 --gpus all,依赖 nvidia-docker 原生支持 GPU,只需 --nv 标志
网络隔离 默认网络隔离,可配置 使用宿主网络,不隔离
安全模型 默认以 root 身份运行容器(需降权) 以当前用户身份运行,无法提权
HPC 兼容性 差(权限限制,共享文件系统支持弱) 好(常见排队系统直接支持)
仓库互转 可直接导出为 Singularity 镜像 支持从 Docker Hub 直接构建

从表格能看出来,本地开发和测试首选 Docker,因为它生态丰富,文档最全,调试方便。生产级 HPC 部署必须用 Singularity,否则管理员大概率直接打回。

我的结论: 你只需要一套 Docker 镜像定义(Dockerfile),然后同时在本地用 Docker 跑,在集群上用 Singularity 从那个 Docker 镜像构建 .sif 文件。Nextflow 的配置文件里可以轻松切换 container engine,后面章节会详细讲。

2.4 容器化在单细胞流程中的典型用法

单细胞分析流程通常包含多个工具:Cell Ranger、STAR、samtools、Seurat、Scanpy、scVI……每个工具的环境都可能互相冲突。容器化以后,每个工具跑在自己的盒子里,互不干扰。

举个 Nextflow 的伪代码例子:

process SEURAT_CLUSTERING {
    container 'docker://hub/user/seurat:4.0'
    input:
        path counts
    output:
        path "clusters.rds"
    script:
    """
    Rscript cluster.R -i $counts -o clusters.rds
    """
}

这个 process 指定的 container 就是 Docker 镜像地址。Nextflow 会自动拉取镜像并在容器里运行。如果你的集群只支持 Singularity,那就在 nextflow.config 里加一句:

singularity.enabled = true
process.container = 'shub://user/seurat:4.0'

或者直接用 Docker 镜像地址,Nextflow 会自动把它转成 Singularity 格式。我个人觉得这个自动转换功能特别省事。

2.5 避坑指南

最后分享几个我踩过的坑:

  • 路径权限问题: Docker 默认以 root 运行,生成的文件属于 root。你在宿主机上删不掉。记得在 Dockerfile 里用 RUN useradd -m user 并切换用户,或者运行容器时加 --user $(id -u):$(id -g)。
  • GPU 加速: 运行单细胞数据时,scVI、Cell Bender 等工具依赖 GPU。Docker 需要额外安装 nvidia-docker 运行时;Singularity 只需要 --nv。我建议能上 GPU 的流程优先用 Singularity 部署。
  • 镜像大小: 单细胞环境往往要装很多 Python 和 R 包,镜像轻松超过 5GB。合理利用多阶段构建(multi-stage build)可以大幅缩减体积。比如只保留运行时的包,构建时的编译工具可以扔掉。
  • 网络代理: 在超算上,很多节点不能访问外网。提前把 Docker 镜像拉到本地私仓,或者用 Singularity 在可以上网的登录节点上 build 好 .sif 文件,再传到计算节点。
推荐做法: 建立一个内部镜像仓库(比如 Harbor 或 GitLab Container Registry),所有单细胞流程的容器镜像都统一管理。版本号用 semver(比如 1.2.3),方便回退。我带的团队因为没做版本管理,有一次升级 Seurat 后流程跑出来的结果对不上,查了一天才发现是容器缓存了旧版本。
容器化技术路线图:Docker ↔ Singularity Docker 环境 Dockerfile → 构建 → 镜像 运行容器(docker run) 推送到 Docker Hub / 私有仓库 本地开发,GPU 需 nvidia-docker Singularity 环境 直接从 Docker 构建 .sif 或通过 .def 文件构建 运行容器(singularity exec) HPC 默认,GPU 直接 --nv singularity build docker pull / push Nextflow 流程统一调度 nextflow.config 中指定 container engine 自动拉取、转换、缓存镜像 每个 process 可指定不同容器 支持 Docker / Singularity / Podman / Shifter

这张图概括了整章的核心逻辑:你在本地用 Docker 开发,然后把 Docker 镜像直接丢给 Singularity 构建 .sif,最终都交给 Nextflow 调度。实际项目中,我习惯在 GitLab CI 里自动构建 Docker 镜像,测试通过后再推送到私仓,然后 HPC 集群那边用配置好的 Nextflow pipeline 直接拉取。

记住一个原则:镜像定义是代码,镜像本身是制品,不要手动修改运行中的容器。所有环境变更都改 Dockerfile,重新构建。这样你的流程才能可复现、可审计。单细胞分析最怕的就是“昨天还能跑,今天报错”,容器化就是把这个风险降到最低。