3. Singularity 基础命令:pull、build、shell、exec、run

好,我们开始动手。玩转 Singularity,你得先熟练掌握五个核心命令。我个人觉得,这五个命令就像工具箱里的扳手、螺丝刀和锤子——各有各的用处,组合起来就能搞定 90% 的容器操作。

这一章咱们就把它们挨个捋一遍。我保证,你学完之后,日常的拉取、构建、调试和运行,基本不会卡壳。

3.1 pull:把远程镜像“拽”到本地

pull 的作用很简单:从远程仓库(比如 Docker Hub 或 Sylabs Cloud)下载一个已构建好的镜像。说白了,就是“拿来用”。

我记得第一次用 Singularity 时,第一件事就是去拉一个 Ubuntu 镜像。当时还纳闷,这玩意儿跟 Docker pull 有啥区别?后来发现,Singularity 的 pull 会自动把 Docker 的层(layer)压成一个单一的 .sif 文件,干净利落。

基本用法:

singularity pull ubuntu.sif docker://ubuntu:22.04

这条命令会从 Docker Hub 拉取 Ubuntu 22.04,然后保存到本地,文件名叫 ubuntu.sif。一个 .sif 文件就是一个完整的容器,你可以直接拷到别的机器上用,不需要联网。

我建议你养成给镜像文件起名的好习惯,别偷懒用默认名。比如:

singularity pull visium_env.sif docker://username/visium_env:latest

这样一看就知道这个镜像里装了什么,版本是多少。

小技巧: 很多公共仓库的镜像都支持直接拉取。如果你团队内部有私仓,只需要在 URI 前加 oras:// 或者 library://,Singularity 也能优雅处理。
注意: 如果你拉的是 Docker 镜像,而本地磁盘空间不够,Singularity 会在临时目录(/tmp)解压缓存,可能撑爆系统盘。养成好习惯:用 SINGULARITY_TMPDIR 环境变量指定一个空间充足的分区。

3.2 build:从零构筑你的专属镜像

build 才是真正彰显工匠精神的地方。pull 只是拿来主义,而 build 让你从一份定义文件(Singularity Definition File)出发,自定义软件、库、环境变量,甚至系统依赖。

我个人更倾向于用 build 来创建生产镜像。为什么呢?因为定义文件可复现、可审计、可版本控制。你想想看,如果半年后有人问你“这个Visium分析环境怎么配的”,你甩给他一份 .def 文件,一切清清楚楚。

构建一个镜像,大概长这样:

singularity build my_visium.sif my_visium.def

如果 .def 文件里写的是从 Docker 继承,那 build 底层其实会先 pull 再 layer 上去,最后打包成 .sif。

另外,你还可以用 --sandbox 选项构建一个沙箱目录,这样便于调试:

singularity build --sandbox my_visium_sandbox/ my_visium.def

沙箱模式创建的不是 .sif 文件,而是一个文件夹。你可以像操作普通文件系统一样进出它、修改它。我曾经在一个古老的服务器上调试Visium环境,就是因为系统 glibc 版本太低,导致直接 build .sif 一直失败。换成沙箱模式后,我一步步手动装依赖,最后才找到问题——原来是默认的 centos 7 基础镜像里缺了 libssl。这事我印象很深。

核心区别总结:
  • pull:从远程获取现成镜像
  • build:从定义文件或本地源构建镜像
  • build --sandbox:构建可写的目录,适合调试

3.3 shell:钻进容器内部,交互式操作

shell 让你像 SSH 进一台远程机器一样,进入容器内部。这是调试阶段的神器。你可以在里面乱跑、装包、改脚本,甚至把容器搞坏——反正没人管你。

singularity shell my_visium.sif

进入之后,你会看到提示符变了,但工作目录($PWD)和家目录($HOME)默认还是宿主机的。这是 Singularity 安全设计之一:不会像 Docker 那样默认给你一个隔离得严严实实的根文件系统,而是“开放但不越界”。

我个人习惯在 shell 之后立刻执行一两条命令,确认环境能用:

singularity shell my_visium.sif
Singularity> which python
Singularity> python --version
Singularity> pip list | grep visium

如果这些命令都能顺利跑通,那恭喜你,镜像大概率没毛病。

