Skip to content

HTTP API

The server binds KAIO_ADDR (default 127.0.0.1:8080) and serves the frontend alongside these endpoints. Kaio has no authentication: whatever reaches its port controls the Docker daemon, which is root-equivalent on that host. Put your own layer in front before this port is reachable beyond localhost: see Security.

Endpoint Method Description
/api/health GET Health check. Returns status, version, sha, the git commit the binary was built from, and server_id, this server’s identity, which is how a probe tells it apart from another machine that took its address
/api/dashboard GET Current system summary and full container list
/api/events GET SSE stream for real-time Docker events
/api/events/history GET Historical events (limited to 100)
Endpoint Method Description
/mcp POST Model Context Protocol over streamable HTTP, for AI agents. Not routed unless KAIO_MCP names a mode, and guarded by its own Host and Origin allowlists rather than the API’s CORS policy. See MCP server
Endpoint Method Description
/metrics GET Prometheus exposition of the state Kaio knows: stacks, containers, pending updates, CVE counts, cluster health and Kaio’s own loops. Not routed unless KAIO_METRICS=on, and requires the bearer token KAIO_METRICS_TOKEN names when one is set. See Prometheus and Grafana
/metrics/targets GET The nodes of this server’s registry in Prometheus’ HTTP service discovery format, so one Kaio hands Prometheus the rest of the cluster. Each group carries a node label. Same flag and same token as /metrics

How a server joins another one’s registry. See Multiple servers.

Endpoint Method Description
/api/cluster GET Whether this server is a control plane, a member, or neither
/api/cluster/token GET The join token. 404 when the server has none: reading never creates a credential
/api/cluster/token POST Mint one, retiring any previous. This is the only route that creates it
/api/cluster/register POST Called by a joining node: {token, server_id, name?, advertise?}. On a valid token the node is added to the registry under its own server_id, probed, and its settled {name, url, status, error} returned. A server that is the control plane itself is refused
/api/cluster/join POST Asks this server to join the cluster in {control_plane_url, token, name?, advertise?}
/api/cluster/membership DELETE Leave: asks the control plane to drop this server’s registry entry, then forgets it locally. Returns {left, removed, error}, and still forgets when the control plane cannot be reached

register reads the connection’s peer address when advertise is missing, so a node behind a wildcard bind is enrolled without naming its own address.

The registry of Kaio instances this server pilots. See Multiple servers.

Endpoint Method Description
/api/nodes GET List the nodes that have joined, ordered by name
/api/nodes/{name} GET One node
/api/nodes/{name} DELETE Drop the node from the registry. The node itself is untouched; it rejoins by running kaio-cli join again
/api/nodes/{name}/check POST Probe one node and return it with the result
/api/nodes/{name}/proxy/{*path} any Forward the request to that node’s own API, at {node url}/{path}. A node that is not in the registry answers 404: the registry is what makes a node reachable this way
/api/nodes/check POST Probe every node and return them all

/api/nodes/{name}/proxy/{*path} makes every route on this page reachable on a node: GET /api/nodes/prod-01/proxy/api/dashboard answers with prod-01’s dashboard. The query string is carried over and all three transports survive the hop: JSON with any method and body, SSE streamed rather than buffered, and a WebSocket upgrade bridged socket to socket so the container terminal works.

Request bodies are streamed too, so an upload crosses the gateway at constant memory whatever its size. A Content-Length is replayed unchanged to the node; a chunked upload stays chunked. Authorization is not forwarded.

Reaching a node this way requires it to be in the registry. Removing it removes the route, which is the only thing the gateway checks: the node’s own address stays as open as it was.

Nodes only enter the registry by joining (see the cluster routes above), and nothing here edits one. status is the result of the last probe: online, offline, unauthorized or unknown.

Endpoint Method Description
/api/containers/{id} DELETE Delete a container
/api/containers/{id}/action POST start, stop, restart, pause, unpause
/api/containers/{id}/logs GET SSE stream of logs. ?follow=false for a one-shot snapshot of the last lines; ?tail= overrides KAIO_LOGS_TAIL
/api/containers/{id}/shell WS WebSocket for interactive sh access
Endpoint Method Description
/api/stacks POST Deploy a stack (name, content, optional env array replacing the stack environment)
/api/stacks/{name} DELETE Remove a stack, its files and its containers
/api/stacks/{name}/update POST Pull and update (pull + up -d)
/api/stacks/{name}/restart POST Restart
/api/stacks/{name}/start POST Start every stopped container of the stack
/api/stacks/{name}/pause POST Pause every running container of the stack
/api/stacks/{name}/unpause POST Resume every paused container of the stack
/api/stacks/{name}/scan POST Start a Trivy scan of all the stack’s images
/api/stacks/{name}/check-update POST Trigger a remote image update check
/api/stacks/{name}/logs GET SSE stream merging the logs of every container of the stack, each line prefixed by its service. ?tail= overrides KAIO_LOGS_TAIL, per container
/api/stacks/{name}/compose GET Retrieve the docker-compose.yml
Endpoint Method Description
/api/stacks/{name}/env GET List variables: [{key, value, secret}], secret values returned as null
/api/stacks/{name}/env PUT Replace the environment without deploying (a null value keeps the stored one). Returns env_pending
/api/stacks/{name}/env/export GET Same list with secret values in the clear, for a server moving a stack to another server. Requires the cluster token as Authorization: Bearer <token> and answers 401 without it
/api/stacks/{name}/apply POST Redeploy with the stored environment, create a version and clear env_pending
Endpoint Method Description
/api/stacks/{name}/copy POST Copy a stack to another node, or onto this one under a new name. Answers the plan it followed

