Build and Run HPC Containers

Package and run reproducible scientific workloads using Apptainer on a Slurm cluster.

Introduction

30 Second Summary

Running code on your laptop is one thing. Running the exact same code on a shared cluster with dozens of users and different software versions is another challenge entirely.

In this project, you will install Apptainer on your cluster, build a custom scientific container from a definition file, and submit containerized workloads through Slurm.

What You'll Build

You'll run a Monte Carlo pi simulation inside a single-file container image, submit it to your cluster's job scheduler, and get identical results no matter which node picks up the job.

By the end of this project, you'll have:

  • A verified Apptainer install on your cluster and a container pulled from Docker Hub that lets you interact with a completely different Linux distribution running inside your Ubuntu host.
  • A custom-built SIF image containing a reproducible Python + NumPy environment, written from scratch using an Apptainer definition file and tested with bind mounts that connect host data into the container.
  • A Slurm batch script that submits your containerized Monte Carlo workload to a compute node, proving you can package, distribute, and run reproducible science the way HPC engineers do it in production.
  • Secret Mission: Scale out with a Slurm job array that runs the same container with different parameters across multiple nodes.

Are there any prerequisites?

This project assumes you already have SSH access to a 4-node Ubuntu 24.04 cluster with Slurm and shared storage configured. If you completed the earlier cluster setup project in this series, you're ready to go.

Before We Start

Before We Start...

Before we dive in, let's lock in what you're about to build and why it matters for your HPC engineering work.

Install Apptainer on Your Cluster

Your cluster has Slurm, shared storage, and four Ubuntu nodes ready to go. But without a container runtime, you have no way to package scientific workloads into portable, reproducible units that run identically on every node.

Apptainer is the HPC container standard. It runs without a daemon, executes as your own user (no root escalation), and produces single-file images you can drop onto shared storage. In this step, you'll install it from the official PPA and confirm it works.

In this step, get ready to:
  • Add the Apptainer PPA and install the package on your cluster.
  • Pull and run a test container to confirm unprivileged execution works.
Add the Apptainer PPA and install the package

Ubuntu 24.04 restricts unprivileged user namespaces by default through AppArmor. The official Apptainer PPA ships a correct AppArmor profile that grants the permission your containers need to run without root.

  • In your terminal connected to the cluster login node, check whether Apptainer is already installed by running:
apptainer --version

✔️ I see version 1.5.3

Apptainer is already installed and up to date. Skip ahead to the next substep to verify it with a test container.

ⓧ I see an older version

You have an older version installed. Upgrade to 1.5.3 by adding the official PPA and reinstalling.

  • Add the Apptainer PPA by running:
sudo add-apt-repository -y ppa:apptainer/ppa
  • Update your package list and install the latest version by running:
sudo apt update
sudo apt install -y apptainer
  • Confirm the upgrade succeeded by running:
apptainer --version

You should see apptainer version 1.5.3 printed.

ⓧ Command not found

Apptainer is not installed yet. Install it from the official PPA.

  • Add the Apptainer PPA to your package sources by running:
sudo add-apt-repository -y ppa:apptainer/ppa
  • Update your package list and install Apptainer by running:
sudo apt update
sudo apt install -y apptainer
  • Confirm the installation succeeded by running:
apptainer --version

You should see apptainer version 1.5.3 printed.

Why install from a PPA instead of building from source?

The PPA package includes the AppArmor profile that Ubuntu 24.04 requires for unprivileged user namespaces. Without it, container execution would fail silently or require root. Building from source would mean manually configuring this profile yourself.

Why Apptainer instead of Docker?

Docker requires a persistent daemon running as root, which is a security risk on shared HPC clusters. Apptainer is daemonless and executes containers as your own user process. This makes it safe for multi-tenant environments where you don't control root access.

Apptainer also produces a single SIF file per container. You can copy it to shared storage and every node runs the exact same image. No registry, no layered cache, no daemon coordination.

Verify with a test container

