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 |
The application image
Section titled “The application image”make app-buildmake runThe 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.
The documentation image
Section titled “The documentation image”make docs-buildmake docs-run # http://localhost:8081make docs-stopThis 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.
Screenshots
Section titled “Screenshots”Every screenshot on this site is generated, never taken by hand:
make docs-shots # web UI stills + the dashboard recordingmake docs-shots-tui # the TUI stilldocs/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.
make push # both imagesmake app-push # application onlymake docs-push # documentation onlyEach target pushes the git SHA tag and latest. The target registry is set by
REGISTRY at the top of the Makefile.
Published images
Section titled “Published images”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:latestghcr.io/rgaidot/kaio:<short-sha>ghcr.io/rgaidot/kaio:<version>The release tarball
Section titled “The release tarball”make dist builds the archive that a
systemd install consumes:
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:
scripts/package.sh 0.2.0 arm64 \ backend/target/aarch64-unknown-linux-musl/release \ frontend/distOne 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.
Serving the tarball yourself
Section titled “Serving the tarball yourself”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:
make docs-releases # the version in backend/Cargo.tomlmake docs-releases VERSION=0.2.3make 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.
Tagged releases
Section titled “Tagged releases”.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.
Related
Section titled “Related”Kaio, built by Régis Gaidot