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
这样一看就知道这个镜像里装了什么,版本是多少。
oras:// 或者 library://,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
如果这些命令都能顺利跑通,那恭喜你,镜像大概率没毛病。
%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 就能跑分析”。他一脸惊讶,但试了一次就喊“真香”。
%runscript,那 singularity run 默认会执行 /bin/bash --login,相当于又回到了 shell。这有点反直觉,所以我建议每个生产镜像都写明 runscript。
3.6 一张图理清五个命令的关系
上面讲了五个命令,可能有点杂。我画了一张流程对比图,你看完应该就能串起来了。
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 来清除环境变量。这两个细节在后期自动化脚本里特别容易踩坑。