Containerizing Projects with Apptainer

Author
Dr. Nicholas A. Del Grosso

Reproducibility in computational work is often constrained less by code and more by the surrounding environment: operating system, libraries, system dependencies, and subtle configuration differences. These factors become especially visible in high-performance computing (HPC) settings, where users typically do not control the base system and where software stacks vary across clusters.

Apptainer provides a way to package an entire computational environment into a single, portable artifact that can be executed consistently across systems. Unlike traditional container tools, it is designed to align with HPC security and infrastructure constraints, such as unprivileged execution and integration with shared filesystems.

This notebook explores how environments can be captured, modified, validated, and distributed in a way that reduces ambiguity between development and execution contexts. The emphasis is on building a mental model of how containerized environments behave, and where their boundaries interact with the host system.

By the end of this notebook, you will have hands-on experience building container images, modifying environments, defining reproducible builds, and distributing your work through registries and automated pipelines. Please experiment, make changes, and observe what persists versus what is ephemeral.

Setup

Install Apptainer with Apt

add-apt-repository -y ppa:apptainer/ppa
apt update
apt install apptainer
apt install squashfuse fuse2fs gocryptfs # optional

Section 1: Section 1: Using Existing Containers: Building an Apptainer SIF Image from Docker

A large ecosystem of pre-built environments already exists in the form of Docker images. Rather than recreating environments from first principles, it is often more efficient to reuse and adapt these existing artifacts.

Apptainer can convert Docker images into its native SIF format, which is optimized for portability and single-file distribution. This translation step raises a few important questions:

  • What aspects of the original environment are preserved?
  • What assumptions are baked into the image?
  • How does execution differ when running inside the container versus on the host?

Understanding this layer helps clarify what a container actually encapsulates, and where implicit dependencies may still exist.

Reference

Code Description
apptainer build Build a SIF file from a local or remote source.
apptainer exec Run some commands from within the SIF image.
apptainer exec –fakeroot … Run commands as though you were a super-user.
apptainer exec –bind : Map a local file to the container’s filesystem.

build sif docker:: shell –bind

Exercises

Example: build an Apptainer SIF image file from Dockerhub’s rockylinux container.

apptainer build rocky.sif docker://rockylinux:9 

Example: Run cat etc/os-release from within the SIF image to print out the operating system information, to confirm that it does indeed contain Rocky Linux.

apptainer exec rocky.sif cat etc/os-release

Exercise: build an Apptainer SIF image file from Dockerhub’s debian container.

Solution
apptainer build deb.sif docker://debian:latest

Exercise: Run cat etc/os-release from within the SIF image to print out the operating system information, to confirm that it does indeed contain Debian.

Solution
apptainer exec deb.sif cat etc/os-release

Exercise: build an Apptainer SIF image file from Dockerhub’s python container, and run python --version from inside it.

Solution
apptainer build py.sif docker://python:latest
apptainer exec py.sif python --version

Exercise: What username and permissions do you have from inside the container, and does adding --fakeroot to an apptainer command change that? Run id from inside a container, both with and without adding --fakeroot to compare.

Solution
apptainer exec deb.sif id
apptainer exec --fakeroot deb.sif id

Exercise: The host’s current directory and the home directoy are available from within Apptainer environments, but you can also bind other host directories to the Apptainer filesystem. use --bind .:/mnt/data and run the command ls /mnt/data to see that the current directory is now additionally mapped to /mnt/data:

Solution
apptainer exec --bind .:/mnt/data deb.sif ls /mnt/data

Exercise: Tired of always typing apptainer exec? You can start a new interactive shell from within the container by typing apptainer shell. Try it out and call cat etc/os-release from within the interactive shell. exit or <CTRL-D> will get you out of the shell.

Solution
apptainer shell deb.sif
cat etc/os-release
exit

Section 2: Section 2: Interactively Updating an Apptainer environment with a Sandbox

Containers are often presented as immutable artifacts, but during development, immutability can be a limitation. Iteration requires a way to explore changes without committing to a full rebuild each time.

Apptainer supports both ephemeral and persistent modification workflows. Temporary writable layers allow experimentation without altering the base image, while sandbox environments provide a directory-based representation that can be incrementally modified.

