Installation¶
This page covers what you need to run Otherix, how to obtain the three artifacts, and how to configure and start the control plane. To go from a fresh host to a running VM in one sitting, follow the Quickstart instead - it walks the same steps in order with copy-paste commands.
Requirements¶
Otherix has two cluster components plus an operator CLI:
| Artifact | Role | Platform |
|---|---|---|
otherix-api |
Control plane. Embeds an etcd member, the in-process scheduler, and the worker dispatcher. | Plain Go - runs on any Linux host (and natively on macOS for development). |
otherix-agent |
Node agent. Talks to QEMU directly and reports state over mTLS. | Linux only. Needs KVM + qemu-system-<arch>. |
otherix |
Operator CLI. | Any platform. |
Architectures: amd64 and arm64.
The agent needs a Linux host with hardware virtualization:
/dev/kvmpresent and accessible (verify withls -l /dev/kvm).qemu-system-x86_64orqemu-system-aarch64installed.- For
arm64guests, a UEFI firmware blob (Debian/Ubuntu:qemu-efi-aarch64, default path/usr/share/AAVMF/AAVMF_CODE.fd).
The control plane has no external dependencies. There is no Postgres, Redis, or message queue to operate - the api-server embeds its own etcd member as the only datastore.
Developing on macOS
The agent is Linux only. On a Mac, run the control plane natively and the agent inside a Lima VM. See macOS development.
Getting the artifacts¶
Install a released build¶
The fastest path on a Linux host is the install script. It downloads the
requested artifact from the latest GitHub release, verifies it against the
published SHA256SUMS (fail-closed), and installs it.
# Operator CLI (installs to /usr/local/bin/otherix)
curl -fsSL get.otherix.dev | OTHERIX_COMPONENT=cli sh
# Control plane (.deb; run as root)
curl -fsSL get.otherix.dev | OTHERIX_COMPONENT=api sudo -E sh
# Node agent (.deb; run as root on a KVM host)
curl -fsSL get.otherix.dev | OTHERIX_COMPONENT=agent sudo -E sh
The endpoint serves the script as plain text - read it before piping to a shell. It honours three environment variables:
| Variable | Default | Meaning |
|---|---|---|
OTHERIX_COMPONENT |
api |
api, agent, or cli. |
OTHERIX_VERSION |
latest |
A release tag such as v1.2.3. |
OTHERIX_REPO |
otherix/otherix |
Source owner/repo for releases. |
On macOS the script installs the CLI via Homebrew
(brew install otherix/tap/otherix); the daemons are Linux only.
Build from source¶
make build # otherix-api, otherix-agent, otherix into ./bin/
make build-api # control plane only
make build-cli # CLI only
Cross-compile the daemons for a Linux node from another host:
make build-linux-amd64 # -> bin/linux-amd64/otherix-{api,agent}
make build-linux-arm64 # -> bin/linux-arm64/otherix-{api,agent}
Container images¶
Control-plane and agent images are published to GitHub Container
Registry (ghcr.io). The control-plane image is distroless; the agent
image is Alpine-based and intended for development and CI only (a
production agent runs as a host binary alongside qemu-system-*, not in
a container - it needs /dev/kvm and host networking).
Filesystem layout¶
Otherix follows a fixed convention:
| Path | Contents |
|---|---|
/etc/otherix/ |
Operator-provided config (api.yaml, agent.yaml). |
/var/lib/otherix/ |
Runtime state: etcd data dir, cluster CA, generated certs, pools, VMs. |
The defaults below assume this layout. Override paths in config if your deployment differs.
Configuring the control plane¶
otherix-api reads a single YAML file (--config, default
/etc/otherix/api.yaml). Config also binds environment variables with
the OTHERIX_ prefix and __ as the nesting separator (for example
OTHERIX_SERVER__LISTEN). A full annotated reference ships at
deploy/config/api.example.yaml.
For a working single-node install you care about a handful of blocks:
server.listen- the user-facing HTTP API address. Default0.0.0.0:8080.etcd.data_dir- where the embedded etcd member persists its data. Default/var/lib/otherix/etcd. This is your cluster state; back it up.auth.jwt_secret- HS256 signing key for access tokens. At least 32 bytes. Generate withopenssl rand -hex 32and replace the example value before any non-dev deploy.cluster_ca- on-disk location of the cluster CA (cert + key). The api-server generates it on first boot and reuses it on restart. It signs the per-node agent certs and the per-replica CP server cert.cp_cert- per-replica CP server cert lifecycle. The default (Mode C) auto-generates a fresh cert signed by the cluster CA on every boot. Add hostnames the agent will dial viacp_cert.additional_sans.agent_server- the second HTTPS listener dedicated to mTLS agent traffic (heartbeat, node join, console bridging). Enable it for a working cluster. Default listen0.0.0.0:8443.agent_client- the outbound CP-to-agent client that drives async work (VM create/delete, pool scans). Enable it for a working cluster.workers.enabled- the in-process job dispatcher and periodic scheduler. Defaulttrue. Whentrue,agent_client.enabledMUST also betrueor the api-server refuses to start (it would otherwise wedge every async task inpending).
workers and the agent client are coupled
workers.enabled: true requires agent_client.enabled: true. To
run the api-server as an HTTP-only target with no provisioned mTLS
(smoke testing the contract), set workers.enabled: false instead.
Bootstrap admin¶
The first admin user is seeded from environment variables read on first boot, before the HTTP server starts:
export OTHERIX_BOOTSTRAP_ADMIN_EMAIL=admin@otherix.local
export OTHERIX_BOOTSTRAP_ADMIN_PASSWORD='correct-horse-battery-staple'
With zero existing admin rows and both vars set, the api-server creates the admin. Both set with an admin already present is a no-op. Setting only one is fatal. If you leave both unset, generate a password hash and seed the row yourself:
Minimal api.yaml¶
A single-node config that boots a working cluster (user API + agent listener + workers):
server:
listen: "0.0.0.0:8080"
auth:
# Replace before any non-dev deploy. At least 32 bytes.
jwt_secret: "REPLACE-ME-with-openssl-rand-hex-32-output"
jwt_access_ttl: 15m
jwt_refresh_ttl: 720h
# The second HTTPS listener for mTLS agent traffic (heartbeat, join,
# console). Required for a working cluster.
agent_server:
enabled: true
listen: "0.0.0.0:8443"
# Outbound CP -> agent client. Required when workers.enabled is true.
# mTLS material is sourced automatically from the cluster CA at boot.
agent_client:
enabled: true
# In-process job dispatcher + periodic scheduler (default on).
workers:
enabled: true
cluster_ca:
cert_file: "/var/lib/otherix/ca/cluster-ca.crt"
key_file: "/var/lib/otherix/ca/cluster-ca.key"
# Per-replica CP server cert. Auto-generated from the cluster CA by
# default; list every hostname/IP the agent will dial.
cp_cert:
additional_sans:
# - "cp.example.com"
# - "10.0.0.100"
etcd:
mode: "single"
name: "otherix-0"
data_dir: "/var/lib/otherix/etcd"
storage_pools:
# The CP auto-provisions a "default" pool on every node as it becomes
# ready, so `vm create` works without --pool. Set to "" to opt out and
# manage pools manually.
default_pool_name: "default"
Everything omitted falls back to documented defaults (see
deploy/config/api.example.yaml for the full set, including the
placement scheduler and node-pressure knobs).
etcd peer URL is always HTTPS
Peer (Raft) mTLS is always on, even single-node, so a later grow to
HA needs no transport switch. The api-server auto-generates the peer
cert from the cluster CA on each boot. The default peer_url: auto
resolves to this host's routable IPv4 at boot (falling back to
loopback only when no route is found), so a later grow to HA needs no
change here.
Running the control plane¶
The process embeds etcd, runs the bootstrap hooks (admin, cluster CA,
default-pool seed), generates its server cert, and serves the API. It
blocks until it receives SIGINT/SIGTERM, then shuts down gracefully
within server.shutdown_grace.
Other invocations:
otherix-api --version # print version and exit
otherix-api --hash-password 'plain' # print an argon2id hash and exit
Health probes live outside /v1/ for Kubernetes:
GET /healthz- liveness (process up, no dependency calls).GET /readyz- readiness (checks the store; 503 when not ready).
Single node vs. HA¶
The config above runs a single self-clustering api-server
(etcd.mode: single). For a multi-replica control plane that forms one
etcd cluster over peer mTLS, see
High availability.
Next steps¶
- Quickstart - boot a VM end to end.
- Join a node - enrol an agent with the join-token bootstrap protocol.
- Architecture - how the pieces fit together.