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.
From the image
Section titled “From the image”Nothing to install on the host but Docker itself: the image carries the web UI, the Docker CLI with its Compose plugin, and Skopeo.
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:latestThat 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:
-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.
-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.
Kaio, Prometheus and Grafana in one file
Section titled “Kaio, Prometheus and Grafana in one file”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: noneGrafana 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
Section titled “Every other variable”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.
From the binary, under systemd
Section titled “From the binary, under systemd”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-serverconnects to the daemon at startup and exits when it cannot, which systemd turns into a restart loop. Install it first, withcurl -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, adddocker-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:
docker version && docker compose version && skopeo --versionOne command
Section titled “One command”curl -fsSL https://kaio-labs.gaidot.net/install.sh | KAIO_ADDR=10.0.0.2:8080 shsystemctl enable --now kaioKAIO_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 upThe steps below are that same script, by hand, for a host that will not pipe a script into a shell.
The release tarball
Section titled “The release tarball”Every tag publishes one archive per architecture, built by CI, holding the two binaries, the web UI, and the systemd files:
VERSION=0.3.7ARCH=amd64 # or arm64curl -fsSLO https://github.com/rgaidot/kaio/releases/download/v${VERSION}/kaio-${VERSION}-linux-${ARCH}.tar.gzcurl -fsSLO https://github.com/rgaidot/kaio/releases/download/v${VERSION}/kaio-${VERSION}-linux-${ARCH}.tar.gz.sha256sha256sum -c kaio-${VERSION}-linux-${ARCH}.tar.gz.sha256tar -xzf kaio-${VERSION}-linux-${ARCH}.tar.gzcd kaio-${VERSION}-linux-${ARCH}sudo ./install.shsudo systemctl enable --now kaiokaio-<version>-linux-<arch>/├── kaio-server├── kaio-cli├── public/ the built web UI├── kaio.service the unit below├── kaio.env.example the configuration below└── install.shBuilding 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.
What it installs, and where
Section titled “What it installs, and where”| 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.
The unit
Section titled “The unit”[Unit]Description=KaioAfter=docker.service network-online.targetWants=network-online.target
[Service]User=kaioGroup=kaioSupplementaryGroups=dockerWorkingDirectory=/var/lib/kaioEnvironmentFile=/etc/kaio.envExecStart=/usr/local/bin/kaio-serverRestart=alwaysRestartSec=5NoNewPrivileges=yesPrivateTmp=yesProtectHome=yesProtectSystem=fullReadWritePaths=/var/lib/kaio
[Install]WantedBy=multi-user.targetKAIO_ADDR=127.0.0.1:8080KAIO_DB=sqlite:/var/lib/kaio/kaio.dbKAIO_STACKS_DIR=/var/lib/kaio/stacksKAIO_STATIC_DIR=/usr/share/kaio/publicKAIO_EVENTS_RETENTION_DAYS=30KAIO_LOGS_TAIL=100RUST_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:latestOutside 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.
Run it
Section titled “Run it”systemctl status kaiojournalctl -u kaio -fThe CLI reads the same KAIO_ADDR, so it finds the service with nothing else
set:
export KAIO_ADDR=127.0.0.1:8080kaio-cli statuskaio-cli tuiRootless, as a user service
Section titled “Rootless, as a user service”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.
- Your first stack: Deploy your first stack
- A persistent setup, behind a reverse proxy: Deploy with Compose
- Moving to a newer version later: Upgrading
- Working on the code instead: Development setup
Kaio, built by Régis Gaidot