<- previous - home - next ->


Building a Basic Container

In this section, we will build a brand new container similar to the lolcow container we’ve been using in the previous examples.

To build a singularity container, you must use the build command. The build command installs an OS, sets up your container’s environment and installs the apps you need. To use the build command, we need a definition file. A Singularity definition file is a set of instructions telling Singularity what software to install in the container.

We are going to use a standard development cycle (sometimes referred to as Singularity flow) to create this container. It consists of the following steps:

The Singularity source code contains several example definition files in the /examples subdirectory. Let’s make a new directory, copy the Debian example definition file, and inspect it.

$ mkdir ~/lolcow

$ cp ~/singularity/examples/debian/Singularity ~/lolcow/lolcow.def

$ cd ~/lolcow

$ nano lolcow.def

It should look 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 -y --allow-unauthenticated install vim

See the Singularity docs for an explanation of each of these sections.

Developing a new container

Now let’s use this definition file as a starting point to build our lolcow.img container. Note that the build command requires sudo privileges. (We’ll discuss some ways around this restriction later in the class.)

$ sudo singularity build --sandbox lolcow lolcow.def

This is telling Singularity to build a container called lolcow from the lolcow.def definition file. The --sandbox option in the command above tells Singularity that we want to build a special type of container (called a sandbox) for development purposes.

Singularity can build containers in several different file formats. The default is to build a SIF (singularity image format) container that uses squashfs for the file system. SIF files are compressed and immutable making them the best choice for reproducible, production-grade containers.

But if you want to shell into a container and tinker with it (like we will do here), you should build a sandbox (which is really just a directory). This is great when you are still developing your container and don’t yet know what to include in the definition file.

When your build finishes, you will have a basic Debian container saved in a local directory called lolcow.

Using shell --writable to explore and modify containers

Now let’s enter our new container and look around.

$ singularity shell lolcow

Depending on the environment on your host system you may see your prompt change.

Let’s try installing some software. I used the programs fortune, cowsay, and lolcat to produce the container that we saw in the first demo.

Singularity> sudo apt-get update
bash: sudo: command not found

Whoops!

The sudo command is not found. But even if we had installed sudo into the container and tried to run this command with it, or change to root using su, we would still find it impossible to elevate our privileges within the container:

Singularity> sudo apt-get update
sudo: effective uid is not 0, is /usr/bin/sudo on a file system with the 'nosuid' option set or an NFS file system without root privileges?

Once again, this is an important concept in Singularity. If you enter a container without root privileges, you are unable to obtain root privileges within the container. This insurance against privilege escalation is the reason that you will find Singularity installed in so many HPC environments.

Let’s exit the container and re-enter as root.

Singularity> exit

$ sudo singularity shell --writable lolcow

Now we are the root user inside the container. Note also the addition of the --writable option. This option allows us to modify the container. The changes will actually be saved into the container and will persist across uses.

Let’s try installing our software again.

Singularity> apt-get update

Singularity> apt-get install -y fortune cowsay lolcat

Now you should see the programs successfully installed. Let’s try running the demo in this new container.

Singularity> fortune | cowsay | lolcat
bash: lolcat: command not found
bash: cowsay: command not found
bash: fortune: command not found

Drat!

It looks like the programs were not added to our $PATH. Let’s add them and try again.

Singularity> export PATH=$PATH:/usr/games

Singularity lolcow:~> fortune | cowsay | lolcat
 ________________________________________
/ Keep emotionally active. Cater to your \
\ favorite neurosis.                     /
 ----------------------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Great! Things are working properly now.


NOTE

If you receive warnings from the Perl language about the locale being incorrect, you can usually fix them with export LC_ALL=C.



NOTE

We changed our path in this session, but those changes will disappear as soon as we exit the container just like they will when you exit any other shell. To make the changes permanent we should add them to the definition file and re-bootstrap the container. We’ll do that in a minute.


Building the final production-grade SIF file

Although it is fine to shell into your Singularity container and make changes while you are debugging, you ultimately want all of these changes to be reflected in your definition file. Otherwise if you need to reproduce it from scratch you will forget all of the changes you made. You will also want to rebuild you container into something more durable, portable, and robust than a directory.

