A Singularity Recipe is the driver of a custom build, and the starting point for designing any custom container. It includes specifics about installation software, environment variables, files to add, and container metadata. You can even write a help section, or define modular components in the container called based on the Standard Container Integration Format (SCI-F).

Overview

A Singularity Recipe file is divided into several parts:

  1. Header: The Header describes the core operating system to build within the container. Here you will configure the base operating system features that you need within your container. Examples of this include, what distribution of Linux, what version, what packages must be part of a core install.
  2. Sections: The rest of the definition is comprised of sections, sometimes called scriptlets or blobs of data. Each section is defined by a % character followed by the name of the particular section. All sections are optional.

Please see the examples directory in the Singularity source code for some ideas on how to get started.

The header is at the top of the file, and tells Singularity the base Operating System that it should use to build the container. It is composed of several keywords. Specifically:

  • Bootstrap: references the kind of base you want to use (e.g., docker, debootstrap, shub). For example, a shub bootstrap will pull containers for shub as bases. A Docker bootstrap will pull docker layers to start your image. For a full list see build
  • From: is the named container (shub) or reference to layers (Docker) that you want to use (e.g., vsoch/hello-world)

Depending on the value assigned to Bootstrap:, other keywords may also be valid in the header.

For example, a very minimal Singularity Hub build might look like this:

Bootstrap: shub
From: vsoch/hello-world

A build that uses a mirror to install Centos-7 might look like this:

Bootstrap: yum
OSVersion: 7
MirrorURL: http://mirror.centos.org/centos-%{OSVERSION}/%{OSVERSION}/os/$basearch/
Include: yum

Each build base requires particular details during build time. You can read about them and see examples at the following links:

  • shub (images hosted on Singularity Hub)
  • docker (images hosted on Docker Hub)
  • localimage (images saved on your machine)
  • yum (yum based systems such as CentOS and Scientific Linux)
  • debootstrap (apt based systems such as Debian and Ubuntu)
  • arch (Arch Linux)
  • busybox (BusyBox)
  • zypper (zypper based systems such as Suse and OpenSuse)

Sections

The main content of the bootstrap file is broken into sections. Different sections add different content or execute commands at different times during the build process. Note that if any command fails, the build process will halt.

Let’s add each section to our container to see how it works. For each section, we will build the container from the recipe (a file called Singularity) as follows:

$ sudo singularity build roar.simg Singularity

%help

You don’t need to do much programming to add a %help section to your container. Just write it into a section:

Bootstrap: docker
From: ubuntu

%help
Help me. I'm in the container.

And it will work when the user asks the container for help.

$ singularity help roar.simg 

Help me. I'm in the container.

%setup

Commands in the %setup section are executed on the host system outside of the container after the base OS has been installed. For versions earlier than 2.3, or if you need files during %post, you should copy files from your host to $SINGULARITY_ROOTFS to move them into the container. For >2.3 and cases when you don’t need the files until after %post, we recommend you use %files section. We can see the difference between %setup and %post in the following asciicast:

In the above, we see that copying something to $SINGULARITY_ROOTFS during %setup was successful to move the file into the container, but copying during %post was not. Let’s add a setup to our current container, just writing a file to the root of the image:

Bootstrap: docker
From: ubuntu

%help
Help me. I'm in the container.

%setup
    touch ${SINGULARITY_ROOTFS}/tacos.txt
    touch avocados.txt

Importantly, notice that the avocados file isn’t relative to $SINGULARITY_ROOTFS, so we would expect it not to be in the image. Is tacos there?

$ singularity exec roar.simg ls /
bin   environment  lib	  mnt	root  scif	   sys	      usr
boot  etc	   lib64  opt	run   singularity  **tacos.txt**  var
dev   home	   media  proc	sbin  srv	   tmp

Yes! And avocados.txt isn’t inside the image, but in our present working directory:

$ ls
avocados.txt   roar.simg   Singularity

%files