A version check confirms the binary is installed. Now you need to prove the full pipeline works: pulling an image from a remote registry, converting it to SIF format, and executing a command inside the container, all without root.

  • Before you run the next command, predict what you think will happen. Will the output come from your Ubuntu host, or from somewhere else?
  • Pull and run an Alpine Linux container in one command by running:
apptainer exec docker://alpine cat /etc/alpine-release

You should see a version number (like 3.21.3) printed. That number comes from inside the Alpine container, not from your Ubuntu host.

What does this command do?

This single command performs three things. It pulls the Alpine Linux image layers from Docker Hub, converts them into Apptainer's SIF format, and executes cat /etc/alpine-release inside the resulting container.

The output proves your container runtime works end-to-end. You ran a completely different Linux distribution inside your Ubuntu host without any root privileges.

Seeing a permission denied or namespace error?

This usually means the AppArmor profile is not loaded. Run sudo apparmor_parser -r /etc/apparmor.d/apptainer to reload it, then try the command again.

If you see a network error instead, check that your node has internet access to reach Docker Hub.

Still stuck? Help me troubleshoot an Apptainer namespace or permission error on Ubuntu 24.04.

Apptainer is installed and running containers without root. Next up, you'll pull a full CentOS image and explore how container isolation actually works from the inside.

Pull and Explore a Container

Apptainer is installed and ready on your cluster. Now let's see what makes it fundamentally different from just installing packages on your host.

In this step, you'll pull a CentOS container onto your Ubuntu host. This shows that a container's filesystem is completely isolated from the host, while your home directory and current working directory remain visible inside it by default.

In this step, get ready to:
  • Pull a CentOS Stream 9 container image and convert it to a portable SIF file.
  • Shell into the container and explore the isolation model.
  • Run a one-off command inside the container without entering a shell.
Pull a CentOS Stream 9 container image

Apptainer can pull images from container registries and convert them into its native SIF format. A SIF file is a single compressed file containing an entire operating system userspace.

  • Pull the CentOS Stream 9 image by running this command:
apptainer pull docker://quay.io/centos/centos:stream9

What does this command do?

The docker:// prefix tells Apptainer to use the OCI/Docker protocol to download layers from the Quay.io registry.

Apptainer assembles those layers and compresses them into a single centos_stream9.sif file in your current directory. This file is about 100MB and contains an entire CentOS userspace.

  • Confirm the SIF file exists by running:
ls *.sif

You should see centos_stream9.sif listed.

Don't see the SIF file?

  • Check your internet connection. The pull downloads about 100MB from the registry.
  • Make sure you typed the full URI including docker://quay.io/centos/centos:stream9 with no typos.
  • If you see a permission error, confirm Apptainer installed correctly by running apptainer --version.

Help me troubleshoot why my Apptainer pull command failed on Ubuntu 24.04.

Shell into the container and explore the isolation

You now have a CentOS Stream 9 filesystem packaged in a single file on your Ubuntu host. The key question is: what does the world look like from inside that container?

  • Before you enter the container, think about this: when you shell in, do you expect to become root, or stay as yourself?
  • Open an interactive shell inside the container by running:
apptainer shell centos_stream9.sif

Your prompt changes to indicate you are inside the container.

  • Check which operating system is running inside the container:
cat /etc/redhat-release

You see CentOS Stream release 9 printed. You are running CentOS inside your Ubuntu host.

  • Check who you are inside the container:
whoami

You see your own username printed. Apptainer does not escalate to root. You are the same user inside and outside the container.

  • Check whether your home directory is visible inside the container:
ls ~

You see your regular home directory files listed. Apptainer mounts your home directory into the container by default, so your files are accessible without any extra configuration.

Why is home mounted by default?

Apptainer is designed for HPC environments where users need access to their data. Mounting $HOME, $PWD, /tmp, and a few system paths by default means you can run a container as a drop-in replacement for a local command.

Paths outside these defaults (like a shared project directory at /data) require an explicit bind mount, which you will set up in a later step.

  • Exit the container by typing:
exit
  • Now compare with your host OS by running:
cat /etc/os-release

