8Backends and Runtimes: Nix, Podman, and Apptainer
Using Singularity containers, developers can work in reproducible environments of their choosing and design, and these complete environments can easily be copied and executed on other platforms.
Kurtzer, Sochat, and Bauer, Singularity: Scientific Containers for Mobility of Compute (2017)
8.1 Learning objectives
By the end of this chapter the reader should be able to:
Explain why Docker is one route to the pinned-environment level and not the only one.
Distinguish the three axes along which the framework organizes the alternatives: a different runtime over the same image, a different environment model without a container, and remote execution that moves the container off the analyst’s machine.
Run a container image under Podman or Apptainer, and read a small default.nix declaration produced by rix, without conflating the three.
Choose a backend or runtime to fit a concrete setting: a high-performance cluster, a Windows-only laboratory, a reviewer who must install nothing, a project demanding maximum rigor, or an institution with a licensing constraint.
State honestly which reproducibility level each option reaches, and match the claim to the level.
8.2 Orientation
In Chapter 7 we reached the pinned-environment level, L2 on the ladder of Chapter 3, by packaging an analysis inside a Docker image: an image fixes the operating system, the system libraries, and the R interpreter that a lockfile alone leaves to chance. It is easy to come away from that chapter believing that Docker is reproducibility, that the two words name the same thing. They do not. Docker is one instrument for reaching L2, and a reader who mistakes the instrument for the goal will be at a loss the first time Docker is unavailable, unwanted, or forbidden.
Consider three health scientists who cannot simply follow the Docker recipe of the previous chapter. The first runs a large simulation study that must execute on a shared university cluster, where the scheduler will not grant her the administrative privileges a Docker daemon requires. The second works in a laboratory of Windows machines and a pipeline that mixes R with Python, a combination that sits awkwardly inside the Rocker images. The third has prepared a teaching blog and wants a reviewer to re-run a single post in a browser, installing nothing at all. Each needs the pinned environment that L2 names; none of them can obtain it in the ordinary Docker way. The purpose of this chapter is to show that the ladder does not depend on any one tool, and to equip the reader to choose among the alternatives with judgment rather than habit.
The framework this book uses organizes those alternatives along three axes. The organization is worth internalizing before any command is typed, because it prevents the common error of treating every unfamiliar tool as a special case. The first axis changes only the runner: a different program runs the very same image, and nothing about its pinning, its packages, or its base is reinstalled. The second axis changes the environment model itself, dispensing with the container and declaring the environment directly, so that the pinned environment is reached by a different mechanism. The third axis keeps the container but moves it off the analyst’s machine, into a browser or a hosted service, so that the person reproducing the work provides no environment of their own. A tool belongs to exactly one axis, and knowing which axis a tool occupies tells the analyst almost everything about what it does and does not change.
8.3 The analyst’s contribution
No tool in this chapter chooses itself. Three judgments remain the analyst’s, and they are the substance of the chapter.
Diagnose the constraint before selecting the tool. The decisive question is rarely which tool is best in the abstract but which constraint is binding: an absence of administrative rights on a cluster, an operating system, a mixed-language pipeline, a licensing term, a reviewer’s patience, a demand for maximum rigor. The analyst identifies the binding constraint first, and the constraint usually selects the axis, and often the tool.
Locate each alternative on the correct axis. A great deal of confusion in practice comes from treating Podman, Nix, and Binder as interchangeable answers to one question. They are answers to three different questions. The analyst who keeps the axes distinct will not, for example, expect Podman to solve a Windows-and-Python problem, nor expect Nix to spare a reviewer an installation.
State the level each option actually reaches. A Podman run of a digest-pinned image reaches L2 exactly as the Docker run did; a Nix declaration reaches L2 without a container; a Binder session reaches L2 for the duration of the session but pins nothing on the reviewer’s own machine. The analyst reports the level honestly and never lets the novelty of a tool inflate the claim.
NoteThe vocabulary of this chapter
Runtime. The program that runs a container image: Docker, Podman, or Apptainer. The runtime is interchangeable when the image is a standard OCI image.
OCI image. An image built to the Open Container Initiative specification, the common format that Docker, Podman, and Apptainer can all consume. A Docker image is an OCI image.
Daemon. A background system service that runs continuously with elevated privilege. Docker relies on one; Podman and Apptainer do not, which is why they run without administrative rights.
Rootless. Able to run under an ordinary user account, without the root superuser privilege. A requirement on shared clusters, where users are not administrators.
Backend. The mechanism that supplies the environment. Docker is the container backend; Nix is a container-free backend that declares the environment directly.
Purely functional package manager. A package manager, such as Nix, in which the installed result is a deterministic function of the declared inputs, so that the same declaration yields a deterministic, reproducible environment.
Input-addressed. Identified by a hash of the build inputs rather than by a mutable name, so that a reference cannot silently point at different content later.
SIF. The Singularity Image Format, a single-file archive that Apptainer produces from an image, suitable for depositing alongside a compendium.
8.4 Axis one: a different runtime over the same image
We begin with the cheapest of the three changes, because it changes the least. The image built in Chapter 7, pinned to its base by content digest and installing its packages from a dated snapshot, is a standard OCI image. Docker is the program that ran it, but the image does not belong to Docker; any OCI-compatible runtime can run it. Selecting a different runtime is therefore a parameter, not a redesign: the base image, the digest pin, the dated snapshot, and the lockfile all carry over unchanged, and only the command that launches the container differs.
8.4.1 Podman
Podman is a daemonless, rootless, Docker-command-compatible runtime. Where Docker interposes a background daemon running with elevated privilege, Podman runs the container directly under the invoking user’s account, and its command-line interface is close enough to Docker’s that podman run stands in for docker run in most cases:
Two properties recommend Podman to the health scientist. The first is that, being daemonless and rootless, it runs on a multi-user Linux machine where an analyst has no administrative rights, a situation in which Docker’s daemon is often simply unavailable. The second is a matter of licensing rather than technology, and it is worth stating plainly because it catches many university users unawares. Docker Desktop, the packaged Docker distribution for macOS and Windows, is under its current terms not free for use by larger commercial and institutional organizations, and many universities fall within those terms. The terms do carve out an exception for education, but institutions differ on whether a given use qualifies. The point is contested, which is why many institutions either license Docker Desktop or avoid it. Podman carries no such restriction. For an analyst whose institution has declined to license Docker Desktop, Podman reaches exactly the same L2 environment, from the same image, at no licensing cost.
8.4.2 Apptainer
Apptainer, known until its move to the Linux Foundation as Singularity, is the runtime built for high-performance computing (Kurtzer et al., 2017). Kurtzer and colleagues designed it for precisely the setting that defeats Docker on a cluster. It runs unprivileged, integrating with the batch schedulers, message-passing libraries, and graphics processors that scientific clusters provide. A user with no administrative rights can nonetheless run a containerized analysis under the cluster’s job system.
Two rules govern its use, and observing them keeps the image pinning of Chapter 7 intact. The first is that an Apptainer image should be built from the digest-pinned Docker image, never authored independently, so that the same content digest carries through:
The second is that the single file Apptainer produces, the SIF, is itself an archival artifact. Unlike a Docker image, which lives in a layered local store, an Apptainer image is one file that can be checksummed, deposited in a data repository, and cited alongside the compendium. It is thus both the thing that runs the analysis on the cluster and the thing that is archived to let others run it later, a convenient coincidence for the verification of Chapter 12.
The expected pattern, and the one we recommend, is to develop on the laptop with Docker or Podman and to run at scale on the cluster with Apptainer, all from a single image. Nothing is reinstalled for the cluster; Apptainer repackages the pinned image into SIF, carrying the content over from the same digest and converting only the format, so the same pinned environment runs under a different program.
This is the point at which the archetypes of Chapter 5 begin to matter. A simulation study, which may need to run thousands of replicates across the nodes of a cluster, is the archetype that most naturally reaches for Apptainer: the SIF file travels to the cluster, the scheduler runs it unprivileged across many cores, and the same file is later deposited as the archival record of the environment. A plain data-analysis project that runs comfortably on a laptop has no such need and is better served by Docker or Podman. The archetype does not change the image; it changes which runtime is the natural fit.
NoteCheck your understanding: what changes with the runtime
An analyst has a Docker image pinned as FROM rocker/verse@sha256:... with packages installed from a dated snapshot. She switches from Docker to Podman to avoid a licensing constraint. Which of the following change: the base image, the pinned digest, the installed package versions, the reproducibility level reached?
NoteAnswer
None of them change. The runtime is a parameter over the same OCI image. The base image, its digest, and the package versions are all properties of the image, not of the program that runs it, and the level reached is L2 in either case. Only the launching command differs (podman run for docker run). This is exactly why a runtime swap is the cheapest of the three axes: it changes the runner and nothing else.
8.5 Axis two: a different environment model without a container
The second axis is a deeper change. Here we abandon the container altogether and declare the environment directly, reaching the pinned environment by a different mechanism. The container is not run by a different program; it is not run at all, because there is none.
8.5.1 Nix and the rix front-end
Nix is a purely functional, input-addressed package manager (Dolstra et al., 2004). The phrase deserves explanation. ‘Purely functional’ means that the installed environment is a deterministic function of a declaration: given the same declaration, Nix reconstructs the same package set and dependency graph, on any machine, at any later time. ‘Input-addressed’ means that every built component is identified by a hash of its inputs, so that a reference cannot silently come to mean something else, the mutable-tag failure we guarded against in Chapter 7 by pinning digests. Dolstra, in the work that introduced Nix, made deterministic build the organizing principle rather than an afterthought, and it is that property that recommends Nix here.
The decisive fact for our purposes is that a single Nix declaration fixes the R version, the R packages, and the system libraries together. It therefore reaches the pinned-environment level, L2, without a container at all. This is why the ladder of Chapter 3 speaks of ‘a Docker or Nix image’: Nix is not a supplement to the container, in the way that renv is a supplement, but a peer of it, an alternative road to the same rung.
The obstacle is that the Nix language is unfamiliar to almost every biostatistician, and this is the dominant reason it is not the default. The rix package removes much of that obstacle by generating the Nix declaration from R, in R idiom that a health scientist already knows (Rodrigues & Baumann, 2024). Rodrigues and Baumann designed rix so that the analyst writes a familiar R call and rix writes the Nix:
That call produces a default.nix file, the declaration that pins the environment. A reader need not master the Nix language to recognize its shape; a minimal default.nix reads, in outline, as follows:
The pinned nixpkgs revision, written as <rev> above, is the analogue of the base-image digest: it fixes the exact set of package definitions from which the environment is built. Entering the environment is then a single command, and no image is built:
$ nix-shell default.nix
Newer Nix workflows prefer a flake, a flake.nix paired with a generated flake.lock that records the exact pin, the direct analogue of renv.lock. Because flakes remain behind an experimental flag in current Nix, the framework tolerates both the classic default.nix and the flake form, and the reader will encounter both in the wild.
We should be candid about the trade-offs, in the two-sided manner this book prefers. On the plus side, Nix delivers the entire environment pillar in one mechanism and reaches L2 without the container machinery. It gives deterministic, reproducible environments that Docker, whose reproducibility depends on careful digest and snapshot discipline, approaches but does not guarantee by construction. On the minus side, the Nix model is genuinely alien to the audience, and although rix hides much of the language, diagnosing a build that has failed still calls for some Nix knowledge. The first build is slow and consumes substantial disk. Nix is a host prerequisite comparable to Docker, requiring Linux, macOS, or the Windows Subsystem for Linux. And rix is young enough that its own version must be pinned. These caveats are the reason Nix is carried as an opt-in backend for the analyst who wants maximum rigor, while renv with a container remains the accessible default. The choice is real, and it is not free either way.
8.5.2 Guix and the conda family
Two neighbors of Nix deserve mention, because a reader will meet them and should know where they sit. GNU Guix is Nix’s close sibling, sharing the purely functional, input-addressed model, with a smaller R ecosystem; for the health scientist it is best understood simply as Nix’s relation, reaching the same rung by the same idea.
The conda family occupies a genuinely different middle ground, and it answers two constraints the container route handles awkwardly. Conda (Anaconda Inc., 2023), with the faster resolver mamba and the lockfile-oriented pixi(prefix.dev, 2024), reproduces natively on Windows, without the Linux-emulation layer a Docker image requires there. It also spans R and Python in a single environment, rather than forcing the mixed-language analyst to bridge two toolchains. For a Windows-only laboratory running a pipeline that mixes the two languages, this is a decisive practical advantage. The cost is rigor: conda draws its packages from conda-forge rather than from CRAN or the Posit Package Manager, and it pins less strictly than Nix. It is, in the honest accounting, a middle-ground option, more reproducible than an unpinned install and less exacting than Nix. It is the right choice precisely when its two native strengths, Windows and mixed languages, are the binding constraint.
ImportantThe honest level
It is tempting, having adopted a tool as sophisticated as Nix, to describe an analysis as ‘fully reproducible’ on the strength of the tool’s reputation. Resist this. Nix reaches L2, the pinned environment, and reaches it well; it does not by itself reach L3, verification, which requires actually regenerating the recorded outputs and showing they match, as Chapter 12 develops. A default.nix in the repository is a claim of L2, no more, until the outputs have been reproduced. Match the claim to the rung, whichever backend supplied it.
8.6 Axis three: remote execution
The third axis keeps the container but moves it off the analyst’s machine. The pinning is unchanged; what changes is who runs it. This axis exists to answer a specific limitation, the learning curve and installation burden that a container imposes on the person reproducing the work, rather than to replace the container format.
Binder runs a compendium’s container in the browser, so that a reviewer or reader installs nothing whatever (Project Jupyter et al., 2018). Jupyter’s Binder service reads the configuration files already present in a well-formed compendium, builds the environment on a hosted server, and serves it as an interactive session reached by a single link. For the health scientist the appeal is exact: a reviewer clicks a link and is inside the running environment, with no Docker, no Nix, and no local installation of any kind. The corresponding limitation is equally exact, and it returns us to the honest-level theme: the Binder session reaches L2 for the life of the session, on a server that pins the environment. But it pins nothing on the reviewer’s own machine, and it does not persist. It is superb for demonstration and review, and it is not an archival guarantee.
Development containers, and the hosted Codespaces service built on them, bring the same pinned environment into an editor rather than a browser (Development Containers Project, 2022). A .devcontainer/ configuration in the compendium describes the environment, and a compatible editor or the Codespaces service builds and opens it, so that a collaborator edits and runs inside the pinned environment without assembling it locally. The principle is the same presence-driven one we have met throughout: the configuration file’s existence drives the automated build.
Here again the archetype guides the choice. A blog or a manuscript archetype, whose whole purpose is to be read and its results inspected by others, benefits most from browser execution. A reader of a teaching post, or a reviewer of a manuscript, re-runs the analysis in Binder with nothing to install, which is exactly the low-friction path such archetypes want. A blog that renders many posts gains the most, since each post becomes independently re-runnable by a reader at a link. A simulation archetype, by contrast, gains little from browser execution and much from the cluster, which is the Apptainer story of the first axis. The reproducibility machinery is the same across all archetypes; the archetype tells the analyst which face of it to present.
8.7 Choosing among the alternatives
Having laid out the three axes, we can now give the concrete guidance the chapter exists to deliver. The governing question, as the analyst’s second contribution insisted, is which constraint binds. We answer it by setting the guidance beside a compact comparison, generated at build time from the same tibble that a reader could inspect directly.
library(tibble)library(knitr)comparison <-tibble(name =c("Docker", "Podman", "Apptainer","Nix (via rix)", "Guix", "conda / pixi","Binder", "Devcontainer / Codespaces" ),axis =c("runtime", "runtime", "runtime","backend", "backend", "backend","remote", "remote" ),pins =c("the image (OS, R, libraries)","the image (OS, R, libraries)","the image, as one SIF file","R, packages, system libraries","R, packages, system libraries","R and Python packages, less strictly","the image (server-side, per session)","the image (in the editor)" ),container =c("yes", "yes", "yes","no", "no", "no","yes (hosted)", "yes (hosted)" ),best_fit =c("the default local runtime","licensing constraint; rootless Linux","an HPC cluster; archival SIF","maximum rigour; deterministic builds","a Nix-aligned site","Windows-only or mixed R and Python","a reviewer who installs nothing","a collaborator editing in the environment" ))kable( comparison,col.names =c("Name", "Axis", "What it pins","Container?", "Best-fit setting" ))
Table 8.1: Backends and runtimes for reaching the pinned environment, by axis
Name
Axis
What it pins
Container?
Best-fit setting
Docker
runtime
the image (OS, R, libraries)
yes
the default local runtime
Podman
runtime
the image (OS, R, libraries)
yes
licensing constraint; rootless Linux
Apptainer
runtime
the image, as one SIF file
yes
an HPC cluster; archival SIF
Nix (via rix)
backend
R, packages, system libraries
no
maximum rigour; deterministic builds
Guix
backend
R, packages, system libraries
no
a Nix-aligned site
conda / pixi
backend
R and Python packages, less strictly
no
Windows-only or mixed R and Python
Binder
remote
the image (server-side, per session)
yes (hosted)
a reviewer who installs nothing
Devcontainer / Codespaces
remote
the image (in the editor)
yes (hosted)
a collaborator editing in the environment
Read as guidance, the table resolves into a short set of recommendations, each keyed to a binding constraint. When the destination is an HPC cluster and the analyst has no administrative rights, the choice is Apptainer, built from the same digest-pinned image and run under the scheduler, with the SIF file doubling as the archival deposit. When the setting is a Windows-only laboratory, or the pipeline mixes R and Python, the choice is the conda family, pixi in particular, which reproduces natively on Windows and spans both languages at the cost of strict pinning. When a reviewer must install nothing, the choice is Binder, which serves the running environment in a browser for the life of a session. When the project demands maximum rigor and the team can absorb the learning curve, the choice is Nix through rix, which gives deterministic, reproducible builds and reaches L2 without a container. And when the obstacle is a licensing constraint, an institution that has not licensed Docker Desktop, the choice is Podman, which reaches the identical L2 environment from the identical image at no licensing cost. The default, for the analyst under none of these constraints, remains Docker with renv, the composition of Chapter 7.
NoteCheck your understanding: match the constraint to the axis
For each situation, name the axis and the tool. (a) A genomics simulation must run ten thousand replicates on a university cluster where you are not an administrator. (b) A reviewer of your manuscript should re-run the main figure without installing anything. (c) Your laboratory runs Windows and your pipeline calls both R and Python.
NoteAnswer
Axis one, a different runtime over the same image: Apptainer, built from the digest-pinned image and run under the scheduler, with the SIF as the archival record. (b) Axis three, remote execution: Binder, which serves the container in a browser per session. (c) Axis two, a different environment model: the conda family, and pixi in particular, the one option that reproduces natively on Windows and spans both languages. Note that no single tool answers all three; each constraint selects a different axis.
8.8 Worked example
To make the axes concrete, consider a single research group with one image and three destinations, which is a common situation once a project matures. The group has built a pinned image for a cohort-analysis compendium, ghcr.io/lab/cohort-env, resolved to an immutable digest.
On the analyst’s own laptop, an institution that has declined to license Docker Desktop, the image runs under Podman with no change to the image and no licensing cost:
$ podman run --rm-v"$(pwd)":/project \ ghcr.io/lab/cohort-env@sha256:9f2c... \ Rscript -e'source("analysis/run.R")'
When a sensitivity analysis must be repeated across many simulated cohorts, the work moves to the university cluster. The analyst builds a SIF from the same digest and submits it to the scheduler; nothing is rebuilt, and the environment is provably the one used on the laptop:
Finally, when the paper is submitted, a reviewer wishes to re-run the principal figure without installing anything. A .binder/ configuration in the compendium lets Binder build the same environment on a hosted server and serve it in the browser, reached by a single link in the cover letter.
Three destinations, three tools drawn from two of the three axes, and one image throughout. The pinning was done once, in Chapter 7; this chapter merely ran it in three places. Had the group instead chosen Nix from the outset, the image would have been replaced by a default.nix, and the laptop and cluster runs would have entered the environment through nix-shell rather than a runtime. The discipline, one pinned declaration exercised in several places, would have been identical.
8.9 Collaborating with an LLM
A language model is a capable assistant for the mechanics of this chapter and a poor judge of its trade-offs. The following triples show where each stands.
Translating a Docker command to another runtime.
Prompt. ‘Convert this docker run command to the equivalent Podman and Apptainer commands, preserving the bind mount and the digest-pinned image.’
Watch for. A model may drop the --platform flag, may invent an Apptainer --volume syntax that differs from the real one, or may translate a mutable tag rather than the digest, quietly undoing the pin.
Verification. Run the translated command and confirm it executes the analysis; confirm the digest, not a tag, appears in the Apptainer docker:// reference; and confirm the bind mount exposes the working tree as expected.
Generating a Nix declaration.
Prompt. ‘Write a rix::rix() call that pins R 4.4.1 with dplyr, ggplot2, and targets, producing a default.nix.’
Watch for. A model may hand-write raw Nix rather than the rix call requested, may omit the nixpkgs revision that is the whole point of the pin, or may name packages that do not exist under pkgs.rPackages.
Verification. Run the rix call, inspect the generated default.nix for a concrete pinned revision, and enter the environment with nix-shell to confirm the packages load. A declaration that builds is the only evidence that counts.
Advising on which alternative to adopt.
Prompt. ‘My analysis mixes R and Python and my lab uses Windows. Should I use Nix, Podman, or conda?’
Watch for. A model may recommend the most fashionable tool rather than the one that fits the constraint, and may overstate a tool’s rigor, presenting conda as reaching the same level as Nix.
Verification. Check the recommendation against the binding constraint: mixed languages on Windows point to the conda family, whatever a tool’s general reputation. The analyst, not the model, owns the honest statement of the level reached.
8.10 Exercises
Take a Docker image you have built in a previous chapter and run it under Podman without rebuilding it. Confirm that the container starts and that the analysis runs. State, in one sentence, why the reproducibility level is unchanged.
Build an Apptainer SIF file from a digest-pinned docker:// reference. Compute a checksum of the SIF and explain how that single file could serve both as the cluster runtime and as an archival deposit.
Using rix::rix(), generate a default.nix that pins a specific R version and three packages. Locate the pinned nixpkgs revision in the generated file and explain what would change in the built environment if that revision were altered.
For each of the following, name the axis and the single most appropriate tool, and justify the choice in one sentence: (a) a laboratory forbidden from licensing Docker Desktop; (b) a manuscript reviewer who must run a figure with no local installation; (c) a simulation study destined for a shared cluster; (d) a Windows-only pipeline mixing R and Python.
A colleague claims that adopting Nix makes an analysis ‘fully reproducible’. Write a two-sentence reply that corrects the level claim without dismissing Nix, naming the rung Nix reaches and the rung it does not.
Consider a simulation archetype and a blog archetype built from the same compendium machinery. Describe which runtime or backend each most naturally uses and why the underlying pinning is nonetheless identical.
8.11 Solutions
Attempt an exercise before expanding its solution. The first three are hands-on and their outputs will vary by machine; the boxes record the reasoning and the expected shape of the result.
NoteExercise 1
podman run --rm-v"$PWD":/project -w /project \<image>@sha256:... Rscript analysis/scripts/run.R
The container starts and the analysis runs, with no rebuild, because Podman consumes the same OCI image format Docker produces. The command differs only in its first word.
Why the level is unchanged. The reproducibility level is a property of what is captured, and the image is the capture. The base image, its digest, the installed package versions, and the determinism variables are all properties of the image rather than of the program that launches it, so swapping the launcher changes nothing that the ladder grades. The analysis sits at L2 under either runtime.
This is the sense in which the runtime is the cheapest of the three axes: it changes the runner and nothing else. The one caveat worth noting is that rootless Podman handles file ownership on bind mounts differently from Docker, so the mechanics of the mount may need adjusting even though the environment inside does not change.
The build converts the OCI image into a single SIF file. The checksum is a hash of that file.
How one file serves both purposes. As a cluster runtime, the SIF needs no daemon and no root, which is what makes it acceptable on a shared HPC system where the user is not an administrator; the scheduler runs apptainer exec analysis.sif ... directly. As an archival deposit, the SIF is a single immutable file with a checksum, which is exactly the shape an archive wants: it can be uploaded to a repository, assigned a DOI, and verified on download by rehashing.
The property that unifies the two roles is that a SIF is a file rather than a service. A Docker image lives in a daemon’s storage and is addressed by a name that a registry must resolve; a SIF is bytes on disk that can be copied, hashed, and deposited like any other artifact. That is why the chapter treats it as the archival form of a container.
Two honest qualifications. The SIF is opaque: it records the environment without recording how it was made, so the Dockerfile remains the source of truth and the SIF is the built product. And SIF files are large, often several gigabytes, which some archives will not accept.
The pinned nixpkgs revision appears near the top of the generated default.nix, in the fetchTarball or fetchFromGitHub expression that imports nixpkgs. It is a full git commit hash of the nixpkgs repository, accompanied by a sha256 of the fetched archive.
What would change if the revision were altered. Essentially everything below the R packages, and some of the packages too. The nixpkgs revision determines the entire dependency graph: the R version itself, the C and Fortran compilers used to build it, the BLAS and LAPACK implementations, every system library, and the versions of the R packages available at that revision. Moving the revision moves all of them together.
This is the property that makes Nix interesting for reproducibility and is worth stating precisely: the revision is a single value that pins the whole stack from the system libraries upward, where a lockfile pins only the R packages and a container pins a built artifact rather than a recipe for one. It is also the property that makes Nix unforgiving, since there is no way to move one component without moving the revision, and moving the revision moves everything.
NoteExercise 4
(a) A laboratory forbidden from licensing Docker Desktop. Axis one, a different runtime over the same image: Podman. The images, the digests, and the recipes are unchanged, so the constraint is met at the cost of changing one word in the launch command.
(b) A reviewer who must run a figure with no local installation. Axis three, remote execution: Binder, which builds the environment from the repository and serves it in a browser for the session. The reviewer installs nothing.
(c) A simulation study destined for a shared cluster. Axis one again, but a different tool: Apptainer, because a shared cluster will not grant the root daemon Docker requires, and the SIF doubles as the archival record.
(d) A Windows-only pipeline mixing R and Python. Axis two, a different environment model: the conda family, and pixi in particular, which reproduces natively on Windows and spans both languages without a container.
The pattern the exercise is built to show is that no single tool answers all four, and that each constraint selects an axis before it selects a tool. Naming the axis first is what makes the choice principled rather than a matter of preference.
NoteExercise 5
A serviceable reply:
Nix pins the whole stack from the system libraries upward, and does it from a recipe rather than a built artifact, so it reaches L2 as cleanly as a container does and arguably more transparently. What it does not reach is L3, because pinning an environment is capture and L3 is validation: it needs evidence that the analysis was rebuilt from the pinned definition and regenerated its recorded outputs, which is a continuous-integration question rather than a Nix one.
The correction to make is the level claim, not the tool choice. ‘Fully reproducible’ is the phrase that overstates, and it overstates in the direction this book cares about: it asserts verification that has not been performed. The colleague is right that Nix is a strong capture mechanism and right to prefer it if they value a declarative recipe; they are wrong that capture is the whole ladder.
Worth adding if the conversation continues: Nix pins more of the stack than a lockfile and does not pin the hardware or the intrinsic non-determinism, so even the capture claim has a ceiling.
NoteExercise 6
The simulation archetype most naturally uses Apptainer on a scheduler. Its work is many independent replicates, which suits a cluster, and a cluster will not run a root daemon. The SIF is built once from the digest-pinned image and dispatched to every node.
The blog archetype most naturally uses remote execution, a CI runner that renders the site and publishes it, or Binder if readers are to run the posts themselves. Its work is a render on every push, which is small, and its audience is on the web.
Why the pinning is nonetheless identical. Both are built from the same compendium machinery: the same digest-pinned base image, the same dated snapshot, the same restored lockfile, the same determinism variables. The SIF is a conversion of that image, not a different environment, and the CI runner pulls that same image. Neither archetype changes what is captured; they differ only in where the captured environment is executed and by what program.
This is the chapter’s central claim in miniature. The backends and runtimes are an axis of delivery, and the reproducibility level is set on an axis of capture. A simulation on a cluster and a blog in CI can sit on exactly the same rung, because they are running the same pinned thing in different places.
8.12 Further reading
The high-performance-computing runtime is described by its authors in Kurtzer et al. (2017), which sets out the unprivileged, scheduler-integrated design and the single-file image format that make Apptainer, formerly Singularity, the natural cluster companion to a Docker workflow.
The foundational account of the purely functional, input-addressed model is Dolstra et al. (2004), and the R front-end that makes it approachable for the health scientist is documented in Rodrigues & Baumann (2024), whose rix package generates the Nix declaration from ordinary R code.
For the conda family as a middle-ground backend, the reference documentation of the conda ecosystem (Anaconda Inc., 2023) and of the lockfile-oriented pixi(prefix.dev, 2024) together describe the native-Windows and mixed-language reproduction that distinguishes it from the container route.
The remote-execution axis is introduced by Project Jupyter et al. (2018), whose Binder service runs a compendium’s container in the browser, and by the development-containers specification (Development Containers Project, 2022), which brings the same environment into an editor. Both rest on the presence-driven principle, a configuration file’s existence driving an automated build, that recurs throughout this book and is developed in Chapter 12.
The container backend against which all of these are alternatives is developed in Chapter 7, and the ladder of levels that lets us state honestly what each of them reaches is Chapter 3.
Development Containers Project. (2022). Development containers specification. https://containers.dev/
Dolstra, E., Jonge, M. de, & Visser, E. (2004). Nix: A safe and policy-free system for software deployment. Proceedings of the 18th USENIX Conference on System Administration (LISA ’04), 79–92.
Kurtzer, G. M., Sochat, V., & Bauer, M. W. (2017). Singularity: Scientific containers for mobility of compute. PLOS ONE, 12(5), e0177459. https://doi.org/10.1371/journal.pone.0177459
prefix.dev. (2024). Pixi: A fast software package manager. https://pixi.sh/
Project Jupyter, Bussonnier, M., Forde, J., Freeman, J., Granger, B., Head, T., Holdgraf, C., Kelley, K., Nalvarte, G., Osheroff, A., Pacer, M., Panda, Y., Perez, F., Ragan-Kelley, B., & Willing, C. (2018). Binder 2.0 — reproducible, interactive, sharable environments for science at scale. Proceedings of the 17th Python in Science Conference (SciPy 2018), 113–120. https://doi.org/10.25080/Majora-4af1f417-011
Rodrigues, B., & Baumann, P. (2024). Rix: Reproducible data science environments with Nix. https://docs.ropensci.org/rix/