GoogleContainerTools/skaffold · warning

could not remove image %q after %d retries

Error message

could not remove image %q after %d retries

What it means

ImageRemove retries removing an image up to `retries` times, sleeping between attempts, but only while the daemon returns a conflict error (errdefs.IsConflict — the image is in use). If every retry hits the conflict, it gives up with "could not remove image %q after %d retries". Any non-conflict error is returned immediately and does NOT produce this message.

Source

Thrown at pkg/skaffold/docker/image.go:640

	res, err := l.apiClient.ImageInspect(ctx, img, client.ImageInspectWithRawResponse(&raw))
	if err != nil {
		return image.InspectResponse{}, nil, err
	}
	return res.InspectResponse, raw.Bytes(), nil
}

func (l *localDaemon) ImageRemove(ctx context.Context, image string, opts client.ImageRemoveOptions) ([]image.DeleteResponse, error) {
	for i := 0; i < retries; i++ {
		res, err := l.apiClient.ImageRemove(ctx, image, opts)
		if err == nil {
			return res.Items, nil
		}
		if !errdefs.IsConflict(err) {
			return nil, err
		}
		time.Sleep(sleepTime)
	}
	return nil, fmt.Errorf("could not remove image %q after %d retries", image, retries)
}

func (l *localDaemon) ImageList(ctx context.Context, ref string) ([]image.Summary, error) {
	res, err := l.apiClient.ImageList(ctx, client.ImageListOptions{
		Filters: client.Filters{}.Add("reference", ref),
	})
	if err != nil {
		return nil, err
	}
	return res.Items, nil
}

func (l *localDaemon) DiskUsage(ctx context.Context) (uint64, error) {
	usage, err := l.apiClient.DiskUsage(ctx, client.DiskUsageOptions{})
	if err != nil {
		return 0, err
	}
	return uint64(usage.Images.TotalSize), nil

View on GitHub (pinned to a1189de023)

Solutions

  1. Remove containers using the image first: `docker ps -a --filter ancestor=<image>` then `docker rm <container>`.
  2. Force removal with the force option (client.ImageRemoveOptions{Force: true}) if the caller's opts allow it.
  3. Remove dependent child images before the parent (`docker rmi <child>` first).
  4. Retry later if another process holds the image; check for concurrent skaffold/docker activity.

Example fix

// before
client.ImageRemoveOptions{}
// after
client.ImageRemoveOptions{Force: true, PruneChildren: true}
Defensive patterns

Strategy: fallback

Validate before calling

// find blockers before removing
cs, _ := client.ContainerList(ctx, client.ContainerListOptions{All: true})
for _, c := range cs { if c.ImageID == imageID { return fmt.Errorf("container %s uses image", c.ID) } }

Try / catch

if _, err := daemon.ImageRemove(ctx, img, opts); err != nil && strings.Contains(err.Error(), "retries") {
    // conflict: image in use — remove dependent containers/children first, then retry
    removeContainersUsingImage(ctx, img)
    _, err = daemon.ImageRemove(ctx, img, client.ImageRemoveOptions{Force: true, PruneChildren: true})
}

Prevention

When it happens

Trigger: Repeated ImageRemove(ctx, image, opts) calls where the Docker daemon keeps returning HTTP 409 Conflict for the full retry window — typically a container still exists that is based on the image, or a child image (dependent layer) is referenced.

Common situations: Trying to prune an image whose container (even stopped) still exists, image untagging blocked by dependent images, concurrent skaffold processes holding the image, k8s/kind cluster node still using the image.

Related errors


AI-assisted analysis of GoogleContainerTools/skaffold@a1189de023 (2026-09-05). Data as JSON: /api/errors/eb374600e3e1de09. Report an issue: GitHub.