If you want to copy files from your host system into the container, you should do so using the %files section. Each line is a pair of <source> and <destination>, where the source is a path on your host system, and the destination is a path in the container.

The %files section uses the traditional cp command, so the same conventions apply.

Files are copied after any %post or installation procedures, so if you need the copied files or directories to exist during %post you must do this via %setup. Let’s add the avocado.txt into the container, to join tacos.txt.

Bootstrap: docker
From: ubuntu

%help
Help me. I'm in the container.

%setup
    touch ${SINGULARITY_ROOTFS}/tacos.txt
    touch avocados.txt

%files
avocados.txt
avocados.txt /opt

Notice that I’m adding the same file to two different places. For the first, I’m adding the single file to the root of the image. For the second, I’m adding it to opt. Does it work?

$ singularity exec roar.simg ls /
 singularity exec roar.simg ls /
**avocados.txt**  dev	   home   media  proc  sbin	    srv        tmp
bin	      environment  lib	  mnt	 root  scif	    sys        usr
boot	      etc	   lib64  opt	 run   singularity  **tacos.txt**  var

$ singularity exec roar.simg ls /opt
**avocados.txt**

We have avocados!

%labels

To store metadata with your container, you can add them to the %labels section. They will be stored in the file /.singularity.d/labels.json as metadata within your container. The general format is a LABELNAME followed by a LABELVALUE. Labels from Docker bootstraps will be carried forward here. Let’s add to our example:

Bootstrap: docker
From: ubuntu

%help
Help me. I'm in the container.

%setup
    touch ${SINGULARITY_ROOTFS}/tacos.txt
    touch avocados.txt

%files
avocados.txt
avocados.txt /opt    

%labels
Maintainer Vanessasaurus
Version v1.0

The easiest way to see labels is to inspect the image:

$ singularity inspect roar.simg
{
    "org.label-schema.usage.singularity.deffile.bootstrap": "docker",
    "MAINTAINER": "Vanessasaurus",
    "org.label-schema.usage.singularity.deffile": "Singularity",
    "org.label-schema.usage": "/.singularity.d/runscript.help",
    "org.label-schema.schema-version": "1.0",
    "VERSION": "v1.0",
    "org.label-schema.usage.singularity.deffile.from": "ubuntu",
    "org.label-schema.build-date": "2017-10-02T17:00:23-07:00",
    "org.label-schema.usage.singularity.runscript.help": "/.singularity.d/runscript.help",
    "org.label-schema.usage.singularity.version": "2.3.9-development.g3dafa39",
    "org.label-schema.build-size": "1760MB"
}

You’ll notice some other labels that are captured automatically from the build process. You can read more about labels and metadata here.

%environment

As of Singularity 2.3, you can add environment variables to your Singularity Recipe in a section called %environment. Keep in mind that these environment variables are sourced at runtime and not at build time. This means that if you need the same variables during build time, you should also define them in your %post section. Specifically:

  • during build: the %environment section is written to a file in the container’s metadata folder. This file is not sourced.
  • during runtime: the file written to the container’s metadata folder is sourced.

Since the file is ultimately sourced, you should generally use the same conventions that you might use in a bashrc or profile. In the example below, the variables VADER LUKE and SOLO would not be available during build, but when the container is finished and run:

Bootstrap: docker
From: ubuntu

%help
Help me. I'm in the container.

%setup
    touch ${SINGULARITY_ROOTFS}/tacos.txt
    touch avocados.txt

%files
avocados.txt
avocados.txt /opt    

%labels
Maintainer Vanessasaurus
Version v1.0

%environment
    VADER=badguy
    LUKE=goodguy
    SOLO=someguy
    export VADER LUKE SOLO

For the rationale behind this approach and why we do not source the %environment section at build time, refer to this issue. When the container is finished, you can easily see environment variables also with inspect, and this is done by showing the file produced above:

$ singularity inspect -e roar.simg # Custom environment shell code should follow

    VADER=badguy
    LUKE=goodguy
    SOLO=someguy
    export VADER LUKE SOLO

or in the case of variables generated at build time, you can add environment variables to your container in the %post section (see below) using the following syntax:

%post
    echo 'export JAWA_SEZ=wutini' >> $SINGULARITY_ENVIRONMENT

When we rebuild, is it added to the environment?

singularity exec roar.simg env | grep JAWA
JAWA_SEZ=wutini

Where are all these environment variables going? Inside the container is a metadata folder located at /.singularity.d, and a subdirectory env for environment scripts that are sourced. Text in the %environment section is appended to a file called /.singularity.d/env/90-environment.sh. Text redirected to the $SINGULARITY_ENVIRONMENT variable will added to a file called /.singularity.d/env/91-environment.sh. At runtime, scripts in /.singularity/env are sourced in order. This means that variables in $SINGULARITY_ENVIRONMENT take precedence over those added via %environment. Note that you won’t see these variables in the inspect output, as inspect only shows the contents added from %environment.

See Environment and Metadata for more information about the %labels and %environment sections.

%post

Commands in the %post section are executed within the container after the base OS has been installed at build time. This is where the meat of your setup will live, including making directories, and installing software and libraries. We will jump from our simple use case to show a more realistic scientific container. Here we are installing yum, openMPI, and other dependencies for a Centos7 bootstrap:

%post
    echo "Installing Development Tools YUM group"
    yum -y groupinstall "Development Tools"
    echo "Installing OpenMPI into container..."

    # Here we are at the base, /, of the container
    git clone https://github.com/open-mpi/ompi.git

    # Now at /ompi
    cd ompi
    ./autogen.pl
    ./configure --prefix=/usr/local
    make
    make install

    /usr/local/bin/mpicc examples/ring_c.c -o /usr/bin/mpi_ring

You cannot copy files from the host to your container in this section, but you can of course download with commands like git clone and wget and curl.

%runscript

The %runscript is another scriptlet, but it does not get executed during bootstrapping. Instead it gets persisted within the container to a file (or symlink for later versions) called /singularity which is the execution driver when the container image is run (either via the singularity run command or via executing the container directly).

When the %runscript is executed, all options are passed along to the executing script at runtime, this means that you can (and should) manage argument processing from within your runscript. Here is an example of how to do that, adding to our work in progress:

Bootstrap: docker
From: ubuntu

%help
Help me. I'm in the container.

%setup
    touch ${SINGULARITY_ROOTFS}/tacos.txt
    touch avocados.txt

%files
avocados.txt
avocados.txt /opt    

%labels
Maintainer Vanessasaurus
Version v1.0

%environment
    VADER=badguy
    LUKE=goodguy
    SOLO=someguy
    export VADER LUKE SOLO


%post
    echo 'export JAWA_SEZ=wutini' >> $SINGULARITY_ENVIRONMENT

%runscript
    echo "Rooooar!"
    echo "Arguments received: $*"
    exec echo "$@"

In this particular runscript, the arguments are printed as a single string ($*) and then they are passed to echo via a quoted array ("$@") which ensures that all of the arguments are properly parsed by the executed command. Using the exec command is like handing off the calling process to the one in the container. The final command (the echo) replaces the current entry in the process table (which originally was the call to Singularity). This makes it so the runscript shell process ceases to exist, and the only process running inside this container is the called echo command. This could easily be another program like python, or an analysis script. Running it, it works as expected:

$ singularity run roar.simg 
Rooooar!
Arguments received: 

$ singularity run roar.simg one two
Rooooar!
Arguments received: one two
one two

%test

You may choose to add a %test section to your definition file. This section will be run at the very end of the build process and will give you a chance to validate the container during the bootstrap process. You can also execute this scriptlet through the container itself, such that you can always test the validity of the container itself as you transport it to different hosts. Extending on the above Open MPI %post, consider this real world example:

%test
    /usr/local/bin/mpirun --allow-run-as-root /usr/bin/mpi_test

This is a simple Open MPI test to ensure that the MPI is build properly and communicates between processes as it should.

If you want to build without running tests (for example, if the test needs to be done in a different environment), you can do so with the --notest argument:

$ sudo singularity build --notest mpirun.simg Singularity

This argument is useful in cases where you need hardware that is available during runtime, but is not available on the host that is building the image.

Apps

What if you want to build a single contianer with two or three different apps that each have thier own runscripts and custom environments? In some circumstances, it may be redundant to build different containers for each app with almost equivalent dependencies.

Starting in Singularity 2.4 all of the above commands can also be used in the context of internal modules called apps based on the Standard Container Integration Format. For details on apps, see the apps documentation. For a quick rundown of adding an app to your container, here is an example runscript:

Bootstrap: docker
From: ubuntu

%environment
    VADER=badguy
    LUKE=goodguy
    SOLO=someguy
    export VADER LUKE SOLO

%labels
   Maintainer Vanessasaur

##############################
# foo
##############################

%apprun foo
    exec echo "RUNNING FOO"

%applabels foo
   BESTAPP=FOO
   export BESTAPP

%appinstall foo
   touch foo.exec

%appenv foo
    SOFTWARE=foo
    export SOFTWARE

%apphelp foo
    This is the help for foo.

%appfiles foo
   avocados.txt


##############################
# bar
##############################

%apphelp bar
    This is the help for bar.

%applabels bar
   BESTAPP=BAR
   export BESTAPP

%appinstall bar
    touch bar.exec

%appenv bar
    SOFTWARE=bar
    export SOFTWARE

Importantly, note that the apps can exist alongside any and all of the primary sections (e.g. %post or %runscript), and the new %appinstall section is the equivalent of %post but for an app. The title sections (######) aren’t necessary or required, they are just comments to show you the different apps. The ordering isn’t important either, you can have any mixture of sections anywhere in the file after the header. The primary difference is now the container can perform any of it’s primary functions in the context of an app:

What apps are installed in the container?

$ singularity apps roar.simg 
bar
foo

Help me with bar!

$ singularity help --app bar roar.simg
This is the help for bar.

Run foo

singularity run --app foo roar.simg 
RUNNING FOO

Show me the custom environments Remember how we defined the same environment variable, SOFTWARE for each of foo and bar? We can execute a command to search the list of active environment variables with grep to see if the variable changes depending on the app we specify:

$ singularity exec --app foo roar.simg env | grep SOFTWARE
SOFTWARE=foo
$ singularity exec --app bar roar.simg env | grep SOFTWARE
SOFTWARE=bar

Examples

For more examples, for real world scientific recipes we recommend you look at other containers on Singularity Hub. For examples of different bases, look at the examples folder for the most up-to-date examples. For apps, including snippets and tutorial with more walk throughts, see SCI-F Apps Home.

Best Practices for Build Recipes

When crafting your recipe, it is best to consider the following:

  1. To make your container internally modular, use SCI-F apps. Shared dependencies (between app modules) can go under %post.
  2. For global installs to %post, install packages, programs, data, and files into operating system locations (e.g. not /home, /tmp, or any other directories that might get commonly binded on).
  3. Make your container speak for itself. If your runscript doesn’t spit out help, write a %help or %apphelp section. A good container tells the user how to interact with it.
  4. If you require any special environment variables to be defined, add them the %environment and %appenv sections of the build recipe.
  5. Files should never be owned by actual users, they should always be owned by a system account (UID less than 500).
  6. Ensure that the container’s /etc/passwd, /etc/group, /etc/shadow, and no other sensitive files have anything but the bare essentials within them.
  7. It is encouraged to build containers from a recipe instead of a sandbox that has been manually changed. This ensures greatest possibility of reproducibility and mitigates the black box effect.

Are you a recipe pro and now ready to build? Take a look at the build documentation.