Skip to content

Installation

Two ways in, and they give the same product: the image, which carries everything, or the release tarball under systemd, which keeps the manager out of Docker. Start with the first unless you have a reason not to.

Nothing to install on the host but Docker itself: the image carries the web UI, the Docker CLI with its Compose plugin, and Skopeo.

Terminal window
docker run -d --name kaio \
-p 8080:8080 \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
-v ./data:/app/data \
-e KAIO_ADDR=0.0.0.0:8080 \
-e KAIO_DB=sqlite:/app/data/kaio.db \
ghcr.io/rgaidot/kaio:latest

That is enough to try it. For a setup that survives a reboot, with the same options in a file you keep, use Deploy with Compose.

Nothing here turns on the MCP endpoint, because it stays off until you ask for it. An agent reaching a container needs two more variables, one to pick the rung and one to name the Host it answers:

Terminal window
-e KAIO_MCP=read \
-e KAIO_MCP_ALLOWED_HOSTS=kaio.example.net \

Prometheus metrics work the same way: off until you ask, then two variables, one to serve /metrics and one to put a token in front of an endpoint that names every stack and image you run.

Terminal window
-e KAIO_METRICS=on \
-e KAIO_METRICS_TOKEN=a-long-random-string \

Neither is needed to run Kaio, and nothing watches you from outside until you turn one on.

To stand up the whole thing rather than try it, this compose.yml is complete: Kaio with its metrics on, a Prometheus scraping it, an Alertmanager and a Grafana. Save it, docker compose up -d, and Grafana answers on :3000 with admin / admin.

services:
kaio:
image: ghcr.io/rgaidot/kaio:latest
container_name: kaio
restart: unless-stopped
ports:
- "8080:8080"
environment:
- KAIO_ADDR=0.0.0.0:8080
- KAIO_DB=sqlite:/app/data/kaio.db
- KAIO_METRICS=on
# Prometheus carries this same value below. Change one, change the other.
- KAIO_METRICS_TOKEN=kaio-scrape-token
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- kaio:/app/data
prometheus:
image: docker.io/prom/prometheus:v3.15.0
container_name: kaio-prometheus
restart: unless-stopped
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus:/prometheus
command:
- --config.file=/etc/prometheus/prometheus.yml
- --storage.tsdb.retention.time=90d
depends_on:
- kaio
alertmanager:
image: docker.io/prom/alertmanager:v0.34.1
container_name: kaio-alertmanager
restart: unless-stopped
ports:
- "9093:9093"
volumes:
- ./alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
grafana:
image: docker.io/grafana/grafana:13.2.3
container_name: kaio-grafana
restart: unless-stopped
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
volumes:
- grafana:/var/lib/grafana
depends_on:
- prometheus
volumes:
kaio:
prometheus:
grafana:

It needs two small files beside it. prometheus.yml:

global:
scrape_interval: 15s
alerting:
alertmanagers:
- static_configs:
- targets: [alertmanager:9093]
scrape_configs:
- job_name: kaio
metrics_path: /metrics
authorization:
credentials: kaio-scrape-token
static_configs:
- targets: [kaio:8080]

And alertmanager.yml, which groups alerts and sends them nowhere until you give it a receiver:

route:
receiver: none
receivers:
- name: none

Grafana starts empty here: add Prometheus at http://prometheus:9090 as a data source, then import the dashboard from packaging/grafana/dashboard.json. The repository carries the same stack with the dashboard, the data source and nine alert rules already provisioned, which is one command instead of this file: see Prometheus and Grafana.

Every other variable, and the default it falls back to, is in Configuration. The systemd install below writes them all into /etc/kaio.env, commented out, so the file doubles as the list.

No container: kaio-server is a single binary configured entirely through the environment, so systemd can supervise it directly. Prefer this when you would rather not run the manager itself in Docker, or when the host already has a service layout you want it to follow. The host then has to provide what the image otherwise carries:

  • Docker itself, and this one is not optional: kaio-server connects to the daemon at startup and exits when it cannot, which systemd turns into a restart loop. Install it first, with curl -fsSL https://get.docker.com | sh
  • the Docker CLI with the Compose plugin, since every stack is deployed by shelling out to docker compose. Without it the server runs and the dashboard fills, then each deploy, update, rollback and restart fails. The script above installs it; on a host whose Docker came from elsewhere, add docker-compose-plugin, or the distribution’s equivalent
  • Skopeo, used by the update checker to inspect remote image digests

