Skip to main content
An overview of how the Lager platform components fit together. It runs from the CLI commands on your laptop to the instruments wired to your DUT.

High-Level Overview

All three entry points reach the box through the same Tailscale tunnel. They end at the same drivers. Inside the box they take different routes. A command that drives a net through the box API posts directly to port 9000. lager supply is one. lager python instead uploads a script to the execution service on port 5000. That service runs the script as a subprocess. The subprocess gets full access to the lager.* hardware libraries. A CI runner is a developer machine that is ephemeral. It uses the same path as the command it runs.

Terminology

Lager Box Internals

A single Docker container named lager (started with --restart always) runs every service. The services are peer processes, not a pipeline. A start script launches each one and restarts it if it dies. Only one of them calls another. The box API on port 9000 reaches the hardware service on port 8080 over HTTP. The debug and MCP services are independent. A script running under the execution service drives its drivers itself.

Port Summary

The Exposed column describes a box that publishes its ports, which is the default. A box started with start_box.sh --no-publish (or LAGER_NO_PUBLISH=1) publishes none of them. Every service still listens inside the container. The lagernet Docker network still reaches it, and a reverse proxy owns the host ports. No Yes row answers at <box-ip>:<port>.Port 22 is the exception. SSH is the host’s own daemon rather than a published container port, so --no-publish does not affect it.

Why there are two HTTP ports

A box answers on :9000 and on :5000, and the split is historical rather than functional. :9000 is the box API and the primary one. Net metadata, instrument discovery, box locking, file download and version reporting all go there. :5000 is the older script-upload path. lager python still uses it to send a script to the box and to stop a running one. Nothing new is added to it. Some state answers on both. Lock state is one: the box exposes it on each server, and the CLI reads it from :9000. Open both to your VPN. A box that publishes only :9000 answers lager nets and lager hello but fails lager python.

Optional Control Plane Integration

A Lager Box publishes SSH keys from a key directory, /etc/lager/authorized_keys.d/. An external control plane can therefore provision access with no human typing SSH commands. Put a <name>.pub file there, and the key reaches the box account’s ~/.ssh/authorized_keys in about five seconds. The box bind-mounts /etc/lager into the runtime container, so a control plane can write that file from inside the container. That is how it bootstraps before it has any SSH access to the box. start_box.sh owns only the region of authorized_keys between its # BEGIN LAGER MANAGED KEYS and # END LAGER MANAGED KEYS markers. It rebuilds that region from the key directory on every pass. Two consequences follow:
  • Deleting a .pub revokes the key. Nothing else does; editing authorized_keys by hand inside the marked region is undone on the next pass.
  • Keys installed by other means stay untouched. ssh-copy-id and cloud-init append outside the marked region, and start_box.sh preserves those lines verbatim. Any other system that manages this file must claim its own distinct marker pair. Two managers that share one pair each rebuild the other’s region on every pass.
  • Preserved is not the same as durable. start_box.sh preserves a loose line against its own rebuild. It cannot preserve that line against someone else’s. A second key manager rebuilds authorized_keys from its own source. It keeps only its own marked region, so it drops every loose line. start_box.sh then re-creates its region from the key directory alone. A key that never reached that directory does not come back. For this reason lager ssh-setup, lager update, and lager install do both. They append the public key, and they write it into the key directory as lager-box-<user>-<host>.pub. Any tool that installs a key it expects to survive must do the same.
Lager itself does not require or run a control plane — this is a hook, not a dependency. Leaving the key directory absent or empty simply means no keys are published from it. The Professional Services directory lists the commercial control planes that build on this hook. They add org, RBAC and SSO, audit logging, and scheduling on top of Lager.

Net Abstraction

A Net is the central abstraction that decouples CLI commands from physical hardware details. The record backing psu1 looks like this:
Swapping the physical supply means editing this record. Every command that names psu1 keeps working.

Supported Net Types

Execution Flows

CLI Command Execution

Step-by-step data path for lager supply psu1 voltage 3.3 --yes: The hardware service owns and caches the driver for each physical device. It serializes access under a per-device lock. Concurrent requests to the box API cannot interleave I/O on the same instrument.

Custom Script Execution (lager python)

The lager python command uploads a user-written Python script to the execution service on port 5000. This is a different path from the box API commands above. The service runs the script as its own subprocess. The script constructs and drives its drivers in-process. It does not call the hardware service.
The script runs inside the Docker container with full access to the lager.* hardware libraries. The box streams its output back in real time.

Physical Wiring

How instruments physically connect between the Lager Box and the DUT:

Connection Types

Running from CI

A CI runner drives a Lager Box with the same commands a developer uses. Lager needs no CI-specific infrastructure. There are two arrangements. A runner on a separate host reaches the box across the network, usually over a Tailscale VPN. A runner installed on the box needs no network hop and no secrets. It also serializes the jobs for that bench. Using Lager in CI covers both arrangements and the workflow for each. It also covers bench locking, firmware delivery, and cleanup after a cancelled job.