Concepts

Airfield Overview

Airfield runs each part of a robot’s ROS 2 stack in its own Docker container, without changing how the ROS code itself is written or built. It defines a consistent project layout, package metadata schema, and command surface so teams can build and run their code reproducibly on a laptop, in CI, and on the robot.

This page walks from the command surface down to what actually happens at launch time through one example project: my_robot/, with three Airfield packages called base_driver, camera_driver, and nav_stack.

Tip: green dashed terms in the text are clickable. Click one and the part of the diagram it refers to lights up.

Core principles

  1. Encapsulated package execution with isolated dependencies
  2. Convention-over-configuration project layout
  3. Explicit dependency declarations by target architecture (arm64, x86_64)
  4. Deterministic orchestration from package plans
  5. Flexible enough for research iteration, strict enough for production bring-up

The command surface

Airfield command tree: project and package namespaces plus support commands

Every invocation has the same : airfield <namespace> <command> [arguments]. The two namespaces mirror Airfield’s two core nouns:

There are also two additional conveniences. First, a [pkg name] argument can be omitted when your shell is already inside that package’s folder, because Airfield detects the package from the working directory (build is the one exception: pass .). Second, commands resolve by unique prefix, so airfield pa b means airfield package build.

A project on disk

Airfield project layout: airfield.yaml, packages, dependencies, and plans folders

A project is a normal directory with four Airfield-specific pieces at the root:

The central idea in this diagram: an Airfield package wraps one or more ROS packages. bundles three (motor_driver, joystick, gui); bundles two; wraps exactly one. In that last case the package’s own folder is the ROS package (source_path: "."), which is why its package.xml sits at the top level right next to airfield.yaml.

Note: containers isolate dependency environments, not ROS packages. It is preferred to bundle ROS packages whose dependencies agree, and split out a separate Airfield package where dependencies diverge. Bundling costs no runtime isolation since at launch each pane still gets its own container, as the last diagram shows.

The extra base_driver/ and camera_driver/ folders marked (ROS metapackage) are the glue that makes bundling work; they’re explained in the next diagram. The optional file holds machine-local extras (such as additional container mounts) that belong to one machine rather than to the shared package config.

Inside one Airfield package

Anatomy of one Airfield package: airfield.yaml fields mapped onto the source tree

Zooming into packages/base_driver/: the left side is its entire ; the right side is the source tree it points at.

Note: the src/ at the top of the package (Airfield’s source_path) is a workspace-style folder that holds whole ROS packages, while the src/ inside gui/ is that ROS package’s own C++ source folder.

What sits below each inner package is decided by that package’s build type, which is a ROS convention rather than anything Airfield imposes:

Build vs run: the two-phase lifecycle

Airfield's two phases: building the environment image, then running disposable containers

Phase 1: airfield package build produces an environment image, . It resolves each entry in dependencies: against the , then : a base image (package override, else project default, else the ROS-distro default), the apt/pip installs from the manifests, and a small . No ROS source is compiled into the image, which is exactly why it only needs rebuilding when dependencies change.

Phase 2: every starts a fresh, disposable container from that image. Your source is , so edits on the host are instantly visible inside: no rebuild, no copy, and nothing is lost when an image is rebuilt. The entry script then makes : if install/base_driver already exists in the workspace, it skips straight to your command; on first run it compiles with , which is where the metapackage from the previous diagram earns its keep, pulling all three bundled ROS packages into one build. Either way it finishes by sourcing ROS plus the workspace install and your command.

install/ is the directory colcon writes finished build output into, one subfolder per ROS package, so install/base_driver is nothing more than colcon’s own record that this bundle has already been compiled. Because --packages-up-to builds the metapackage last, after the three packages it depends on, that single folder appearing is enough to prove all four succeeded.

The (src, build, install, log) lives on the host and is mounted into every container: the first container to need a package compiles it once, and every later one (including every pane of a plan) just sources the result. Simultaneous first runs can’t collide; the build is serialized with a lock in the same shared folder. None of this needs configuring — Airfield mounts the workspace itself.

~/workspace is the path inside the container, and it is the same everywhere. On the host, build/, install/, and log/ live in the project, at <project>/.airfield/workspace/, so two projects never share one install/. See Shared colcon workspace.

Anatomy of a plan

Anatomy of an Airfield plan: windows and panes with package and cmd keys

A plan is one YAML file in plans/ that describes everything airfield project up should launch. windows split into , and each pane sets :

is exported into every pane, which makes it the place for session-wide environment like MAP=speedway. The same package may drive several panes (base_driver appears in both the and the ), which costs nothing extra: one image, two containers, as the next diagram shows. A pane can also be left null to get a plain shell inside the session, handy for debugging alongside the running nodes.

Launch time: project up

What airfield project up does: renders a tmuxinator config and starts one container per pane

airfield project up navstack renders the into a (.airfield/navstack.tmuxinator.yml) and starts it as a tmux session. Each pane executes airfield package cmd <package> -- "<cmd>". In other words, every pane is exactly one phase-2 run from the previous diagram, reusing the from phase 1.

Follow the fan-out in the middle: three images back four . The single spawns two independent containers ( and ), so bundling several ROS packages into one Airfield package never sacrifices runtime isolation. All four containers mount the same , so the first pane to need a package builds it and the rest just source install/: build once, launch many.

kills the session, and each pane’s Airfield process stops its own container on the way out, leaving no orphaned containers running.

Where to go next

Ready to try it? Quick Start walks the same loop with real commands, the tutorials cover each piece in depth, and Runtime & Environment documents how the containers are wired to the host.