Skip to content

Containers

Containers package a node and all of its dependencies into a single, self-contained image. A containerized node runs identically regardless of what is installed on the host — no more “works on my machine” issues.

Use containers when you need:

  • Portability — ship a node to another machine without worrying about system dependencies.
  • Reproducibility — guarantee the same runtime environment every time.
  • Isolation — prevent conflicts between nodes that need different versions of the same library.

PeppyOS uses Apptainer as its container runtime. On macOS, Apptainer runs transparently inside a Lima virtual machine — no extra setup is needed.

Pass the --container flag to peppy node init:

Terminal window
peppy node init --toolchain uv --container my_node

This generates the same project scaffolding as a regular node, plus an apptainer.def file that describes how the container image is built.

A container node uses a container block instead of the usual process block. The two are mutually exclusive — a node is either a container node or a process node, never both.

peppy.json5
{
schema_version: 1,
manifest: {
name: "my_node",
tag: "0.1.0",
language: "python",
},
container: {
def_file: "apptainer.def",
},
interfaces: {}
}

The def_file field points to the Apptainer definition file relative to the node root. You can rename or relocate it as long as def_file matches.

Compare this with a standard process node, which defines add_cmd and start_cmd instead:

peppy.json5 (process node)
{
schema_version: 1,
manifest: {
name: "my_node",
tag: "0.1.0",
language: "python",
},
process: {
add_cmd: ["uv", "sync", "--no-editable"],
start_cmd: ["./.venv/bin/python", "-m", "my_node"]
},
interfaces: {}
}

Container nodes don’t need add_cmd or start_cmd — the definition file takes care of both building and running the node.

The generated definition file is a standard Apptainer definition file. Here is what peppy node init --container generates:

apptainer.def
Bootstrap: docker
From: ubuntu:24.04
%labels
Name my_node
Version 0.1.0
%environment
export PATH="/opt/my_node/.venv/bin:$PATH"
%files
. /opt/my_node
%post
set -eux
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y --no-install-recommends \
ca-certificates curl python3 python3-venv
rm -rf /var/lib/apt/lists/*
curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="/root/.local/bin:$PATH"
cd /opt/my_node
uv sync --no-editable
%runscript
cd /opt/my_node
exec ./.venv/bin/python -m my_node

Each section serves a specific purpose:

SectionPurpose
Bootstrap / FromBase image to build from (Ubuntu 24.04 by default)
%labelsMetadata embedded in the image
%environmentEnvironment variables set when the container runs
%filesCopies the node source into the image at /opt/<node_name>
%postBuild steps — install system packages, toolchains, and compile the node
%runscriptEntry point executed when the container starts

Adding a container node works the same as a regular node:

Terminal window
peppy node add ./my_node

Under the hood, PeppyOS runs apptainer build --fakeroot to produce a .sif (Singularity Image Format) file. This replaces the add_cmd step used by process nodes — the entire build happens inside the container according to the %post section of the definition file.

The resulting .sif file is stored in PeppyOS’s internal storage and is ready to be started.

Starting a container node also uses the same command:

Terminal window
peppy node start my_node

PeppyOS runs the .sif image with apptainer run. Environment variables such as PEPPY_RUNTIME_CONFIG are passed into the container automatically — you don’t need to configure anything beyond what a regular node requires.

The container executes the %runscript section, which runs the compiled binary (Rust) or the Python module entry point.

On macOS, Apptainer is not natively available. PeppyOS bundles a Lima virtual machine that runs Apptainer inside a lightweight Linux guest. This is handled transparently — all peppy node commands work identically on macOS and Linux. No additional installation or configuration is required.

The generated apptainer.def is a starting point. You can modify it freely to fit your needs. Common customizations include:

Add packages to the %post section:

%post
apt-get update
apt-get install -y --no-install-recommends \
ca-certificates curl build-essential pkg-config \
libopencv-dev libudev-dev
rm -rf /var/lib/apt/lists/*

Swap the From line to use a different base:

Bootstrap: docker
From: nvidia/cuda:12.4.0-devel-ubuntu24.04

Add variables to the %environment section so they are available at runtime:

%environment
export PATH="/root/.cargo/bin:$PATH"
export RUST_LOG=info