#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
Preflight — credential gate, container runtime availability, discovery of this project's running containers. Nothing has changed yet, and an abort here costs nothing.
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.Foreground flow continues: the stop/wait/cancel prompt, waiting for working agents to block, the builder pre-stop warning, stopping containers.
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.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.
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.
#Related
lazy upgrade --imagesis the separate non-disruptive path: it rebuilds only the image, in the foreground, and stops nothing and restarts nothing. Only newly-created containers pick the new image up (lazy upgrade --images --dry-runprints the exact boundary).- Image tags are versioned, never bare
:latest— see the rationale insrc/capture/image-tag.tsanddocs/agent-container.md. - An upgrade is not the only thing that refreshes the image — see "When else the image gets rebuilt" below.
#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.