容器化技术基础: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。这样容器就能读写你的数据文件了。我一开始总忘记挂载目录,结果跑出来的结果全丢了,后来就写成一个模板脚本,每次都复用。
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_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 文件,再传到计算节点。
这张图概括了整章的核心逻辑:你在本地用 Docker 开发,然后把 Docker 镜像直接丢给 Singularity 构建 .sif,最终都交给 Nextflow 调度。实际项目中,我习惯在 GitLab CI 里自动构建 Docker 镜像,测试通过后再推送到私仓,然后 HPC 集群那边用配置好的 Nextflow pipeline 直接拉取。
记住一个原则:镜像定义是代码,镜像本身是制品,不要手动修改运行中的容器。所有环境变更都改 Dockerfile,重新构建。这样你的流程才能可复现、可审计。单细胞分析最怕的就是“昨天还能跑,今天报错”,容器化就是把这个风险降到最低。