Image distribution¶
Every session container runs the convocate:<semver> image. That
image has to land on every agent host before any session can start.
This page describes how it gets there.
What's in the image¶
- Ubuntu 24.04 base
- Build toolchain:
build-essential,cmake,pkg-config,python3,python3-pip,python3-venv, Node.js, npm,git,curl,wget,jq,ripgrep,tmux, vim/nano,openssh-client,sudo - Go 1.26 (
/usr/local/go/binonPATH) - Locale set to
en_US.UTF-8 claudeuser created at container start by the entrypoint, using the UID/GID the agent passes viaCLAUDE_UID/CLAUDE_GIDenv vars (so file ownership matches the host'sclaudeuser)
The Anthropic Claude CLI itself is not baked in — it's bind-
mounted from the agent host at /usr/local/bin/claude (read-only).
This means upgrading Claude on the agent host immediately picks up
in every session container without rebuilding the image.
Build pipeline¶
The image is built on the shell host during convocate install:
Where <version> is the binary's compile-time Version string
(stamped by the Makefile's -X main.Version=$(VERSION) ldflag).
After the build, the embedded Dockerfile + entrypoint.sh + skel
content (under internal/assets/data/*.gz, regenerated by go
generate) are the source of truth for what the image contains.
Distribution to agents¶
convocate-host init-agent and convocate-host update ship the image
to a target agent. The flow:
- Source check. Verify
convocate:<version>exists locally (docker images convocate) — refuse to push otherwise. - Save + compress.
docker save convocate:<version> | gzipinto a temp file. SHA-256 the resulting tarball. - Transfer.
scp(or, more accurately, the SSH file copy used byhostinstall.SSHRunner) the tarball to a temp path on the target —/tmp/convocate-image-<hex>.tar.gz. - Verify on the receiving end. SHA-256 the received file. If it doesn't match the source hash, abort.
- Load.
gunzip -c | docker loadon the agent. - Stamp the active version. Write
/etc/convocate-agent/current-imagewith the version tag. The agent reads this file at session-start time to know which image tag todocker run. - Clean up the temp file.
If any step fails the temp file stays in place for debugging — the
operator can rerun update with a clean retry.
Version pinning per session¶
Each session, when started or restarted, reads
/etc/convocate-agent/current-image to get the version tag. The tag
is captured in docker run's arguments at start time, so:
- A session running
convocate:v3.0.0keeps running onv3.0.0even if the operator pushesv3.1.0to the agent. - The new tag is picked up the next time that session is
(R)estarted.
This makes cluster-wide rollouts session-by-session: the operator restarts each session when they're ready for the new tag, instead of the whole cluster cutting over at once.
Image pruning¶
Each agent has a daily cron at
/etc/cron.daily/convocate-image-prune that:
- Lists every
convocate:*tag. - Lists every running container's image.
- Removes every tag that isn't referenced by any running container.
This keeps stale tags from accumulating without manual sweep. Tags that are still in use by even one running container are preserved so that container can survive across daemon restarts.
Why not pull from a registry?¶
We could host a private Docker registry and have agents pull. We don't, for three reasons:
- No registry to keep up. The image distribution step already has the SSH connection, so adding a registry-pull step is more moving parts, not fewer.
- Air-gap-friendly.
docker save | ssh | docker loadworks on isolated networks, behind paranoid firewalls, in environments where pulling from Docker Hub is impossible. - SHA-256 integrity is end-to-end. We hash the tarball at source and verify at destination — no trust delegated to a registry's TLS or signing infrastructure.
The trade-off is bandwidth: every agent receives the full ~1GB image at every release. For a small cluster (single-digit agents) the bandwidth cost is negligible compared to the operational simplicity.