Conda environments with Apptainer

If an application provides Conda installation instructions or an environment.yml file, install that environment inside a container. Build the container once, save its SIF image on /project, and reuse it in Slurm jobs. Neither a host Conda installation nor conda activate is needed to run it. This example uses the existing apptainer/1.4.4 module and micromamba inside the image.

For an overview of the available installation methods, see Installing Python packages.

The environment is installed at /opt/env inside the container. This path remains the same in every job, preserving absolute paths in installed packages. The SIF file itself can be moved or shared. Independent jobs can use the same read-only image, with separate writable temporary directories. Apptainer manages the mounts; no user-maintained mount helper or shared writable overlay is needed.

Prepare the environment and container definition

Download these files into the same directory on your project storage:

The sample environment contains Python and NumPy:

channels:
  - conda-forge
  - nodefaults
dependencies:
  - python=3.11
  - numpy=2.4
  - pip

For your application, replace the sample dependencies and channels with the upstream requirements. Remove a machine-specific prefix: from an exported YAML file; the definition explicitly chooses /opt/env. Packages available only through pip can be included in a pip: dependency subsection. If the upstream installation also requires source files, build tools or external data, include those steps explicitly in the definition; the YAML alone may not be the complete installation procedure.

The definition copies the YAML into the image, installs the environment, records the resolved packages, and removes the package cache:

Bootstrap: docker
From: mambaorg/micromamba:2.9.0

%files
    environment.yml /opt/environment.yml

%post
    set -eu
    export MAMBA_ROOT_PREFIX=/opt/conda
    micromamba create --yes --prefix /opt/env --file /opt/environment.yml
    micromamba list --prefix /opt/env --explicit > /opt/conda-explicit.txt
    /opt/env/bin/python -m pip freeze > /opt/pip-freeze.txt
    micromamba clean --all --yes

%environment
    export PATH="/opt/env/bin:$PATH"
    export PYTHONNOUSERSITE=1
    export PYTHONDONTWRITEBYTECODE=1

%runscript
    exec micromamba run --prefix /opt/env "$@"

%test
    /opt/env/bin/python -c 'import numpy as np; assert np.arange(5).sum() == 10; print("NumPy test passed")'

The %runscript activates the environment through micromamba run, including package activation hooks, and runs the supplied command. %test checks the sample NumPy installation; replace it with a smoke test for your own application. Keep the definition and environment file with the image. For reproducible builds, pin the base image by digest and use exact package versions or an upstream lockfile supported by micromamba; the sample version constraints alone are not a complete lockfile. The image also contains /opt/conda-explicit.txt and /opt/pip-freeze.txt as build records.

Build on a compute node

Request an interactive allocation, then change to the directory containing the downloaded files:

srun -p test -N 1 -n 1 -c 8 --mem-per-cpu=4000 --time=01:00:00 --pty bash -l
cd "/project/${project}/container-recipe"
module load apptainer/1.4.4

The test partition has a one-hour limit; use an appropriate partition for longer builds. Inside this allocation, run:

(
    set -euo pipefail
    IMAGE_DIR="/project/${project}/images"
    mkdir -p "$IMAGE_DIR"
    BUILD_WORK=$(mktemp -d /tmp/conda-container-build.XXXXXXXX)
    trap 'rm -rf -- "$BUILD_WORK"' EXIT
    export APPTAINER_TMPDIR="$BUILD_WORK/tmp"
    export APPTAINER_CACHEDIR="$BUILD_WORK/cache"
    mkdir -p "$APPTAINER_TMPDIR" "$APPTAINER_CACHEDIR"

    apptainer build --fakeroot "$IMAGE_DIR/environment-v1.sif" environment.def
    sha256sum "$IMAGE_DIR/environment-v1.sif" > "$IMAGE_DIR/environment-v1.sif.sha256"
)

No sudo is required. The %files source path is relative to the build’s working directory, so keep environment.yml beside environment.def and run the command there. The first build needs network access to the base-image registry and package channels.