You see Ubuntu 24.04 details printed. Inside the container was CentOS Stream 9. Outside is Ubuntu 24.04. Two completely different operating systems, running on the same machine.

Still seeing CentOS after exiting?

  • Make sure you typed exit and pressed Enter. Your prompt should return to your normal shell prompt (without the Apptainer> prefix).
  • If your prompt still shows the container, type exit again. You may have opened a nested shell.

Help me figure out if I'm inside or outside my Apptainer container.

Run a command inside the container without a shell

Entering an interactive shell is useful for exploration. In production, you typically run a single command inside the container and get back the result directly.

  • What do you think will happen if you ask for a list of installed RPM packages while on your Ubuntu host? Do you expect rpm to work?
  • Run a one-off command inside the container using apptainer exec:
apptainer exec centos_stream9.sif rpm -qa | head -5

You see five CentOS RPM package names printed. Your Ubuntu host uses apt and has no rpm command. The container has rpm and its full package database.

What is apptainer exec?

The exec subcommand runs a single command inside the container and returns you to your host shell immediately. No interactive session, no prompt change.

This is how you will run containerized workloads in Slurm batch scripts later. The scheduler calls apptainer exec or apptainer run and the job completes without human interaction.

Seeing 'command not found' for rpm?

  • Make sure you included the SIF filename between exec and the command. Without it, your host shell tries to run rpm directly on Ubuntu.
  • Verify the SIF file is in your current directory with ls centos_stream9.sif.

Help me understand why rpm is not found when I run apptainer exec.

You have explored the core of Apptainer's isolation model. A container carries its own OS and packages, but you remain the same user and your home directory stays accessible. Next up, you will build your own custom container from a definition file, tailored to run a scientific workload.

Build a Custom Scientific Container

In the previous step, you pulled a pre-built CentOS container and explored it on your Ubuntu host. That showed the power of container isolation, but pre-built images rarely match your exact scientific workloads.

A definition file is the "source code" for your container. Building from it produces an immutable SIF image that guarantees identical execution on every node. In this step, you will write a definition file, create a Python script that estimates pi using Monte Carlo sampling, and build both into a single portable container.

In this step, get ready to:
  • Write an Apptainer definition file that specifies your container's base image, packages, and runtime behaviour.
  • Create a Python script that estimates pi using random sampling with NumPy.
  • Build the definition file into an immutable SIF image and verify the environment works.
Create the definition file

An Apptainer definition file declares everything about your container: the base image, packages to install, files to include, environment variables, and what happens when the container runs.

  • Create montecarlo.def by opening a new file in nano:
nano montecarlo.def
  • Paste the following content into the file:
Bootstrap: docker
From: ubuntu:24.04