Let’s update our definition file with the changes we made to this container.

Singularity> exit

$ nano lolcow.def

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
    echo "Hello from inside the container"
    apt-get update
    apt-get -y install fortune cowsay lolcat

%environment
    export PATH=$PATH:/usr/games

Let’s rebuild the container with the new definition file.

$ sudo singularity build lolcow.sif lolcow.def

Note that we changed the name of the container. By omitting the --sandbox option, we are building our container in the standard Singularity file format (SIF). We are denoting the file format with the (optional) .sif extension. A SIF file is compressed and immutable making it a good choice for a production environment.

As we saw in the previous section when we used the inspect command to read the runscript, Singularity stores a lot of useful metadata. For instance, if you want to see the definition file that was used to create the container you can use the inspect command like so:

$ singularity inspect --deffile  lolcow.sif
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

%environment
    export PATH=$PATH:/usr/games

Building from existing containers

In the preceding section we always used the following header in our definition file to build a container:

BootStrap: debootstrap
OSVersion: stable
MirrorURL: http://ftp.us.debian.org/debian/

This uses the program debootstrap to build the root file system using a mirror URL. In this case, we supply a URL that is maintained by Debian. We could also use an Ubuntu URL since it is a derivative of Debian and can also be built with the debootstrap program. If we wanted to build a CentOS container from the distribution mirror we could use the yum package manager similarly. There are actually a ton of different ways to build containers. See this list of “bootstrap agents” in the Singularity docs.

In practice, most people do not build containers from a distribution mirror like this. Instead they tend to build containers from existing containers on the Container Library or on Docker Hub and use the %post section to modify those containers to suit their needs.

For instance, to use an existing Debian container from the Container library as your starting point, your header would look like this:

BootStrap: library
From: debian

Likewise to start from a debian container on Docker Hub, your header would contain the following:

Bootstrap: docker
From: debian

You can also build a container from a base container on your local file system.

Bootstrap: localimage
From: /home/student/debian.sif

Each of these methods can also be called without providing a definition file using the following shorthand. For an added bonus, none of these build commands require root privileges.

$ singularity build debian1.sif library://debian

$ singularity build debian2.sif docker://debian

$ singularity build debian3.sif debian2.sif

Behind the scenes, Singularity creates a small definition file for each of these commands and then builds the corresponding container as you can see if you use the inspect --deffile command.

$ singularity inspect --deffile debian1.sif
bootstrap: library
from: debian

$ singularity inspect --deffile debian2.sif
bootstrap: docker
from: debian

$ singularity inspect --deffile debian3.sif
bootstrap: localimage
from: debian2.sif

Note that the third command may not seem very useful because you are just copying the container called debian2.sif to a new container called debian3.sif. But you can also use build in this way to convert a SIF file to a sandbox and back again:

$ singularity build --sandbox deb-sand debian3.sif

$ singularity build deb-sif deb-sand/

This can be a useful trick during container development. But it can also produce a container with an uncertain build history if it is misapplied because the changes made to the sandbox will not be reflected in the containers definition file.

Security considerations and --fakeroot

In the preceding we’ve been executing the build command as root via sudo. In our examples, that is a reasonably safe thing to do, because we use disposable virtual machines for this class and we are building directly from mirrors hosted by same groups that create the OS distributions. (Though mirrors can still contain malware.)

But in general, it’s a bad idea to build a container as root. In particular you should never build a container from an untrusted base image as root on a machine you care about. This is the same as downloading random code from the internet and running it as root on your machine. (See this blog for a technical discussion.)

On operating systems with recent kernels (such as Ubuntu 18.04), you can invoke the --fakeroot option when building containers instead. (For those interested in technical details, this feature leverages the user namespace).

$ singularity build --fakeroot container.sif container.def

Doing so allows you to pretend to be the root user inside of your container without actually granting singularity elevated privileges on host system. This is a much safer way to build and interact with your container, and it is going to become more prevalent (eventually probably even default) as more distributions ship with user namespaces enabled. For instance, this feature is enabled by default in RHEL 8.

For more about the --fakeroot option, see the Singularity documentation.


<- previous - home - next ->