Both the temporary build tree and the Apptainer cache must stay on local storage. Otherwise Apptainer’s default cache under $HOME/.apptainer would put build files on Lustre. On the tested node, /tmp is RAM-backed: allow memory for the unpacked container, downloaded packages and build processes, not just the final compressed image. df reports node-wide space, not your job’s memory limit. The subshell removes its temporary build directory on exit.

Run from Slurm

The downloadable job script uses one node, activates the container environment, and forwards the application command and arguments:

#!/bin/bash
#SBATCH --job-name=conda-container
#SBATCH --partition=test
#SBATCH --nodes=1
#SBATCH --ntasks=1
#SBATCH --cpus-per-task=8
#SBATCH --mem-per-cpu=4000
#SBATCH --time=00:10:00

# Usage: sbatch run-environment.sbatch \
#        "/project/${project}" "/project/${project}/images/environment-v1.sif" \
#        python simulation.py
set -euo pipefail
if (( $# < 3 )); then
    echo "Usage: PROJECT_DIRECTORY IMAGE COMMAND [ARG...]" >&2
    exit 1
fi
PROJECT_DIR=$(realpath -e -- "$1")
IMAGE=$(realpath -e -- "$2")
shift 2
module load apptainer/1.4.4

JOB_WORK=$(mktemp -d /tmp/conda-container.XXXXXXXX)
trap 'rm -rf -- "$JOB_WORK"' EXIT
mkdir -p "$JOB_WORK/cache" "$JOB_WORK/mamba"

# The image is shared read-only; each job has its own writable scratch space.
srun apptainer run --cleanenv --contain \
    --bind "$PROJECT_DIR:/work" --bind "$JOB_WORK:/jobtmp" --pwd /work \
    --env TMPDIR=/jobtmp,XDG_CACHE_HOME=/jobtmp/cache,MAMBA_ROOT_PREFIX=/jobtmp/mamba \
    --env NUMBA_CACHE_DIR=/jobtmp/cache/numba,MPLCONFIGDIR=/jobtmp/cache/matplotlib \
    --env OMP_NUM_THREADS="$SLURM_CPUS_PER_TASK",OPENBLAS_NUM_THREADS="$SLURM_CPUS_PER_TASK" \
    --env NUMBA_NUM_THREADS="$SLURM_CPUS_PER_TASK" \
    "$IMAGE" "$@"

For a first check, submit it from the login node:

sbatch run-environment.sbatch \
    "/project/${project}" "/project/${project}/images/environment-v1.sif" \
    python -c 'import numpy as np; print(np.arange(5).sum())'

The Slurm output should contain 10. To run your own program, place simulation.py in your project directory and replace the last line with python simulation.py. The project directory is visible as /work inside the container, which is also its working directory; write persistent results there. Add explicit bind mounts if your application needs other project or scratch directories. Adapt the partition, time, CPU and memory settings to your workload; the supplied settings are for a short test.

apptainer run invokes this image’s %runscript. apptainer exec runs a command directly and bypasses that activation step. For an interactive check inside an existing allocation, use the same apptainer run options as the job script, replacing the application command with bash.

The job script isolates the home and temporary directories with --contain, clears inherited environment settings with --cleanenv, and keeps application caches on job-local storage. User Python site-packages are disabled in the image. Temporary files are discarded at job exit; cleanup after a forced kill or node failure may require the site’s normal temporary-directory cleanup.

Update and share the image

Keep the SIF image and its build inputs on project storage. Other users with read access can use the same image in their own jobs; the job script gives each job its own temporary directories.

To update packages, rebuild a new image version. Do not replace an image while jobs are using it. GPU and multi-node MPI applications need additional configuration; see the container documentation and the application’s requirements. This recipe validates a general Python/NumPy environment, not every Conda application’s compatibility.

Further details: Apptainer definition files, build/cache configuration, and micromamba base images.