Issue #34 reports container Recreate / native auto-update (#28, #10) failing on modern
Docker Engines via the agent with "starting container with non-empty request body ...
removed in v1.24". The proposed fix: make API-version resolution robust in all three
docker client constructors.
- api/docker/client/client.go: add a shared negotiateWithFloor() used by createLocalClient,
createTCPClient, createAgentClient. It eagerly NegotiateAPIVersion (10s bound) instead of
relying on lazy first-call negotiation, and enforces a modern floor: if the negotiated
version is empty or < 1.24 (versions.LessThan), rebuild pinned at 1.44 (WithVersion, which
the SDK requires INSTEAD of negotiation — WithVersion sets manualOverride, disabling
NegotiateAPIVersion). Negotiation stays primary; the floor is only a safety net. Effective
API version is now guaranteed >= 1.24. Custom transport / TLS / agent headers untouched.
- Tests (api/docker/client/client_test.go): modern daemon -> empty start body; below-floor
daemon -> pinned to floor; unreachable daemon -> stays >= 1.24.
IMPORTANT FINDING (needs maintainer confirmation before this is called a complete fix for
#34): on the pinned SDK (github.com/docker/docker v28.5.2), ContainerStart posts a nil body
UNCONDITIONALLY (client/container_start.go: cli.post(ctx, ".../start", query, nil, nil) — no
version branch), and NewClientWithOpts defaults to version 1.51 (negotiation only downgrades).
So on develop, the server-side path the issue names (autoupdate.go -> Recreate ->
cli.ContainerStart) cannot emit a request body regardless of API version — this change hardens
the client path but is unlikely to be the actual source of the reported symptom. The
"non-empty body" more likely originates from a DIFFERENT layer (an OLDER docker SDK in the
failing deployment, where ContainerStart did attach HostConfig at API < 1.24; or the docker
PROXY forwarding a raw browser request). Recommend confirming the failing environment's build
before merging as the #34 fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- F1: test that clicking the badge/UpdateNowButton actually dispatches the update
(confirm->mutate) for standalone and stack, and not on dismiss.
- F2: Go test that a successful forced re-check repopulates the caches (a later
non-force read hits cache, no second registry HEAD).
- F3: throttle forced image-status re-checks against registry amplification —
coalesce concurrent forced re-checks of the same image via singleflight, plus a
5s per-image min-interval (== remoteDigestCache TTL) caching only successes. The
non-force path (daemon + background badges) is unchanged.
- F4: notifications are now per-container. Stack-member containers each emit their
own EventUpdated (not one aggregate stack event), Event carries the stack name
(from the com.docker.compose.project label), and the new image digest is fetched
best-effort by re-inspecting the container after the redeploy. Message:
'Environment | .. / Stack [<name>] / Update [<container>]: <old> -> <new>'.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make the container image-status badge actionable, matching native Portainer:
- Clicking "Update available" opens the update confirm dialog and runs the
existing update flow (standalone recreate-with-pull / stack redeploy), gated
and disabled while in flight to avoid a double submit. The confirm+apply logic
is extracted from UpdateNowButton into a shared useApplyContainerImageUpdate
hook so the details button and the list badge share one implementation.
- Clicking "Up to date" re-queries the registry. Because the server caches image
status (statusCache 5m + remoteDigestCache 5s), a plain refetch was a no-op, so
the endpoint gains an optional ?force=true that bypasses BOTH caches for a
manual re-check while still repopulating them; the default (auto badges + the
auto-update daemon) keeps using the caches unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
F1: cap the image-status cache TTL at 5m (was 24h) — the cache is keyed by the
LOCAL imageID, which doesn't change when upstream pushes a new image under the
same tag, so the 24h TTL hid new images from both the badge and the auto-update
daemon; a short TTL re-resolves the remote digest within the poll window.
F2: document that the update->rollback guard map is in-memory (restart implication).
F3: skip auto-update for an unnamed container when rollback is on (the endpoint+name
keyed guard can't record it, so it would loop) — pure skipUnnamedForRollback + test.
F4: wrap the pre-update ContainerInspect in context.WithTimeout(endpointTimeout).
F5: document Reload() does not interrupt an in-flight tick.
F6: floor auto-heal CheckInterval at 1s (mirrors auto-update) + test.
F7: wontfix — migration is currently correct; namespace rework is out of scope.
F8: correct the misleading SSRF/AllowList comment (no filter is applied).
F9: front auto-heal interval floor + test; dedup STALE_TIME; fix invalidation comment.
Also refresh three stale '24h/long-lived cache' comments to match the 5m TTL.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
F1: ContainerImageStatus now reads the 24h statusCache (keyed by imageID)
before the remote registry digest lookup, so the cache is effective on the
input side for all callers instead of being write-only. This avoids the
rate-limited registry HEAD on repeat loads.
F2: add nodeName to the imageStatus query key so cached results cannot be
reused across nodes.
F3: correct the swagger annotations to reflect that engine-level issues
degrade to a 200 skipped/error status rather than 400/404.
F4: return a generic error message to the client instead of the raw
registry/engine error; the raw error is still logged server-side.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>