避坑指南: 进入 shell 后,有些用户发现环境变量没生效,或者 PATH 不对。别慌,检查一下你的 .def 文件里 %environment 段是不是写对了,或者用 --cleanenv 参数取消自动加载宿主机环境。

3.4 exec:在容器里执行单条命令

exec 是 shell 的“精简版”。你不需要进入交互 shell,直接扔一条命令给容器,跑完就退出。这在自动化脚本和 HPC 作业提交时特别有用。

举个例子,我想在容器里跑 Space Ranger 的 spaceranger count,但不想手动敲三行命令:

singularity exec my_visium.sif spaceranger count --id=sample1 --transcriptome=refdata --fastqs=fastq_path --sample=s1

一条命令搞定。输出的结果直接落在宿主机当前目录下,无需额外拷贝。

你还可以用 --bind 参数挂载外部数据目录,让容器访问宿主机文件:

singularity exec --bind /home/user/data:/data my_visium.sif spaceranger count --id=s1 --transcriptome=/data/refdata --fastqs=/data/fastq --sample=s1

这里 /home/user/data 是宿主机目录,/data 是容器内的挂载点。这样你的原始测序数据就不需要拷进镜像了,既节省空间又安全。

说白了,exec 就是一套组合拳:挂载 + 执行。你可以在脚本里把它写到循环里,批量处理几十个样本,效率极高。

3.5 run:定义默认的启动动作

run 执行的是容器内定义好的默认命令。这个默认命令是在构建时通过 %runscript 段指定的。比如你的 Visium 分析流程是 spaceranger count,那你可以在 .def 里写:

%runscript
    exec spaceranger count "$@"

之后,用户只要执行:

singularity run my_visium.sif --id=s1 --transcriptome=/ref --fastqs=/data --sample=s1

效果等价于手动敲 singularity exec my_visium.sif spaceranger count --id=...。但 run 更简洁、更符合直觉。用户不需要知道容器内部的软件叫什么名字、放在哪个路径,只需运行容器本身就行。

我记得有一次帮同事部署分析环境,对方是个纯生信背景的博士,对容器完全没概念。我直接把 .sif 文件和一个跑脚本扔给他,告诉他“双击这个 .sif 就能跑分析”。他一脸惊讶,但试了一次就喊“真香”。

注意: 如果 .def 里没有定义 %runscript,那 singularity run 默认会执行 /bin/bash --login,相当于又回到了 shell。这有点反直觉,所以我建议每个生产镜像都写明 runscript。

3.6 一张图理清五个命令的关系

上面讲了五个命令,可能有点杂。我画了一张流程对比图,你看完应该就能串起来了。

Singularity 核心命令关系图 Singularity 核心命令关系与流程 远程镜像源 Docker Hub / Sylabs Cloud pull Singularity 定义文件 .def(配方文件) build 本地镜像 .sif / 沙箱目录 shell 交互式进入 exec 单条命令执行 run 默认启动动作 快速对照 pull → 拿来主义 build → 自定义构建 shell → 钻进去调试 exec → 跑单条命令 run → 一键启动流程 注意:run 依赖 %runscript, 否则退化为 shell。 建议生产镜像必配 rundef。

3.7 五命令对照速查表

最后,我把这五个命令的关键属性做成了一张表。你可以把它当成“备忘录”,用到时直接查。

命令 核心用途 输入 输出 是否侵入性 常见场景
pull 从远程获取镜像 URI(docker://, library://) .sif 文件 只读 部署现成的公开镜像
build 从定义文件构建镜像 .def 文件或沙箱目录 .sif 文件 / 沙箱 只读(生产)或可写(沙箱) 定制Visium环境
shell 交互式进入容器 .sif 或沙箱路径 交互终端 读/写(沙箱内) 调试、手动安装包
exec 在容器内执行单命令 .sif + 命令 + 参数 命令 stdout/stderr 只读 脚本批量处理、HPC作业
run 执行容器默认行为 .sif + 参数(传给%runscript) runscript 的输出 只读 一键启动分析流水线

说到这张表,我想补一句:shell 和 exec 的单字母参数经常被混淆。shell 支持 -s 指定 shell 类型(比如 -s /bin/zsh),而 exec 有 --cleanenv 来清除环境变量。这两个细节在后期自动化脚本里特别容易踩坑。