Check all three before installing Kaio, since the installer only warns about what is missing:

Terminal window
docker version && docker compose version && skopeo --version
Terminal window
curl -fsSL https://kaio-labs.gaidot.net/install.sh | KAIO_ADDR=10.0.0.2:8080 sh
systemctl enable --now kaio

KAIO_ADDR is optional: give it and the address lands in /etc/kaio.env straight away, leave it out and the server answers on 127.0.0.1:8080, which is the default this page cautions about below. An existing /etc/kaio.env is never overwritten either way, so reinstalling keeps your configuration.

It works out the architecture, fetches the matching tarball, verifies its checksum when one is published next to it, then runs the install.sh the archive carries. Everything happens in a temporary directory that is removed on the way out. Run it as root, or have sudo on the host: it elevates only for the install step, and prints what it is doing.

Run the same command again to upgrade: /etc/kaio.env is kept, the binaries and the web UI are replaced, and the service restarts only if it was already running. KAIO_BASE_URL points it at another directory, KAIO_TARBALL at another file name.

/etc/kaio.env carries every optional variable commented out, the MCP endpoint and Prometheus metrics included, so turning one on is uncommenting two lines and systemctl restart kaio. A binary install is scraped exactly like a container one: Prometheus reaches that host’s port and neither knows nor cares how Kaio got there.

What it prints at the end is a numbered list of what is left to do, and it adapts: a host without Docker is told so first, with the consequence spelled out, and the step about KAIO_ADDR disappears once the address is not the loopback.

installed; the server will listen on 10.0.0.2:8080
1. systemctl enable --now kaio
2. kaio-cli status, in a new shell so it picks that address up

The steps below are that same script, by hand, for a host that will not pipe a script into a shell.

Every tag publishes one archive per architecture, built by CI, holding the two binaries, the web UI, and the systemd files:

Terminal window
VERSION=0.3.7
ARCH=amd64 # or arm64
curl -fsSLO https://github.com/rgaidot/kaio/releases/download/v${VERSION}/kaio-${VERSION}-linux-${ARCH}.tar.gz
curl -fsSLO https://github.com/rgaidot/kaio/releases/download/v${VERSION}/kaio-${VERSION}-linux-${ARCH}.tar.gz.sha256
sha256sum -c kaio-${VERSION}-linux-${ARCH}.tar.gz.sha256
tar -xzf kaio-${VERSION}-linux-${ARCH}.tar.gz
cd kaio-${VERSION}-linux-${ARCH}
sudo ./install.sh
sudo systemctl enable --now kaio
kaio-<version>-linux-<arch>/
├── kaio-server
├── kaio-cli
├── public/ the built web UI
├── kaio.service the unit below
├── kaio.env.example the configuration below
└── install.sh

Building that same archive from a checkout is a make dist away, described in Build and release.

install.sh warns about a missing docker, docker compose or skopeo instead of failing: the binaries install fine, the features that shell out to them do not work until the tools are there. It creates the docker group when the host has none, because the unit joins it and systemd refuses to start a service whose supplementary group does not exist, with a 216/GROUP that names nothing; the Docker packages reuse that group when they are installed later. Run it again to upgrade: it keeps /etc/kaio.env, replaces the binaries and the assets, and restarts the service only if it was already running. Back up before you do, for the reason given in Upgrading.

Path Holds
/usr/local/bin/kaio-server, /usr/local/bin/kaio-cli the binaries
/usr/share/kaio/public the web UI, replaced wholesale on upgrade
/var/lib/kaio the SQLite database and stacks/<name>/compose.yml
/etc/kaio.env the configuration, never overwritten
/etc/systemd/system/kaio.service the unit
/usr/bin/kaio-server, /usr/bin/kaio-cli symlinks, and only when /usr/local/bin is absent from PATH
/etc/profile.d/kaio.sh exports KAIO_ADDR for login shells, reading it from /etc/kaio.env

