Skip to content

fix(dockerutil): drop AutoRemove on one-off helper containers - #2

Merged
whalelephant merged 1 commit into
dchainfrom
fix/volumeowner-autoremove-race
Jul 31, 2026
Merged

fix(dockerutil): drop AutoRemove on one-off helper containers#2
whalelephant merged 1 commit into
dchainfrom
fix/volumeowner-autoremove-race

Conversation

@whalelephant

@whalelephant whalelephant commented Jul 31, 2026

Copy link
Copy Markdown

Problem

Three short-lived busybox helper containers in dockerutil run with HostConfig.AutoRemove: true:

  • volumeowner.go — chowns a chain's data volume
  • filewriter.go — writes files (config/genesis) into the volume
  • fileretriever.go — reads files back out

Two of them (volumeowner, filewriter) start the container and then call ContainerWait(..., WaitConditionNotRunning). Their command (a chown) exits in microseconds. With AutoRemove on, the daemon removes the container the moment its process exits — and on some daemons that removal wins the race against the wait being registered, so ContainerWait returns No such container. That fails SetVolumeOwner / the file write, and with it any chain boot.

Concretely this reproduces on Docker Desktop with Engine 28.3.0 (macOS) — boot panics first at set volume owner, then (after fixing that helper) at overwriting config/config.toml — while the same code passes on the older Engine used in CI. It's a timing difference, not a code difference.

Root cause

This is a wait-ordering race, not a Docker version regression. The moby Go client documents that ContainerWait returns its channels only after the wait request has been acknowledged by the daemon, specifically so callers can establish the wait before starting the container:

ContainerWait blocks until the request has been acknowledged by the server (with a response header), then returns two channels on which the caller can wait for the exit status of the container or an error… This allows the caller to synchronize ContainerWait with other calls, such as specifying a "next-exit" condition before issuing a ContainerStart request.
moby client/container_wait.go

These helpers do the opposite (start, then wait), so for a near-instant command the exit + AutoRemove can complete before the wait attaches. AutoRemove (daemon-side removal on process exit) has existed unchanged since API v1.25 / Docker 1.13, so this is a latent bug that newer daemons' removal timing simply exposes — same race, provider-independent (cf. podman #25479).

Fix

Drop AutoRemove from all three helpers and let the existing deferred ContainerRemove(..., Force: true) clean up the stopped container. This closes the race (the container persists until the wait observes its exit) while still guaranteeing removal. fileretriever never starts its container (it only CopyFromContainers the created filesystem), so it was already race-free — changed only for uniformity.

Verification

TestDchainBoots (dchain interchaintest) boots the 3-validator network to height 5 and PASSes on Docker Engine 28.3.0 / Docker Desktop (macOS), where it previously panicked at set volume owner.

References

The short-lived busybox helpers that chown a chain's data volume
(volumeowner), write files into it (filewriter), and read files back out
(fileretriever) each ran with HostConfig.AutoRemove=true. For the two that
start the container and then ContainerWait on it, the near-instant command
exits before the wait attaches on newer Docker Engines (25+), so the daemon
reaps the container first and the wait returns "No such container" — failing
SetVolumeOwner / file writes and any chain boot on such hosts (e.g. Docker
Desktop with Engine 28.x on macOS).

Remove AutoRemove from all three and rely on the existing deferred
ContainerRemove(Force) to clean up the stopped container instead, closing the
race while still removing it. (fileretriever never starts its container, so it
was race-free; changed for uniformity.)

Verified: dchain interchaintest TestDchainBoots boots to height 5 and PASSes
on Docker Engine 28.3.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a Docker wait-ordering race in dockerutil one-off helper containers by removing daemon-side AutoRemove so containers persist long enough for ContainerWait(..., NotRunning) to reliably observe exit, and then relying on explicit ContainerRemove(..., Force: true) cleanup.

Changes:

  • Removed HostConfig.AutoRemove: true from the volume owner helper container to avoid ContainerWait racing container removal.
  • Removed HostConfig.AutoRemove: true from the file writer helper container for the same ContainerWait/removal race.
  • Removed HostConfig.AutoRemove: true from the file retriever helper container for uniformity (even though it is never started).

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
dockerutil/volumeowner.go Drops AutoRemove to prevent helper container removal racing ContainerWait.
dockerutil/filewriter.go Drops AutoRemove to prevent helper container removal racing ContainerWait.
dockerutil/fileretriever.go Drops AutoRemove for uniformity across helper containers.
Suppressed comments (2)

dockerutil/filewriter.go:82

  • The deferred ContainerRemove uses the same ctx that can be canceled (the select below explicitly returns ctx.Err()). If ctx is canceled, this cleanup call will likely fail with "context canceled" and leave the helper container behind. Consider using a non-cancelable cleanup context (with a short timeout) for the remove step so the container is reliably reaped even on test timeouts/cancellation.
	defer func() {
		if err := w.cli.ContainerRemove(ctx, cc.ID, types.ContainerRemoveOptions{
			Force: true,
		}); err != nil {
			w.log.Warn("Failed to remove file content container", zap.String("container_id", cc.ID), zap.Error(err))
		}

dockerutil/volumeowner.go:81

  • The deferred ContainerRemove uses the same ctx that can be canceled (the select below explicitly returns ctx.Err()). If ctx is canceled, this cleanup call will likely fail with "context canceled" and leave the helper container behind. Consider using a non-cancelable cleanup context (with a short timeout) for the remove step so the container is reliably reaped even on test timeouts/cancellation.
	defer func() {
		if err := opts.Client.ContainerRemove(ctx, cc.ID, types.ContainerRemoveOptions{
			Force: true,
		}); err != nil {
			opts.Log.Warn("Failed to remove volume-owner container", zap.String("container_id", cc.ID), zap.Error(err))
		}

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@whalelephant
whalelephant merged commit 8703e3d into dchain Jul 31, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants