This is an introductory tutorial taught by the staff at the NIH HPC. For more information about the topics covered in this class, see the following:
Simply typing singularity
will give you an summary of all the commands you can use. Typing singularity help <command>
will give you more detailed information about running an individual command.
To build a singularity container, you must issue 2 commands. First you must create
an empty container. Then you must bootstrap
an OS and any apps into the empty container.
First, let's create an empty Singularity container.
$ cd ~
$ singularity create lolcow.img
By default, singularity creates a container with 768MB in size. You can change this default with the --size
option.
Now that we have an empty singularity container, we need to use bootstrap
to install an OS. To use the bootstrap command, we need a definition file. A definition file is like a set of blueprints telling Singularity what software to install in the container.
The Singularity source code contains several example definition files. You can browse the examples from the current release (v2.3.1) at https://github.com/singularityware/singularity/tree/2.3.1/examples. Let's download the debian example to our home directory and inspect it.
$ wget https://raw.githubusercontent.com/singularityware/singularity/2.3.1/examples/debian/Singularity
$ cat Singularity
It should look something like this:
BootStrap: debootstrap
OSVersion: stable
MirrorURL: http://ftp.us.debian.org/debian/
%runscript
echo "This is what happens when you run the container..."
%post
echo "Hello from inside the container"
apt-get update
apt-get -y --force-yes install vim
See the Singularity docs for an explanation of each of these sections.
Let's update our definition file to make it a little more interesting.
$ nano Singularity
Here is what our updated definition file should look like.
BootStrap: debootstrap
OSVersion: stable
MirrorURL: http://ftp.us.debian.org/debian/
%runscript
echo "This is what happens when you run the container..."
%post
apt-get update
apt-get -y install fortune cowsay lolcat
apt-get clean
%environment
export PATH="$PATH:/usr/games"
export LC_ALL=C
Now let's use the definition file to bootstrap our lolcow.img
container. Note that the bootstrap command requires sudo
privileges.
$ sudo singularity bootstrap lolcow.img Singularity
This should take a few minutes while all of the components of an OS are downloaded and installed. When the bootstrap finishes, you will have a Debian container.
Now let's enter our new container and look around.
$ singularity shell lolcow.img
Depending on the environment on your host system you may see your prompt change. Let's look at what OS is running inside the container.
$ cat /etc/os-release
PRETTY_NAME="Debian GNU/Linux 9 (stretch)"
NAME="Debian GNU/Linux"
VERSION_ID="9"
VERSION="9 (stretch)"
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"
No matter what OS is running on your host, your container is running Debian 9!
Now try running the programs we installed in the container, then get back to the host environment.
$ fortune | cowsay | lolcat
________________________________________
/ "You have been in Afghanistan, I \
| perceive." |
| |
| -- Sir Arthur Conan Doyle, "A Study in |
\ Scarlet" /
----------------------------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
$ exit
Singularity does not try to isolate your container completely from the host system. This allows you to do some interesting things.
Using the exec command, we can run commands within the container from the host system.
$ singularity exec lolcow.img cowsay 'How did you get out of the container?'
_______________________________________
< How did you get out of the container? >
---------------------------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
In this example, singularity entered the container, ran the cowsay
command, and then displayed the standard output on our host system terminal.
You can also use pipes and redirection to blur the lines between the container and the host system.
We are going to consider an extended example describing a program that takes a file as input, analyzes the data in the file, and produces another file as output. This is obviously a very common situation.
Let's imagine that we want to use the cowsay program in our lolcow.img to "analyze data". We should give our container an input file, it should reformat it (in the form of a cow speaking), and it should dump the output into another file.
Here's an example. First I'll make some "data" using the fortune
program installed in the container
$ singularity exec lolcow.img fortune > input
Now I'll "analyze" the "data"
$ cat input | singularity exec lolcow.img cowsay > output
$ cat output
______________________________________
/ The grass is always greener over the \
\ septic tank /
--------------------------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
This works but the syntax is ugly and difficult to remember. One neat singularity trick is to make a container function as though it were an executable. To do that, we need to create a runscript inside the container. It turns out that our lolcat.def file already builds a runscript into our container for us...
$ ./lolcow.img
This is what happens when you run the container...
Let's rewrite this runscript in the container so that it does something more useful.
$ sudo singularity shell --writable lolcow.img
$ apt-get -y install nano
$ nano /.singularity.d/runscript
change the runscript so that it looks as follows:
#!/bin/sh
if [ $# -ne 2 ]; then
echo "Please provide an input and an output file."
exit 1
fi
cowsay < $1 > $2
then exit the container
$ exit
Now we can call the lolcow.img as though it were an executable, and simply give it two arguments: one for input and one for output.
$ ./lolcow.img input output2
$ cat output2
______________________________________
/ The grass is always greener over the \
\ septic tank /
--------------------------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
Now our container behaves as though it's an executable that expects 2 arguments!
It's possible to create and modify files on the host system from within the container. In fact, that's exactly what we did in the previous example when we created output files in our home directory.
There are several special directories that Singularity bind mounts into your container by default. These include:
/home/$USER
/tmp
/proc
/sys
/dev
You can specify other directories to bind to using the --bind
option or the environment variable $SINGULARITY_BINDPATH
Let's say we want to use our cowsay.img
container to "analyze data" and save results in a different directory. For this example, we first need to create a new directory with some data on our host system.
$ mkdir data
$ echo 'I am your father' > data/vader.sez
The filesystem hierarchy by default includes an empty directory for use as a mount point. It's called /mnt. Now let's see how bind mounts work. First, let's list the contents of /mnt
within the container without bind mounting.
$ singularity exec lolcow.img ls -l /mnt
total 0
The /mnt
directory within the container is empty, as expected. Now let's repeat the same command after bind mounting our data folder to it.
$ singularity exec --bind $PWD/data:/mnt lolcow.img ls /mnt
vader.sez
Now the /mnt
directory in the container is bind mounted to the data
subdirectory of your home directory on the host system and we can see its contents in that location. Typically, you will use this feature to mount directory trees not already visible by default.
Now what about our earlier example in which we used a runscript to run a our container as though it were an executable? The singularity run
command accepts the --bind
option and can execute our runscript like so.
$ singularity run --bind $PWD/data:/mnt lolcow.img /mnt/vader.sez /mnt/output3
$ cat data/output3
__________________
< I am your father >
------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
But that's a cumbersome command. Instead, we could set the variable $SINGULARITY_BINDPATH
and then use our container as before.
$ export SINGULARITY_BINDPATH="$PWD/data:/mnt"
$ ./lolcow.img /mnt/output3 /mnt/metacow2
$ ls data
metacow2
output3
vader.sez
$ cat data/metacow2
________________________________________
/ __________________ < I am your father \
| > |
| |
| ------------------ |
| |
| \ ^__^ |
| |
| \ (oo)\_______ |
| |
| (__)\ )\/\ |
| |
| ||----w | |
| |
\ || || /
----------------------------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
For a lot more info on how to bind mount host directories to your container, check out the NIH HPC Binding external directories section.
We've spent a lot of time on building and using your own containers to demonstrate how Singularity works. But there's an easier way! Docker Hub hosts over 100,000 pre-built, ready-to-use containers. And singularity makes it easy to use them.
A container similar to the one we built here is available on Singularity Hub. You can download it like so:
$ singularity pull shub://0xaf1f/lolcow
$ ls
$ ./0xaf1f-lolcow-master.img
You can build and host your own images on Docker Hub, (using docker) or you can download and run images that others have built.
$ singularity shell docker://tensorflow/tensorflow:latest-gpu
python
import tensorflow as tf
quit()
exit
You can even use images on Docker Hub as a starting point for your own images. definition files allow you to specifiy a Docker Hub registry to bootstrap from and you can use the %post
section to modify the container to your liking.
If you want to host your containers, but you don't want to learn how to write Docker files (definition files for Docker), you can use Singularity Hub to build and host container images.
Both Docker Hub and Singularity Hub link to your GitHub account. New container builds are automatically triggered every time you push changes to a Docker file or a Singularity definition file in a linked repository.