Wheelhouse docs

Apps are configuration#

Wheelhouse's answer to plugins is a container declared in the router's configuration tree. Installing an app stages set container name <app> … operations into the working set; you review the diff and commit it, exactly as you would a firewall rule. Nothing about an app has a lifecycle of its own: stopping it is a set … disable, starting it is a delete of that node, uninstalling it deletes the subtree, and a rollback takes the apps with it.

Why that is the whole idea#

An install that is configuration inherits every property configuration already has.

PropertyBecause
You see what it will do firstThe install renders as set commands before anything is staged.
It is atomicOne commit, not a package script with steps that can half-finish.
It is reversiblerollback 1 restores the configuration that declared the app.
It survives an image upgrade/config is the router's persistent partition.
It is isolatedEach app is a container with its own namespaces; host access is explicit and labelled in the catalogue entry.

The comparison with a package-manager plugin, drawn honestly in both directions including where the other model is ahead, is in docs/apps.md.

Installing is three steps, and only one is configuration#

The router validates a container at commit and refuses it if either prerequisite is missing, so both happen before the diff exists (docs/apps.md, agent/apps.go):

StepEndpointWhy it is not configuration
1 · Create the volume source directoriesPOST /api/apps/prepareThe router answers Volume "conf" source path "…" does not exist at commit. Making a directory is an action.
2 · Pull the image onto the routerPOST /api/apps/pullThe router answers Image "…" does not exist locally at commit.
3 · Stage the installPOST /api/apps/plan, then stagingThis is the only part that is configuration.

Step 1 only works when the agent runs on the router. Off-router it has no access to the router's filesystem, so it hands back the exact mkdir -p command rather than pretending to have done something — see On-router, off-router, fleet.

Lifecycle is configuration too#

There is no control socket, and the router's API has no restart operation for a container. So:

ActionWhat it actually is
StopStage set container name <app> disable
StartStage a delete of that disable node
RestartA two-commit bounce: commit the disable, then commit its deletion. Both commits are audited. A stopped app refuses with 409, and if the second commit fails the error says the app is down, not "restart failed".
UninstallDelete the whole container name <app> subtree
UpdatePOST /api/apps/update re-pulls the image and compares the local image ID before and after

Volumes under /config are deliberately not removed on uninstall, so a reinstall picks up where it left off.

The update answer is careful about what it knows: already current, or a newer image is on the router but the running container still holds the old one — restart to pick it up. A pull is not presented as an upgrade.

What makes it a plugin rather than a container#

Each catalogue entry declares what the UI should do once the app is running (agent/catalog.go):

  • nav_label / nav_icon — the app gets its own left-nav entry under Installed, with a dot when it is declared but not running.
  • web_ui — an Open link built from the container's real address.
  • health — a TCP probe of the port, plus an HTTP request when a path is given. Deliberately not an app-specific API call: the product does not claim knowledge of an app's internals it does not have.
  • hints — an offer surfaced on another Wheelhouse page, with the app's real address substituted in and the exact operations attached. GET /api/apps/hints/{page} is what a page asks for.

A hint's operations may carry four placeholders, filled from the router the hint is offered on: {{address}} (the container's address), {{dhcp.network}} and {{dhcp.subnet}} (the router's first DHCP scope), and {{dns.domain}} (system domain-name, else its first authoritative zone). A router that cannot fill one keeps the advice and loses the commands, and the card says what is missing. The catalogue itself never names a network or a domain, and a test refuses one.

Hints stage; they never apply. The Commit Bar is still the only thing that commits.

Feature modules: a catalogue entry with no container#

Two of the 38 catalogue entries are kind: "builtin" — WireGuard and Suricata IDS. They are native router features whose Wheelhouse page is gated on being installed, which is the plugin model applied to something the kernel already has.

  • Installing one is instant and stages nothing. There is no image, no volume, no commit. The flag lives in the agent's own state under /config/wheelhouse, because it is a surface preference rather than router behaviour (agent/apps.go, handleAppFeature; agent/store.go, the Features field).
  • Configuration outranks the flag. If the feature is configured on the router — by the CLI, by another operator, by anything — the page shows regardless. Wheelhouse never hides configuration that exists. Uninstalling with live configuration present hands the delete operations back for review and the page stays until they land.

What to know before you rely on it#

UPnP is deliberately absent. Registry checks found no credible published miniupnpd container — every candidate was an unaudited personal build, which is not acceptable provenance for a privileged host-network daemon on a firewall. The gap stays open and the Companions page says so.

Per-app dashboard widgets are not built, deliberately: the health probe is TCP plus an optional HTTP path and never an app-specific API.

See also#

Checked against#

docs/apps.md · agent/apps.go · agent/catalog.go · agent/catalog.json · agent/store.go · agent/main.go · README.md

Updated 2026-09-02 concepts apps containers