Skip to content

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/bin on PATH)
  • Locale set to en_US.UTF-8
  • claude user created at container start by the entrypoint, using the UID/GID the agent passes via CLAUDE_UID / CLAUDE_GID env vars (so file ownership matches the host's claude user)

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:

# happens inside `convocate install`:
docker build -t convocate:<version> .

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:

  1. Source check. Verify convocate:<version> exists locally (docker images convocate) — refuse to push otherwise.
  2. Save + compress. docker save convocate:<version> | gzip into a temp file. SHA-256 the resulting tarball.
  3. Transfer. scp (or, more accurately, the SSH file copy used by hostinstall.SSHRunner) the tarball to a temp path on the target — /tmp/convocate-image-<hex>.tar.gz.
  4. Verify on the receiving end. SHA-256 the received file. If it doesn't match the source hash, abort.
  5. Load. gunzip -c | docker load on the agent.
  6. Stamp the active version. Write /etc/convocate-agent/current-image with the version tag. The agent reads this file at session-start time to know which image tag to docker run.
  7. 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.0 keeps running on v3.0.0 even if the operator pushes v3.1.0 to 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:

  1. Lists every convocate:* tag.
  2. Lists every running container's image.
  3. 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:

  1. 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.
  2. Air-gap-friendly. docker save | ssh | docker load works on isolated networks, behind paranoid firewalls, in environments where pulling from Docker Hub is impossible.
  3. 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.