%post
    apt-get update && apt-get install -y --no-install-recommends \
        python3 \
        python3-numpy
    apt-get clean
    rm -rf /var/lib/apt/lists/*
    mkdir -p /opt

%files
    montecarlo.py /opt/montecarlo.py

%environment
    export LC_ALL=C

%runscript
    python3 /opt/montecarlo.py "$@"

%labels
    Author HPC-Engineer
    Version 1.0

%help
    Monte Carlo Pi Estimation Container
    Usage: apptainer run montecarlo.sif <num_samples>
    Or with config: apptainer run --bind /path/to/data:/data montecarlo.sif

What does this definition file do?

  • Bootstrap: docker and From: ubuntu:24.04 tell Apptainer to use a Docker Hub Ubuntu image as the starting point.
  • %post runs commands inside the container during the build. It installs Python 3 and NumPy, then cleans up package caches to keep the image small.
  • %files copies montecarlo.py from your host into /opt/montecarlo.py inside the container at build time.
  • %runscript defines what happens when you apptainer run the container. The "$@" passes any arguments you provide through to the Python script.
  • Save the file by pressing Ctrl+O, then Enter, then Ctrl+X to exit nano.
  • Confirm the file was created by running:
ls montecarlo.def

You should see montecarlo.def listed in the output.

Create the Monte Carlo Python script

The Python script estimates pi by generating random points in a unit square and counting how many fall inside a quarter circle. This is a classic Monte Carlo method that benefits from large sample sizes, making it a good fit for HPC workloads.

  • Create montecarlo.py by opening a new file in nano:
nano montecarlo.py
  • Paste the imports and the estimate_pi function at the top of the file:
#!/usr/bin/env python3
"""Monte Carlo Pi Estimation - runs inside an Apptainer container."""

import sys
import os
import json
import numpy as np


def estimate_pi(num_samples):
    """Estimate pi using random sampling."""
    rng = np.random.default_rng()
    x = rng.random(num_samples)
    y = rng.random(num_samples)
    inside_circle = np.sum(x**2 + y**2 <= 1.0)
    pi_estimate = 4.0 * inside_circle / num_samples
    return pi_estimate

What does estimate_pi do?

It generates num_samples random (x, y) points between 0 and 1. Points where x² + y² ≤ 1 fall inside a quarter circle. The ratio of points inside to total points, multiplied by 4, approximates pi.

  • Below the estimate_pi function, paste the get_num_samples function:

def get_num_samples():
    """Get sample count from config file or command-line argument."""
    config_path = "/data/config.json"
    if os.path.exists(config_path):
        with open(config_path) as f:
            config = json.load(f)
        return config["samples"]
    elif len(sys.argv) > 1:
        return int(sys.argv[1])
    else:
        return 1000000

What does get_num_samples do?

This function checks for a JSON config file at /data/config.json first. If that path does not exist, it falls back to a command-line argument. If neither is provided, it defaults to 1,000,000 samples.

The /data/ path will become important in the next step when you learn about bind mounts.

  • Finally, paste the main function and entry point below get_num_samples:

def main():
    num_samples = get_num_samples()
    pi_estimate = estimate_pi(num_samples)
    print(f"Host: {os.uname().nodename}")
    print(f"Samples: {num_samples:,}")
    print(f"Pi estimate: {pi_estimate:.6f}")
    print(f"Error: {abs(pi_estimate - np.pi):.6f}")


if __name__ == "__main__":
    main()

What does main print?

It prints the hostname (so you can tell which cluster node ran it), the number of samples used, the pi estimate, and the absolute error from the true value of pi. This output becomes useful when you submit jobs through Slurm later.

  • Save the file by pressing Ctrl+O, then Enter, then Ctrl+X to exit nano.
  • Confirm both files exist by running:
ls montecarlo.def montecarlo.py

You should see both files listed.

✔️ Awesome, I've got everything!

Great. Make sure you have saved both files before moving on to the build.

ⓧ I'd like to double check the full code

Scroll up and confirm your montecarlo.py contains all three sections in order:

  • The imports and estimate_pi function at the top.
  • The get_num_samples function in the middle.
  • The main function and if __name__ == "__main__" block at the bottom.

You can view the file contents without opening the editor by running:

cat montecarlo.py

File looks empty or incomplete?

  • Make sure you pressed Ctrl+O (the letter O, not zero) to write the file before exiting nano.
  • If you accidentally exited without saving, run nano montecarlo.py again and re-paste the content.

Need help? Why is my file empty after I saved it in nano?

Build the SIF image

The apptainer build command reads your definition file, downloads the base image, runs the %post commands, copies your files in, and produces a single immutable SIF file.

  • Build the container image by running:
apptainer build montecarlo.sif montecarlo.def

What happens during the build?

Apptainer pulls the Ubuntu 24.04 layers from Docker Hub, installs python3 and python3-numpy, copies montecarlo.py into the image at /opt/montecarlo.py, and compresses everything into a single read-only SIF file.

This takes 1-2 minutes depending on your network speed. You will see package download and installation output scrolling past.

Once the build completes, you will see a line confirming the SIF file was created.

  • Confirm the image file exists by running:
ls -lh montecarlo.sif

You should see montecarlo.sif listed with a size around 70-120 MB.

Before you test the container, what do you expect to see when you run it with 1,000,000 samples?

  • Test the container by running:
apptainer run montecarlo.sif 1000000

You should see output showing your hostname, the sample count, a pi estimate close to 3.14159, and a small error value.

  • Verify the NumPy environment is properly installed inside the container by running:
apptainer exec montecarlo.sif python3 -c "import numpy; print(numpy.__version__)"

You should see a NumPy version number printed, confirming your custom scientific environment works inside the container.

Seeing a build error?

  • If you see an error about montecarlo.py not found during the %files stage, make sure montecarlo.py is in the same directory where you run the build command.
  • If you see network errors while downloading packages, check your internet connection and retry the build command.
  • If apptainer run prints a Python import error for numpy, the %post section may not have completed successfully. Delete montecarlo.sif and rebuild.

Still stuck? Help me debug my Apptainer build

You now have a custom-built, portable scientific container. Any node that can see montecarlo.sif can run it and get the same result. Next up, you will learn how to connect external data into the container using bind mounts.

Bind Mount External Data

Your Monte Carlo container runs perfectly when you pass sample counts as command-line arguments. In production HPC, workloads read configuration and input data from shared filesystems that live outside the container.

Your container can see your home directory by default, but paths like /tmp/simdata or project-specific data directories are invisible to it. In this step, you'll discover this limitation firsthand and learn how the --bind flag solves it.

In this step, get ready to:
  • Create a data directory with a configuration file on the host.
  • Discover that the container cannot see host paths outside the defaults.
  • Map the host directory into the container using bind mounts.
Create a test data directory

You need a directory on the host that represents external data your container should access. This simulates a shared filesystem or project storage that lives outside the container's default mount points.

  • Create the data directory and a JSON configuration file by running these commands:
mkdir -p /tmp/simdata
echo '{"samples": 5000000}' > /tmp/simdata/config.json
  • Confirm the file was created by running:
cat /tmp/simdata/config.json

You should see {"samples": 5000000} printed.

Try to access the data from inside the container

Your montecarlo.py script is designed to read from /data/config.json inside the container. That path does not exist in the container's filesystem, and /tmp/simdata is not one of Apptainer's default bind points.

Before you run this next command, what do you think will happen when you try to list /data inside the container?

  • Try listing /data inside the container by running:
apptainer exec montecarlo.sif ls /data

You see an error telling you the path does not exist. The container's filesystem has no /data directory, and nothing on the host was mapped there.

Why does this fail?

Apptainer binds a limited set of host paths by default: $HOME, $PWD, /tmp, /proc, /sys, and a few system files like /etc/resolv.conf.

Anything outside these defaults is invisible to the container. Containers are not VMs. You must explicitly tell them which host paths to expose. This is actually a feature: it prevents accidental data leakage and keeps your environment predictable.

Use --bind to map the host directory into the container

The --bind flag maps any host path into a location inside the container. The format is src:dest where src is the host path and dest is where it appears inside the container.

  • Run the same ls /data command, this time with a bind mount:
apptainer exec --bind /tmp/simdata:/data montecarlo.sif ls /data

You should see config.json listed. The host's /tmp/simdata directory now appears as /data inside the container.

What does --bind do?

The --bind /tmp/simdata:/data flag tells Apptainer to overlay the host directory onto /data inside the container. The container process can read and write to this path as if it were a native directory.

In HPC environments, this is how you connect shared storage (NFS exports, Lustre mounts, project directories) to your containerized workloads without baking data into the image.

Still not seeing config.json?

Make sure you created the file in /tmp/simdata (not /tmp/sim-data or another path). The src path in --bind must match the actual host directory exactly.

Also confirm the colon separates source from destination with no spaces: /tmp/simdata:/data.

Why isn't my bind mount working in Apptainer?

Now for the real test. Your montecarlo.py script already checks for /data/config.json and reads the sample count from it. What do you think will happen when you run the container with the bind mount and no command-line argument?

  • Run the container with the bind mount to prove it reads from the config file:
apptainer run --bind /tmp/simdata:/data montecarlo.sif

You should see output showing Samples: 5,000,000 along with a pi estimate and error value. The container read the sample count from your bind-mounted config.json instead of needing a command-line argument.

Why does the container know to read config.json?

The get_num_samples() function in montecarlo.py checks for /data/config.json first. If the file exists (because you bound it in), it reads the sample count from there. If not, it falls back to command-line arguments. If neither is provided, it defaults to 1,000,000 samples.

Your container can now reach external data on the host. Next up, you'll submit this containerized workload through Slurm so it runs on a compute node instead of the login node.

Submit via Slurm

Your container can now read data from the host through bind mounts. That's powerful, but so far you've been running everything interactively on the login node.

In production HPC, you never run workloads on login nodes. Slurm schedules your containerized workload onto compute nodes, and the SIF file on shared storage means every node runs the exact same environment without any per-node installation.

In this step, get ready to:
  • Copy your SIF and data to shared storage so all nodes can access them.
  • Write a Slurm batch script that runs your container on a compute node.
  • Submit the job and verify the pi estimation output from a compute node.
Copy your SIF to shared storage

Compute nodes need to access both your SIF image and your data directory. Your cluster's shared filesystem (NFS) makes your home directory visible from every node.

  • Verify that a compute node can see your SIF file by running this command:
srun ls montecarlo.sif

You should see montecarlo.sif printed, confirming the compute node can access the file through shared storage.

  • Verify the data directory is also accessible from a compute node by running:
srun ls /tmp/simdata/config.json

You should see /tmp/simdata/config.json printed. Both the SIF image and data are accessible from all nodes.

Not seeing the file from the compute node?

If srun reports the file is missing, your shared filesystem may not be mounting /tmp across nodes. Copy the data to your home directory instead:

cp -r /tmp/simdata ~/simdata

Then adjust the bind path in the job script later to use ~/simdata:/data instead.

help me troubleshoot why my compute nodes cannot see /tmp/simdata

Write the Slurm batch script

A Slurm batch script tells the scheduler what resources you need and what command to run. The scheduler picks a compute node, allocates resources, and runs your containerized workload there.

  • Create the batch script by running this command:
cat > montecarlo_job.sh << 'EOF'
#!/bin/bash
#SBATCH --job-name=montecarlo
#SBATCH --output=slurm-%j.out
#SBATCH --nodes=1
#SBATCH --ntasks=1
#SBATCH --time=00:05:00

apptainer run --bind /tmp/simdata:/data montecarlo.sif 10000000
EOF

What does this script do?

  • #SBATCH --job-name=montecarlo gives the job a readable name in the queue.
  • #SBATCH --output=slurm-%j.out sends output to a file named with the job ID.
  • #SBATCH --nodes=1 and --ntasks=1 request one node with one task slot.
  • #SBATCH --time=00:05:00 sets a 5-minute time limit so the job does not run forever if something goes wrong.
  • The final line runs your container with the bind mount and passes 10000000 samples as the argument.
  • Verify the script was created by running:
cat montecarlo_job.sh

You should see the full script contents printed, matching what you wrote above.

Seeing unexpected characters or empty output?

Make sure you included the EOF markers exactly as shown. The heredoc syntax requires EOF on its own line with no leading spaces.

help me fix my heredoc script creation

Submit the job and check the output

Everything is in place. Your SIF is on shared storage, your data is accessible, and your batch script is ready.

Before you submit, what do you think the output file will contain? Consider what montecarlo.py prints when it runs.

  • Submit the job to Slurm by running:
sbatch montecarlo_job.sh

Slurm prints a job ID when it accepts the submission. Note this number.

  • Watch the job in the queue by running:
squeue

You should see your montecarlo job listed. It will show R (running) or PD (pending) in the state column. Wait until it disappears from the queue, which means it completed.

  • Once the job finishes, check the output file by running (replace the job ID with yours):
cat slurm-*.out

You should see output like this, printed by a compute node:

Host: node01
Samples: 10,000,000
Pi estimate: 3.141XXX
Error: 0.000XXX

What does this prove?

The Host: line shows the compute node name, not your login node. Your container ran on a completely different machine, yet produced results identically to when you ran it locally.

No Python was installed on that compute node. No NumPy was installed on that compute node. Everything came from the single SIF file on shared storage.

Job failed or output file is empty?

  • Run sacct -j <jobid> --format=State,ExitCode to see why the job failed.
  • If you see a bind mount error, confirm /tmp/simdata exists on the compute node with srun ls /tmp/simdata/.
  • If the output shows a Python error, run apptainer run montecarlo.sif 1000 locally to reproduce and debug it.

help me debug my failed Slurm job

✔️ Awesome, I've got everything!

Great job. You just ran a containerized scientific workload on a compute node through Slurm.

ⓧ I'd like to double check the full code

Here is the complete montecarlo_job.sh script:

#!/bin/bash
#SBATCH --job-name=montecarlo
#SBATCH --output=slurm-%j.out
#SBATCH --nodes=1
#SBATCH --ntasks=1
#SBATCH --time=00:05:00

apptainer run --bind /tmp/simdata:/data montecarlo.sif 10000000

Your container now runs production-style through a job scheduler. Every compute node executed the identical environment from one SIF file. Ready to scale this across multiple nodes with different parameters? The Secret Mission has you covered.

Secret mission

Scale Out with a Job Array

Your single-job submission proved the container works on a compute node. But real HPC workloads run the same computation with different parameters across many nodes simultaneously. Can you use Slurm job arrays to launch parameterized container runs and collect results from across your cluster?

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely on your local cluster, so there are no ongoing cloud costs.

Resources you used:

  • Apptainer 1.5.3 installed via PPA on cluster nodes.
  • SIF container images: centos_stream9.sif and montecarlo.sif (on shared storage).
  • Project files: montecarlo.def, montecarlo.py, montecarlo_job.sh, montecarlo_array.sh, params.txt.
  • Test data directory: /tmp/simdata/.
  • Slurm output files: slurm-*.out in your working directory.

Keep everything running

No action needed. Choose this if you plan to keep experimenting with Apptainer containers on your cluster. Your SIF images and scripts are ready to use whenever you return.

  • Apptainer stays installed and ready for future container work.
  • Your SIF images remain on shared storage for any node to execute.
  • Slurm output files stay in your working directory for reference.

Pause - I'll come back to this later

Apptainer is daemonless, so there are no running processes to stop. Your SIF images and scripts will sit idle on disk until you return. Optionally, remove the large SIF files to reclaim disk space.

  • Remove the pulled CentOS image to free approximately 100MB by running:
rm centos_stream9.sif
  • Keep montecarlo.sif on shared storage if you plan to run more Slurm jobs later.
  • Keep your .def and .py source files so you can rebuild the container anytime.

Delete - I don't want to use this again

Remove all project resources and uninstall Apptainer from your cluster.

  • Remove all project files and SIF images from your working directory by running:
rm -f centos_stream9.sif montecarlo.sif montecarlo.def montecarlo.py montecarlo_job.sh montecarlo_array.sh params.txt slurm-*.out
  • Remove the test data directory by running:
rm -rf /tmp/simdata
  • Uninstall Apptainer and remove the PPA by running:
sudo apt remove -y apptainer
sudo add-apt-repository --remove ppa:apptainer/ppa
sudo apt update
  • If you installed Apptainer on multiple nodes via Ansible, run the same removal commands on each node.

Nice Work!

Nice Work!

You just built and deployed a production-ready scientific container across your HPC cluster. From a single definition file to a Slurm-scheduled workload, you now have the full container engineering workflow that HPC teams use in production.

You've learned how to:

  • Write an Apptainer definition file and build it into a portable SIF image containing a reproducible Python scientific environment.
  • Use bind mounts to connect host storage into containers, giving your workloads access to external data without baking it into the image.
  • Submit containerized workloads through Slurm, running the same immutable container on compute nodes without any per-node installation.
  • Secret Mission: Scaled out with Slurm job arrays that run parameterized container jobs across multiple nodes.

Ready to quiz yourself?