This is the control plane’s own route, not a node’s: it orchestrates the pieces below. The source is never written to, never stopped and never deleted, so a failed copy costs time rather than data.

{
"from": "prod-01",
"to": "prod-02",
"as": "web-staging",
"include_volumes": true,
"start": false,
"allow_binds": false,
"allow_anonymous_volumes": false,
"dry_run": false
}

Every field is optional. from and to name nodes in the registry; leaving one out means this server. as defaults to the source name, and must differ from it when source and destination are the same server. start defaults to false: a copy that came up on its own would leave two live stacks contending for the same DNS, webhooks, queues and mail.

dry_run returns the plan and changes nothing, blockers included, so a caller can show them before asking for confirmation. A real copy refuses on those same blockers.

The plan carries volumes (each with the name it will take on the destination), binds, images, warnings and blockers. A copy stops before touching the destination when:

  • the destination already runs a stack by that name;
  • a port the stack publishes is already published there, whether or not the copy was asked to start, because the result could never come up. allow_port_clash accepts writing it all the same;
  • a volume keeps its own name on the destination (an explicit name:, or an external one) and both ends are the same server, because the copy would then mount the volume the source is running on;
  • the stack mounts host paths, unless allow_binds says to accept that they stay behind;
  • a volume is anonymous and include_volumes was asked for, unless allow_anonymous_volumes says to accept losing it. Declaring the volume under volumes: in the compose file and redeploying is the better answer: an anonymous volume holding real data would otherwise produce a copy that looks healthy and is empty.

Named volumes are renamed to follow the destination project: copying web as web-staging moves web_data into web-staging_data. A volume given an explicit name: in the compose file keeps it, and an external one is left alone; on another server that is what you want, and on the same server it is refused rather than quietly shared.

A volume declared under volumes: that Docker has never created is left out of the plan rather than failing the copy, and a name: written with ${VAR} is interpolated with the stack’s environment before it is looked up.

If anything fails after the destination has been written to, the copy undoes itself there: the volumes it created are removed, the stack it imported is removed, and a volume that already existed is never touched. When even that fails, an error event names what to remove by hand.

These routes carry a stack, its secrets and its data off the machine, so each one asks for the cluster token in Authorization: Bearer <token>, the way /api/stacks/{name}/env/export does above, and answers 401 without it. They are the pieces a copy or a migration is built from: none of them moves anything by itself.

Endpoint Method Description
/api/stacks/{name}/export GET The whole stack as JSON: compose file, environment with its secrets in the clear, every version with its own environment, the images it references, and an inventory of what it mounts. ?sizes=true also measures each volume, which costs one throwaway container
/api/stacks/import POST Write a stack here from that JSON, under name, without starting it. Refuses a name already taken, files already present, or a current_version missing from versions
/api/volumes/{name}/export GET Stream the volume as an uncompressed tar (application/x-tar), read from a throwaway container
/api/volumes/{name}/import POST Unpack such a tar into the volume, creating it when absent. Refuses a volume that already holds data unless ?force=true
/api/stacks/{name}/up POST Bring an imported stack up without adding a version to its history

The mount inventory separates what can be moved from what cannot:

  • volumes are the named volumes, each with the Compose key it comes from (web_data from data under the project web), where each service mounts it, whether it is external, and whether it is anonymous. An anonymous volume is one Docker named itself: its data is real but its name is not, so it cannot be recreated by name on another host.
  • binds are the host paths the stack mounts. They are never transferred. A path that exists on one server has no reason to exist on the next, and copying it blindly would land the wrong ownership on the wrong files, so the inventory reports them and stops there.

An external: true volume is reported but belongs to something else, so a copy leaves it alone.

The throwaway container runs KAIO_TRANSFER_IMAGE (default alpine:3), pulled on first use, with no network and nothing mounted but the volume.

Endpoint Method Description
/api/stacks/{name}/versions GET List all historical versions
/api/stacks/{name}/rollback/{version} POST Roll back to a specific version
/api/stacks/{name}/versions/{version} DELETE Delete a specific version
Endpoint Method Description
/api/images GET List local images, enriched with scan summaries
/api/images/scan POST Start a Trivy scan for every tagged image on the host
/api/images/{id} DELETE Forcefully delete an image
/api/images/{id}/scan POST Start a scan for one image (asynchronous)
/api/images/{id}/scan GET Latest scan result, with the full CVE list
Endpoint Method Description
/api/volumes GET List volumes
/api/volumes/{name} DELETE Delete a volume
/api/networks GET List networks
/api/networks/{id} DELETE Delete a network
Endpoint Method Description
/api/prune/images POST Prune dangling and unused images
/api/prune/containers POST Prune stopped containers
/api/prune/volumes POST Prune unused local volumes
/api/prune/networks POST Prune unused networks
  • SSE (/api/events, log endpoints): one JSON object per line. Log lines carry service, container, stream, ts and line.
  • WebSocket (/api/containers/{id}/shell): raw PTY bytes in both directions.

Kaio, built by Régis Gaidot