Creating and running software containers with Singularity

How to use Singularity!

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:

Building and Running Containers

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.

Building a basic container

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

Blurring the line between the container and the host system.

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!

Bind mounting host system directories into a container

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:

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.

Singularity Hub and Docker Hub

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.