Reference

Runtime & Environment

This page describes what happens when an Airfield container runs: which container engine is used, how the container is wired to the host, what gets mounted where, and the environment variables that change any of it. The CLI Reference covers commands and file schemas; this page covers behavior.

Container engines

Airfield targets Docker and works with engines that present a Docker-compatible CLI:

If a build fails with invalid mount type "cache", Airfield regenerates the Dockerfile without cache mounts and retries on its own.

Networking and IPC

On Docker, every run, cmd, and shell container starts with --network=host and --ipc=host. This is deliberate: ROS 2 nodes in different containers, and nodes running directly on the host, discover each other over DDS exactly as if they were plain processes on one machine. You do not need to configure discovery servers, port mappings, or a shared Docker network, and tools on the host (ros2 topic echo, rviz2) see topics published from any container.

The trade-off is standard for host networking: containers share the host’s network namespace, so two containers cannot bind the same port.

Who the container runs as

An image holds no account for the person using it. Every command starts, as root, in a small script at /opt/airfield-init.sh, which creates the caller’s account inside the container (same login name, user and group ids, and home path as on the host) and then runs the command as that user. So:

Nothing in the home directory that is mounted from the host is touched.

Images built once, used by many machines

Because an image holds nothing about who built it or where, it can be named after its recipe: airfield-pkg-<name>:<fingerprint>. The fingerprint is taken from everything that decides the image’s contents (the base image’s name, every dependency, the architecture, GPU or CPU where a dependency cares, and Airfield’s own code). Machines of the same kind arrive at the same name for the same package; a machine that differs arrives at another and builds its own.

To let machines share, name a repository in the project’s airfield.yaml:

image_registry: ghcr.io/my-org/my-robot

Each time a package’s image is needed, Airfield then:

  1. uses the image for this recipe if the machine already has it, without running docker build at all;
  2. else pulls it, if another machine has pushed it;
  3. else builds it here.
airfield package build nav_stack --push      # build if needed, then upload
airfield package build nav_stack --rebuild   # build from scratch, whatever exists

What gets mounted

Each container receives:

The working directory defaults to the source mount; default_workdir in airfield.yaml overrides it.

Containers created by one image are independent. Two panes running the same package get two containers from the same image, with the same mounts.

The first-run build

Images built for a ROS package carry a small entry script at /opt/airfield-entry.sh. Before running your command, it checks whether the package has been built into the workspace install/ directory:

Because install/ lives on the host, that check sees work done by earlier containers in the same project: the first pane to need a package compiles it, and every later pane finds it built and goes straight to launching.

A build that failed or was cut short does not count as built, even though colcon leaves the package’s folder behind in install/. The next run tries the build again, so a package that never compiled is never launched as if it had. A container that starts while another is still compiling the same package waits for that build and then goes straight to its command. Packages you built yourself, with colcon in a shell or in an earlier version of Airfield, count as built.

The automatic build uses colcon. A ROS 1 package gets its image and its containers like any other, but its catkin workspace is built by hand, in airfield package shell.

Two protections make this safe on small boards:

Packages without a ros_distro have no entry script; their commands run in a plain login shell.

GPU access

GPU passthrough depends on the platform:

Dependency manifests can declare host_dependencies (an NVIDIA driver with a minimum version, for example). Airfield evaluates them before the image build: interactive sessions are prompted when a required host dependency is missing, and non-interactive runs (CI) fall back to CPU installs rather than failing.

Teardown

Airfield names each container it starts (airfield-run-<pid>-<id>) and runs it with --rm. When the airfield process receives Ctrl-C, SIGTERM, or SIGHUP (a closed terminal, or tmux kill-session), it stops its own container explicitly instead of trusting the in-container process tree to forward signals. This is why airfield project down leaves no orphaned containers: killing the tmux session signals each pane, and each pane stops its container on the way out.

Processes started under nohup keep their SIGHUP immunity; Airfield does not override it.

After a hard crash (SIGKILL, power loss) where no handler could run, sweep the leftovers:

airfield project down --prune

Environment variables

VariableEffect
AIRFIELD_NO_PULLSkip docker build --pull, so a package can build FROM a locally built base image that exists in no registry (a custom L4T base, for example). Often exported in a plan’s pre_window or a wrapper script.
AIRFIELD_IMAGE_REGISTRYRepository that images are shared through, overriding the project’s image_registry on this machine. none switches sharing off.
AIRFIELD_ROSDISTRO_URLWhere rosdep’s lookup table is fetched from. Point it at a mirror or a local copy (file:///...) with the same layout as github.com/ros/rosdistro.
AIRFIELD_ROSDEP_TABLEoff translates names by the naming convention alone, without rosdep’s table.
AIRFIELD_PACKAGES_REPOURL of the shared packages repository. Lets forks, mirrors, and air-gapped sites point at their own manifests. Default: https://github.com/airfield/packages.git.
AIRFIELD_REPOGitHub owner/name slug used by system update and the update check. Set it when running a fork.
AIRFIELD_WORKSPACEHost location of the shared colcon workspace, overriding the default <project>/.airfield/workspace. Point several projects at one path to share a build tree; set it to none to opt out entirely, giving every container a throwaway workspace and removing the lock that serializes concurrent builds.
AIRFIELD_TORCH_INSTALL_TARGETgpu or cpu. Selects GPU passthrough at run time and the wheel flavor for manifests that install PyTorch. TORCH_INSTALL_TARGET is accepted as a fallback name.
AIRFIELD_TORCH_VERSION, AIRFIELD_TORCH_GPU_WHL_TAGVersion and CUDA wheel tag passed to the image build for PyTorch manifests. Airfield sets them itself from a torch==<version> pin and the detected driver; set them manually to override.
AIRFIELD_DEP_<NAME>_VERSIONSet automatically for every exact pin (name==1.2.3) in airfield.yaml, for manifests that read a version at install time.
AIRFIELD_FORCE_DOCKER_CACHE_MOUNTS, AIRFIELD_DISABLE_DOCKER_CACHE_MOUNTSForce BuildKit cache mounts on or off, overriding engine detection. Useful in CI and when troubleshooting builds.
MAKEFLAGS, CMAKE_BUILD_PARALLEL_LEVELOverride the first-run build’s computed compile parallelism.
IN_AIRFIELD_CONTAINERSet to 1 inside every Airfield image. The CLI checks it to refuse container-in-container operations.

Airfield’s own cache and config live under the XDG base directories (~/.cache/airfield, ~/.config/airfield by default) and honor XDG_CACHE_HOME and XDG_CONFIG_HOME.