Skip to content

Iso2 >>> Containerisation

Recommendation. When a package manager with pinned lock files is not enough, containerise the unit: write one Dockerfile per unit as its build definition and build one container image per unit, so that a dependency change in one unit can never affect another. When the study spans CPU architectures, publish multi-architecture images.

When this applies

Reach for a container when a unit's dependencies go beyond what conda can express: software built through a complex build system, packages not available on conda channels, legacy code that needs an old operating system, or a unit that must be fully independent of whatever the host provides. If Iso1 covers a unit, there is no need for this; many studies containerise only one or two units and manage the rest with conda.

Why

A conda environment isolates language-level dependencies but still sits on the host's operating system layer. A container packages that layer too: the unit ships with its own system libraries and userland, so it behaves the same on any host that can run containers. The one-image-per-unit rule is what preserves isolation; a shared "study image" reintroduces exactly the dependency conflicts isolation is meant to remove.

In practice

Each containerised unit gets a Dockerfile next to its code. A representative one for a Julia unit, which is a common containerisation case since Julia is awkward to manage through conda:

dockerfile
FROM julia:1.11

WORKDIR /app
COPY Project.toml Manifest.toml ./
RUN julia --project=. -e 'using Pkg; Pkg.instantiate()'
COPY src/ src/

ENTRYPOINT ["julia", "--project=.", "src/forward_model.jl"]

The order matters. Dependencies are installed before the source code is copied, so Docker's layer cache reuses the expensive dependency layer while we iterate on the code. We build and run it with:

bash
docker build -t ghcr.io/your-org/forward-model:0.1.0 .
docker run --rm -v "$PWD/data:/data" ghcr.io/your-org/forward-model:0.1.0

Docker is the most common build tool, but the Dockerfile format is a standard; Podman and other compatible runtimes build and run the same definitions. Tag images with a version and push them to a registry (GitHub Container Registry, Docker Hub) so collaborators pull a byte-identical image instead of rebuilding locally.

Container images are architecture-specific. An image built on an Apple Silicon laptop is an arm64 image and will only run under emulation, slowly, on an x86_64 cluster. When your collaborators span architectures, we build a multi-architecture manifest, which bundles per-architecture images under a single tag so the right one is pulled automatically:

bash
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t ghcr.io/your-org/forward-model:0.1.0 \
  --push .

buildx is a Docker CLI plugin, included in current Docker installations. Building the foreign architecture happens through emulation and is slower, which is one more reason to hand these builds to CI (Iso4).

Two pitfalls to avoid. Do not use the latest tag at unit boundaries; a unit's contract should name an exact image version, otherwise the environment silently drifts. And keep data out of images; images hold software, while data enters at run time through mounts or the exchange mechanisms of the Interoperation pillar.

Package management (Iso1) remains the lighter default for units that fit it, and a locked conda environment inside a minimal image is a useful middle ground, covered under Iso4. If the unit must run on an HPC cluster, the same Dockerfile feeds the Apptainer route (Iso3).