lazy documentationv0.22

How lazy upgrade rebuilds the container image

lazy upgrade used to do its work strictly in order: find running containers, ask the human what to do about the working ones, wait for them to block, stop everything — and only then start a container image rebuild that can take several minutes. None of that waiting is an input to the build: the image is built from the Dockerfile, not from the state of your tasks. So the build now starts first and runs in the background while the rest of the upgrade proceeds.

The sequence

  1. Preflight — credential gate, container runtime availability, discovery of this project's running containers. Nothing has changed yet, and an abort here costs nothing.

  2. Start the rebuild, in the background, to a staging tag. The build writes <repository>:<tag>-upgrade (e.g. lazy-runner:0.21-upgrade), always with --no-cache — the Dockerfile text is unchanged when a new version of the configured agent's CLI ships, so only busting the cache actually re-fetches it.

    This happens unconditionally: no version comparison, no Dockerfile-hash check, no age check. Upgrading lazy rebuilds the image, always. That is what makes the image tag safe to keep coarse (major.minor) — the tag is an identity, and this is the freshness mechanism.

  3. Foreground flow continues: the stop/wait/cancel prompt, waiting for working agents to block, the builder pre-stop warning, stopping containers.

  4. Collect and promote. The upgrade waits for the background build (usually already finished), then points the canonical tags — lazy-runner:<major.minor> and, for the base repository, lazy-runner:latest — at the staged image and drops the staging tag. The agent binary is rebuilt alongside this.

  5. Stop the daemon, then purge any pre-v0.20 MCP configs left in the repo and rotate the shared daemon token they leaked — the one window where no container and no daemon holds it.

  6. Restart the daemon, which reconciles and auto-resumes interrupted tasks.

Why this is safe

Nothing running is disturbed by the build. A container holds its image by ID from the moment it launches, and any container created while the build is in flight resolves the canonical tag — which still points at the current image. The staging tag is invisible to every launch path.

The canonical tag only moves after you commit. Promotion happens in step 4, past the point where you chose to proceed. Cancel the upgrade, ctrl-c out of a prompt, or hit any failure before that, and lazy-runner:<major.minor> still points exactly where it did. The abandoned build's layers stay in the container runtime's build cache, so the next upgrade starts warm instead of from scratch; the staging tag itself is removed.

A failed build fails the upgrade, loudly. There is no fallback to "keep the old image and carry on" — that would produce an upgrade that upgraded nothing. If the build has already failed by the time you answer the prompt, the upgrade aborts before stopping anything, so your builders and agents keep running on the intact image. If it fails while the upgrade is collecting it, the failure is reported with the staging tag that was not promoted.

When else the image gets rebuilt

An upgrade is the main path, but not the only one. Three triggers rebuild a runner image, on three different axes:

Trigger What it catches
lazy upgrade Everything. Unconditional, --no-cache, described above
The image is more than 14 days old Unpinned contents drifting on a host nobody upgrades
The Dockerfile's content hash changed You edited the Dockerfile

lazy system build lazy-runner --no-cache forces one at any time.

The age backstop exists because a Dockerfile's text is not what goes stale. What it installs is unpinned — apt packages, the agent CLI's installer, the base image — so identical text yields a materially different image weeks later. Only wall-clock time sees that, which is why freshness is time-based and the tag (lazy's major.minor) is only an identity.

None of this can leave you running an old lazy. The image contains no lazy code: lazy-agent is bind-mounted into the container at launch, so it is current the moment lazy is.