Skip to content

Build and release

The repository produces two Docker images, and make build builds both. They share the same tags: the short git SHA of the working tree, and latest.

Target Image Built from
make build both the two Dockerfiles below
make app-build kaio Dockerfile at the repository root
make docs-build kaio-docs docs/Dockerfile
Terminal window
make app-build
make run

The multi-stage Dockerfile compiles the Rust binaries, builds the frontend assets with Vite, and copies both into a minimal Alpine image that also carries docker-cli-compose and Skopeo. make run starts that local image with the Docker socket mounted read-only and ./data bound, on http://localhost:8080.

Terminal window
make docs-build
make docs-run # http://localhost:8081
make docs-stop

This site is built by Astro into static files and served by nginx. The image carries nothing else: no Docker socket, no database, no volume. make docs-build builds it for https://kaio-labs.gaidot.net, which is what the canonical URLs and the sitemap end up pointing at; pass DOCS_SITE= and DOCS_BASE= for another origin or a sub-path. make docs-run builds and starts it through docs/compose.yml for http://localhost instead, and the README describes both arguments in full.

make docs-check is the faster loop while writing: it runs docs/scripts/check-docs.py against the Rust sources, then builds the site without building an image.

Every screenshot on this site is generated, never taken by hand:

Terminal window
make docs-shots # web UI stills + the dashboard recording
make docs-shots-tui # the TUI still

docs/scripts/shots/capture.mjs serves the built frontend on a local port, answers every /api call from docs/scripts/shots/fixtures/demo.mjs, freezes time so timestamps never drift between runs, and drives Chrome through each scene before cropping it. The recording is captured frame by frame and encoded by ffmpeg into a .webm for the site and an animated .webp for the README. The TUI capture runs the real kaio-cli against the same fixtures served over HTTP, and records it with vhs.

Requirements: Chrome (CHROME_PATH overrides the lookup), ffmpeg, and for the TUI vhs plus a debug build of the CLI. make docs-check fails if a page references a screenshot that does not exist, or if a screenshot is not shown on any page.

Terminal window
make push # both images
make app-push # application only
make docs-push # documentation only

Each target pushes the git SHA tag and latest. The target registry is set by REGISTRY at the top of the Makefile.

CI publishes the application image to GHCR on every push to main and on tags. The documentation image is not published by CI, only by make docs-push.

ghcr.io/rgaidot/kaio:latest
ghcr.io/rgaidot/kaio:<short-sha>
ghcr.io/rgaidot/kaio:<version>

make dist builds the archive that a systemd install consumes:

Terminal window
make dist # → dist/kaio-<version>-linux-<arch>.tar.gz(+ .sha256)

It builds the frontend with VITE_GIT_SHA set, compiles kaio-server and kaio-cli in release mode, then hands both to scripts/package.sh, which stages the binaries, public/ (the built frontend), packaging/kaio.service, packaging/kaio.env.example and packaging/install.sh, and writes the tarball plus its checksum.

Called with no argument, it takes the version from version in backend/Cargo.toml, the same value the binary reports on /api/health, since that is CARGO_PKG_VERSION. Every argument is an override, so CI can name the archive and point at cross-compiled output:

Terminal window
scripts/package.sh 0.2.0 arm64 \
backend/target/aarch64-unknown-linux-musl/release \
frontend/dist

One script builds both the local and the published archive, so they cannot drift. make dist needs no Nix shell: SQLite is compiled into the binary and TLS goes through rustls, so a plain cargo and a plain npm are enough.

https://kaio-labs.gaidot.net/install.sh fetches kaio-linux-<arch>.tar.gz from https://kaio-labs.gaidot.net/releases, a name without a version in it, while every archive CI builds carries one. One command bridges the two:

Terminal window
make docs-releases # the version in backend/Cargo.toml
make docs-releases VERSION=0.2.3

make docs-build stages them on its own when they are missing, or older than backend/Cargo.toml, so a version bump restages before the image is built and an image never ships without /releases/. make docs-releases forces the download whatever their state, and make docs-build DOCS_RELEASES= drops the prerequisite when you are rebuilding the site for a version CI has not released yet.

scripts/publish.sh downloads kaio-<version>-linux-<arch>.tar.gz from the GitHub release with gh, checks it against the checksum CI published beside it, copies it to docs/public/releases/kaio-linux-<arch>.tar.gz and writes a fresh .sha256 under that name, which is what sha256sum -c needs the installer to find. Skip the download and publish what dist/ already holds with KAIO_PUBLISH_SOURCE=dist.

It publishes the archive CI built, not the output of a plain make dist. CI targets x86_64-unknown-linux-musl, which yields a static binary that runs anywhere; a local cargo build --release links against the host’s libc, and on a distro whose libraries live outside /usr/lib, NixOS being the obvious case, the result starts on that machine and nowhere else. The script refuses a dynamically linked kaio-server for that reason, and KAIO_ALLOW_DYNAMIC=1 overrides the refusal when the host you install on is the host you built on.

The name inside the archive keeps its version, so the installer works whatever the file is called. docs/public/releases/ is git-ignored: the archives ride in the documentation image, which make docs-build then make docs-push publish, and the site serves them at /releases/. Rebuild that image after every make docs-releases, or the site keeps serving the previous version.

.github/workflows/release.yml runs on v*.*.* tags, in three stages:

Job Does
version Reads version from backend/Cargo.toml and fails the release when the tag disagrees with it, so v0.3.0 cannot ship a binary that answers 0.2.0 on /api/health. Bump the workspace version, commit, then tag
web npm ci && npm run build once, with VITE_GIT_SHA set to the tag, and uploads frontend/dist as an artifact
bundle per architecture: builds the binaries statically against musl (x86_64-unknown-linux-musl natively, aarch64-unknown-linux-musl through cross), downloads that same artifact and calls scripts/package.sh
release collects the archives, builds the changelog and creates the GitHub release with the tarballs and their .sha256 attached

The web UI is built once and reused by every architecture: the assets do not depend on the target, and building them once is what guarantees every archive of a release ships byte-identical frontend files.

A workflow_dispatch run names the archives <version>-<short sha>, builds and uploads them as workflow artifacts without creating a release, which is the way to test a packaging change before tagging. Tagged image builds stay in ci.yml.

Kaio, built by Régis Gaidot