The profile snippet is what spares you an Error: server unreachable from the CLI: bind the server to 10.0.0.2:8080 and 127.0.0.1 stops answering, while kaio-cli still defaults to it. It reads the file at each login rather than baking a value in, so editing KAIO_ADDR is enough, and a wildcard bind becomes 127.0.0.1 since that is what the CLI can reach. It applies to login shells, so ssh host kaio-cli status still needs the variable spelled out.

/var/lib/kaio is the one directory to back up, and it is owned by the kaio system user the script creates. Database migrations are compiled into the binary and run at startup.

/etc/systemd/system/kaio.service
[Unit]
Description=Kaio
After=docker.service network-online.target
Wants=network-online.target
[Service]
User=kaio
Group=kaio
SupplementaryGroups=docker
WorkingDirectory=/var/lib/kaio
EnvironmentFile=/etc/kaio.env
ExecStart=/usr/local/bin/kaio-server
Restart=always
RestartSec=5
NoNewPrivileges=yes
PrivateTmp=yes
ProtectHome=yes
ProtectSystem=full
ReadWritePaths=/var/lib/kaio
[Install]
WantedBy=multi-user.target
/etc/kaio.env
KAIO_ADDR=127.0.0.1:8080
KAIO_DB=sqlite:/var/lib/kaio/kaio.db
KAIO_STACKS_DIR=/var/lib/kaio/stacks
KAIO_STATIC_DIR=/usr/share/kaio/public
KAIO_EVENTS_RETENTION_DAYS=30
KAIO_LOGS_TAIL=100
RUST_LOG=info
# Everything below is optional, shown with the default it falls back to.
# Uncomment what you need. Full reference:
# https://kaio-labs.gaidot.net/reference/configuration/
# The MCP endpoint for AI agents, off until you set it. Each rung adds tools to
# the one below it: read, write, admin. https://kaio-labs.gaidot.net/reference/mcp/
#KAIO_MCP=off
# Host headers /mcp answers, on top of loopback, which is all it accepts unset.
# A remote agent needs its name here, or * to accept any.
#KAIO_MCP_ALLOWED_HOSTS=
# Browser origins allowed to call /mcp. Unset refuses every request carrying an
# Origin header, which is what an agent or a CLI never sends.
#KAIO_MCP_ALLOWED_ORIGINS=
# Browser origins allowed to call the REST API. Unset is permissive.
#KAIO_ALLOWED_ORIGINS=
# A Docker daemon other than the local socket, unix:// or tcp://
#KAIO_DOCKER_HOST=
# Background loops. 0 turns one off.
#KAIO_UPDATE_CHECK_INTERVAL_HOURS=12
#KAIO_TRIVY_SCAN_INTERVAL_HOURS=24
#KAIO_NODE_HEALTH_INTERVAL_SECONDS=60
# The scanner: how many images at once, and the image it runs.
#KAIO_TRIVY_CONCURRENCY=2
#KAIO_TRIVY_IMAGE=aquasec/trivy:latest

Outside a container, 127.0.0.1:8080 is the right bind: the UI and the API stay on the loopback interface, and a reverse proxy terminates TLS in front of them. KAIO_STATIC_DIR is what frees the server from its working directory. It defaults to ./public, which is how the image is laid out. See Configuration for every variable.

Terminal window
systemctl status kaio
journalctl -u kaio -f

The CLI reads the same KAIO_ADDR, so it finds the service with nothing else set:

Terminal window
export KAIO_ADDR=127.0.0.1:8080
kaio-cli status
kaio-cli tui

Under rootless Podman, drop the system user and run the unit as yourself: put it in ~/.config/systemd/user/kaio.service without the User/Group/ SupplementaryGroups lines, add Environment=KAIO_DOCKER_HOST=unix://%t/podman/podman.sock to [Service] (%t expands to the runtime directory, and specifiers are not expanded inside an environment file), and manage it with systemctl --user. loginctl enable-linger $USER keeps it running without an open session. The caveats of rootless operation are in Podman.

Kaio, built by Régis Gaidot