I’ve been wanting to try using containers for managing different development environments for a while now. Docker is a well known container software, but I’ve found it not be well-suited for development. Singularity is an alternative that is designed for high performance computing and not microservices. It integrates more readily with the host OS, while being more secure than Docker. Interestingly, in some applications Singularity containers actually perform better than bare-metal(the native OS). In this post I will outline what I’ve done to use Singularity for a container-based development environment.

Singularity containers use recipes, which can derive off of a variety of different sources, such as Docker containers or Singularity containers. Singularity recipes are analogous to Docker files. Setting up a development environment requires being aware of a few subtleties:

  • You can share the host init file for your shell (~/.bashrc, for example) by launching the Singularity container using singularity exec ubuntu.simg /bin/bash. However, since the configuration in the container is different from the host you may need to tweak your init files to work in both environments.
  • Having a modules (see LMod) system both inside and outside the container can lead to difficulty. You need to run module purge before starting the Singularity container.
  • You can run a different OS in the container than the host under almost all circumstances. The only exception is when you need to install software that is compatible with the host kernel since that may not be available from the container OS’s package manager. The Linux perf utility is the example I encountered.
  • The locale inside the container must be set to the locale of the host OS. Without setting the locale correctly certain fonts like Powerline won’t work and perl gives warnings about the incorrect locale settings. Unfortunately, setting the locale is OS dependent, but I will show how to do it for Ubuntu and Arch Linux.

Sharing init files

To enable my ~/.zshrc to work both inside and outside the container I have the container set the environment variable IN_CONTAINER=true. In Singularity recipes this is achieved using:

1
2
%environment
    export IN_CONTAINER=true

Inside my ~/.zshrc I have the following function that returns true if evaluated inside a container:

1
2
3
4
5
6
7
# Check if we are running inside a container
in_container() {
    if [[ $IN_CONTAINER == "true" ]]; then
        return 0
    fi
    return 1
}

I then use the in_container function throughout my ~/.zshrc file to control exactly what is executed inside the container and outside.

Using modules

In order to use modules both inside and outside the container I set up an alias that purges the modules, drops into a container, and reloads the modules after I exit the container. Here is what I do:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
if ! in_container; then
    # Load modules for host OS
    load_modules() {
        module load \
               blaze-3.2-gcc-7.2.0-mnkglif \
               brigand-master-gcc-6.3.1-pcaobur \
               libxsmm-1.8.1-gcc-6.3.1-mevsuzx \
               openblas-0.2.19-gcc-6.3.1-if6giqo \
               gsl-2.3-gcc-6.3.1-hsntrnj \
               yaml-cpp-master-gcc-6.3.1-gmc3k4n \
               catch-2.1.0-gcc-7.3.0-do5wfi3 \
               kvasir-mpl-develop-gcc-7.2.0-hr2lgqe
    }
    load_modules

    # Drop into a Singularity dev environment, reset modules after
    container() {
        if [ "$1" = "arch" ]; then
            module purge && \
                singularity exec \
                            ~/Research/singularity/arch.simg \
                            /bin/zsh && \
                load_modules
        elif [ "$1" = "ubuntu" ]; then
            module purge && \
                singularity exec \
                            ~/Research/singularity/ubuntu.simg \
                            /bin/zsh && \
                load_modules
        fi
    }
fi

The container function can be called to drop into a Singularity container of your choice. In my case they all load Zsh as the default shell, but you can change this back to bash if you prefer. Since the system modules can interfere with the modules in the container we first purge the system modules, then launch the container, and after exit reload the host modules.

Dealing with kernel dependencies

For my work I really care about performance of code, and so optimization is something I spend a fair amount of my time doing. This means I need to be able to use Linux perf to see how software and hardware are interacting. Since you cannot use perf version that is older than your kernel version, using perf might require you to use the same OS inside and outside the container. For example, I cannot use perf if I use an Ubuntu container since there is no perf available that is compatible with the kernel version. Using an Arch Linux container allows me to easily install a recent version of perf that is compatible with my kernel. The good news is that for most people the OS won’t matter, and if it does the solution is fairly straightforward: use the same OS as your host.

Setting the locale/Powerline fonts support

If the locale inside the Singularity container is not set correctly then fonts like Powerline won’t work. I use the Agnoster Zsh theme, which uses Powerline fonts. Setting the locale is OS dependent so you’ll need to figure out how to do it for the OS you’re using in the container. Here is what is necessary for an Ubuntu recipe:

1
2
3
4
5
6
7
8
9
10
%post
    # In order to get locales working properly inside a Singularity container
    # we need to do the following:
    apt-get update && apt-get install -y \
            locales language-pack-fi language-pack-en
            export LANGUAGE=en_US.UTF-8 && \
            export LANG=en_US.UTF-8 && \
            export LC_ALL=en_US.UTF-8 && \
            locale-gen en_US.UTF-8 && \
            dpkg-reconfigure locales

and what is necessary for an Arch Linux recipe:

1
2
3
4
5
%post
    # Use US english UTF-8 locale
    sed -i 's/#en_US.UTF-8/en_US.UTF-8/g' /etc/locale.gen
    locale-gen en_US.UTF-8
    pacman -Sy --noconfirm zsh

Tweaking recipe files and incremental builds

One nice feature of Singularity containers (similar to Docker containers) is that incremental builds will use the existing image to run the recipe rather than an empty one. Let me give an example. Say I’m building the recipe called Ubuntu.sh, then I would run sudo singularity build ubuntu.simg Ubuntu.sh. If I want to add a new package using apt-get I can comment out all the commands under the %post section that built the existing image and only have the new commands. Then when I run sudo singularity build ubuntu.simg Ubuntu.sh, the additional command will be run and the new image is much faster to build.

Specifically, say the example recipe file below is built.

1
2
3
4
5
6
Bootstrap: docker
From: ubuntu:18.04

%post
    apt-get -y update
    apt-get -y install wget

If the line apt-get -y install emacs is added to the file like:

1
2
3
4
5
6
7
Bootstrap: docker
From: ubuntu:18.04

%post
    apt-get -y update
    apt-get -y install wget
    apt-get -y install emacs

When the image is rebuilt only the new command will be executed and the result added to the image.

Summary

In this post I describe some of the troubles with using Singularity containers for daily development and how to overcome them. Specifically, I address how to use modules, deal with kernel dependencies, and set the locale to get Powerline fonts to work inside Singularity. I’m sharing the container recipes that I use for development on GitHub. Overall Singularity containers are a nice way to develop software, especially if the development is shared by many people and there is a long list of dependencies. For the project I work on mainly, SpECTRE, we’ve been using Docker containers for nearly a year now and just adopted Singularity as an alternative to make local development easier.

I hope you find this post helpful and please feel free to ask any questions in the comments below.