This introduces a key distinction:

  • ephemeral state: useful for exploration, but discarded
  • persistent state: incorporated into a reproducible artifact

Being able to move intentionally between these modes helps avoid a common failure mode where environments are modified interactively but cannot be reconstructed.

build –fakeroot build –writable-tmpfs build –sandbox shell –writable build a a.sif build a.sif a

Exercises

Exercise: Create a new SIF image based on DockerHub’s Debian image and install git into it with the following command: bash -lc "apt update && apt install git && git". As you get errors, add the --writable-tmpfs (which creates a temporary writable filesystem in RAM) and --fakeroot options to the apptainer exec command to see how they help make the command work:

Solution
apptainer build deb2.sif docker://debian:latest
apptainer exec --writable-tmpfs --fakeroot deb2.sif bash -lc "apt update  && apt install git && git"

Exercise: Is git permanently installed in the SIF image now? Try running just the git command in the image to check.

Solution
apptainer exec deb2.sif git  # expect command not found error

Exercise: To get a permanent filesystem to work with, build a “Sandbox” image from the SIF file with the apptainer build --sandbox option. When install git into the new sandbox with --writable and --fakeroot options.

Solution
apptainer build --sandbox deb deb2.sif
apptainer exec --writable --fakeroot deb bash -lc "apt update && apt install -y git && git"

Exercise: Is git permanently installed in the Sandbox SIF image now? Try running just the git command in the image to check.

Solution
apptainer exec deb git --version

Exercise: Shell into the sandbox and install python with apt install python:

Solution
apptainer shell --writable --fakeroot deb
apt update
apt install -y python3
python3 --version
exit

Exercise: Now we can freeze the sandbox back into a SIF file with the apptainer build command. Try it out, and verify the new SIF file has git installed:

Solution
apptainer build deb3.sif deb
apptainer exec deb3.sif git --version
apptainer exec deb3.sif python3 --version

Section 3: Section 3: Definition Files

Interactive modification is useful for discovery, but it does not scale well for collaboration or long-term reproducibility. At some point, environments need to be described declaratively.

Apptainer definition files provide a structured format for encoding:

  • the origin of the environment
  • the transformations applied to it
  • validation checks
  • expected runtime behavior

This shifts environment creation from a sequence of manual steps to a documented, repeatable process. It also makes assumptions explicit, which reduces the risk of hidden dependencies or irreproducible states.

Reference documentation: https://apptainer.org/docs/user/main/definition_files.html

Example File

# rand.def

Bootstrap: docker
From: ghcr.io/prefix-dev/pixi:latest

%files
    # Which files in the host filesystem to copy into the container's filesystem (SOURCE TARGET)
    pixi.toml /app/pixi.toml
    *.py /app/


%post
    # What commands to run while building the file
    cd /app && pixi install

%test
    # At the end of the file build, what code to run to confirm everything works correctly.
    echo "--- checking pixi is available ---"
    pixi --version

    echo "--- checking python is at expected path ---"
    test -x /app/.pixi/envs/default/bin/python || { echo "FAIL"; exit 1; }

    echo "--- all tests passed ---"

%runscript
    # What to run for the `apptainer run image.sif` command.  
    exec /app/.pixi/envs/default/bin/python /app/main.py "$@"


%help
    # Help text. Displays on `apptainer run-help image.sif`
    Print a Random Number using Numpy in Python:

        apptainer build rand.sif rand.def
        apptainer run rand.sif

https://apptainer.org/docs/user/main/definition_files.html#scif-app-sections

Exercises

Exercise: Use apptainer build <target> <source> to build and run the Apptainer image defined in rand.sif.

Solution
apptainer build rand.sif rand.def
apptainer run rand.sif

Exercise: Add another test to the Apptainer image to check that numpy is importable by python (`python -c “import numpy”), then re-build the image.

Solution

Add this check to the %test section of rand.def, then rebuild and test the image:

python -c "import numpy"

apptainer build rand.sif rand.def
apptainer test rand.sif

Exercise: Create a new definition file based on the requirements below, and build and run it!

  1. Is based off of Debian 9
  2. Has Python 3 installed (apt install python3)
  3. Contains a Python file that calculates a random integer using built-in packages (`import random; print(random.randint(1, 100)))
  4. Has help text (check you can view the help text with apptainer run-help rand2.sif)
  5. Once the build is ready, tests that python is available.
