Skip to content

Image schematics and optional extensions

A Janus image is the base system - kernel, init, janusd, HAProxy - plus the optional extensions you choose for it. The choice is written down as an image schematic, identified by an ID, the way Talos Image Factory does it: the same choices always give the same ID, and a node built from a schematic keeps it through its updates.

NameArchitecturesWhat it adds
prometheus-node-exporteramd64, arm64Prometheus node_exporter: CPU, memory, disk, filesystem and network metrics on :9100/metrics
qemu-guest-agentamd64The QEMU guest agent, for Proxmox and other KVM hypervisors: the node’s addresses and OS in the hypervisor’s UI, filesystem freeze for consistent backups, clean shutdown from the hypervisor
nftablesamd64, arm64A firewall: the node’s nftables ruleset managed through the API and the Controller, applied on trial with an automatic revert, named sets editable live - see Firewall (nftables)
keepalivedamd64, arm64VRRP: virtual IPs shared by several nodes, moved when one fails or its HAProxy stops answering - see VRRP (keepalived)
birdamd64, arm64BGP, OSPF, BFD with BIRD 2: announce the node’s addresses - an anycast address withdrawn while HAProxy doesn’t answer - see BGP (BIRD)
letsencryptamd64, arm64Let’s Encrypt (or any ACME CA): the node obtains and renews its HAProxy certificates itself - HTTP-01 answered by HAProxy, DNS-01 through a DNS provider’s API for wildcards - swapped in without a reload - see Let's Encrypt
consulamd64, arm64The Consul agent with your configuration: HAProxy’s servers from Consul’s catalog, the node’s services in it - see Consul. Business Source License 1.1

prometheus-node-exporter was called node-exporter up to v2026.10.01-2. A node built with the old name isn’t stuck on it: the image factory offers it the newest release built with the new name - the same extensions, a new schematic. The Controller shows that update as a rename, to install with the schematic change accepted; with janusctl, pass -allow-schematic-change.

An extension is built into the read-only rootfs: there is no installing or removing one on a running node. janusd runs its services - restarting them if they exit - and shows them next to janusd and haproxy in janusctl system services, the Controller’s System › Services page and the logs. Each runs in its own SELinux domain.

qemu-guest-agent only enables the agent commands a hypervisor needs to observe the node and shut it down: ping and info, OS, host name, time, network interfaces, filesystems and CPUs, filesystem freeze and thaw, shutdown. The commands that would run programs or read and write files on the node (guest-exec, guest-file-*, guest-set-user-password, guest-ssh-*) are disabled - they would be a shell by another name. In Proxmox, enable the agent in the VM’s options (QEMU Guest Agent); until the VM has the agent’s channel, the service shows as waiting rather than failing.

prometheus-node-exporter listens on every address, port 9100, without authentication, like a stock node_exporter: restrict who can reach it. Its address, port and collectors are settings - see Metrics.

{"customization": {"extensions": ["prometheus-node-exporter", "qemu-guest-agent"]}}

The same, as shown in YAML:

customization:
extensions:
- prometheus-node-exporter
- qemu-guest-agent
  • Only the extensions a release lists (its schematic-catalog.json) can be named; order and duplicates don’t matter.
  • The ID is the sha256 of the schematic’s canonical JSON form. The default schematic - no extension, what the official releases are built from - is a055fbb697e2d0abb0c5911e7702b07040f49eb71befeaf9b90495a905327f47.
  • The version, platform, architecture and image format are chosen separately, when downloading: one schematic serves every release and every platform.

The ID is part of the kernel command line of every image built from the schematic (janus.schematic=<id>), which the release key signs. A node therefore knows its schematic (janusctl version shows it, with its extensions), and LifecycleService.Upgrade checks the update’s command line: an update built from another schematic is refused, so a node built with an extension can’t lose it to an update built without it. To change a node’s extensions on purpose, pass -allow-schematic-change (janusctl lifecycle upgrade) - or reinstall.

From the Controller, a node’s System › Update page does it in one place: Change extensions… lists what the image factory offers with the newest release (those the node’s architecture can’t run greyed out), and Prepare the update asks the factory for that release built with the chosen extensions - starting the build if nobody asked for it yet, and following it until it’s ready. The installation form below is then filled in (bundle URL, sha256, schematic change accepted), and the confirmation says which extensions the node gains and loses. Installing is the usual update: A/B, automatic revert if HAProxy isn’t healthy, and Rollback back to the previous image and its extensions. Unchecking every extension goes back to the default image, from the GitHub Release.

The companion site’s Image builder (https://janus.sw-servers.net/builder) walks through the choices - platform, release, extensions - and gives the schematic’s ID, its YAML and the download links:

  • the default schematic downloads straight from the GitHub Release;
  • any other is built on demand, the first time someone asks for it, from the build inputs that release published (below). It takes a few minutes; the page follows the build and offers the files when it’s done. Built images stay available, for the latest releases.

The same service answers nodes’ update questions, per schematic:

Terminal window
curl https://janus.sw-servers.net/api/v1/updates/<schematic-id>?arch=amd64

gives the newest release built for that schematic - the base URL of its update bundle (what janusctl lifecycle upgrade and the Controller’s Update page take) and its sha256 - or starts building it. The Controller’s Update page asks it for each node with extensions (dashboardd -image-factory, this site by default), and offers the update once it’s built. When a new release comes out, the site builds the update bundle of every schematic it has already served, so a node with extensions finds its update ready like one without.

Every release publishes, for each architecture, what a schematic’s image is made of - nothing is compiled again for a schematic, and every node runs the release’s own binaries:

AssetContent
kernel-<arch>the release’s kernel
rootfs-base-<arch>.tarthe base system tree: init, janusd, HAProxy, the SELinux policy, the CA bundle, … with the SELinux types of its executables
extension-<name>-<arch>.tareach extension’s files, manifest and SELinux types
schematic-catalog.jsonthe extensions the release offers, and for which architectures

The site starts .github/workflows/schematic-build.yml on the project’s runner, which checks the schematic against that catalog, layers the extensions onto the base tree (rootfs/assemble-from-base.sh, refusing any extension that would replace a file), writes the squashfs and its dm-verity tree, signs the update bundle’s UKIs with the release key - the key never leaves the runner - builds the disk, ISO and SD card images (image/schematic/build.sh), boots the amd64 disk under UEFI with SELinux enforcing (hack/qemu-schematic-smoke-test.sh), and uploads everything to the site with a manifest listing each file’s sha256.

image/schematic/build.sh works locally too, from a release’s assets or from make schematic-inputs (the same files, built from the tree):

Terminal window
make schematic-inputs # build/inputs/
image/schematic/build.sh build/inputs amd64 my-schematic.json out/

Building an image with extensions from source

Section titled “Building an image with extensions from source”
Terminal window
make extensions-amd64 # build the extensions (Docker)
echo '{"customization":{"extensions":["prometheus-node-exporter"]}}' > my-schematic.json
make disk-image SCHEMATIC=my-schematic.json # or proxmox-image, iso-image, ...
go run ./hack/extpack id -schematic my-schematic.json # its ID

hack/qemu-extensions-test.sh (make qemu-extensions-test) proves the whole chain on a real UEFI boot under SELinux enforcing.