Solution
cat > rand2.def <<'EOF'
Bootstrap: docker
From: debian:9

%post
    apt update
    apt install -y python3
    cat > /usr/local/bin/randint.py <<'PY'
import random
print(random.randint(1, 100))
PY
    chmod +x /usr/local/bin/randint.py

%test
    python3 --version

%runscript
    exec python3 /usr/local/bin/randint.py

%help
    Print a random integer from 1 to 100.
EOF

apptainer build rand2.sif rand2.def
apptainer test rand2.sif
apptainer run rand2.sif
apptainer run-help rand2.sif

Section 4: Section 4: Storing them in GitHub GHCR

Once an environment is packaged, it becomes more useful when it can be shared and versioned. Registries provide a way to store and retrieve container images as addressable artifacts.

In collaborative or distributed settings, this supports workflows where:

  • environments are referenced by version rather than rebuilt ad hoc
  • results can be traced back to a specific image
  • updates can be propagated without manual transfer

Using a registry introduces additional concerns, such as authentication, permissions, and naming conventions, which become part of the overall system. Here, we’ll push to the GitHub Container Registry (GHCR).

registry login

push pull

Web Interface

Exercises

Exercise: Log in to the GitHub Container Registry (GHCR) with a token that has permission to push packages to your GitHub account. Create the token from GitHub Settings -> Developer settings -> Personal Access Tokens -> Tokens (classic).

Solution
apptainer registry login -u <github-username-or-orgname> oras://ghcr.io

Exercise: Push the package onto GHCR. Once pushed, view your packages at https://github.com/<github-username-or-orgname>/?tab=packages.

Solution
apptainer push deb.sif oras://ghcr.io/<github-username-or-orgname>/<package-name>:<tag>

Exercise: Pull the package from GHCR to your local filesystem.

Solution
apptainer pull deb10.sif oras://ghcr.io/<github-username-or-orgname>/<package-name>:<tag>

Section 5: Section 5: Build SIF Packages on Git Push: Using CI Services like GitHub Actions to build your Apptainers

Manual workflows introduce variability: steps may be skipped, environments may drift, and results may depend on local state. Automation reduces this variability by encoding the build process into a system that runs consistently.

Continuous integration (CI) systems allow container builds to be triggered by changes in version-controlled code. This creates a tighter coupling between:

  • the definition of an environment
  • the artifact that results from it

It also provides immediate feedback when assumptions break, such as missing dependencies or failing tests.

In this sense, CI is not just a convenience—it is a mechanism for enforcing reproducibility over time.

Example Workflow

# .github/workflows/apptainer.yml

name: apptainer

on:
  push:
    branches: [main]

permissions:
  contents: read
  packages: write

jobs:
  build:
    runs-on: ubuntu-24.04
    container:
      image: kaczmarj/apptainer

    steps:
      - name: Check out repository
        uses: actions/checkout@v4

      - name: Build SIF
        run: sudo apptainer build rand.sif rand.def

      - name: Log in to GHCR for ORAS push
        run: |
          echo "${{ secrets.GITHUB_TOKEN }}" | \
            apptainer registry login \
              --username "${{ github.actor }}" \
              --password-stdin \
              docker://ghcr.io

      - name: Push SIF to GHCR via ORAS
        run: apptainer push rand.sif oras://ghcr.io/${{ github.repository_owner }}/rand:latest

Exercises

Project: Add the rand.def definition and the .github/workflows/apptainer.yml files to a git repository and push to GitHub, checking the workflow progress under the GitHub repository’s Actions tab. Does the build work successfully? As you detect errors, make corrections to the files locally and re-push under the build is successful.

Solution
One working path is to copy rand.def into the repository root, copy github-ci.yaml to .github/workflows/apptainer.yml, commit both files, and push to main. Then open the repository Actions tab, inspect the build logs, fix any failing definition or workflow step locally, and push again until the workflow builds and pushes rand.sif successfully.