Pinning DOCKER_API_VERSION versus automatic negotiation for a client image running against variable Docker Engine versions
0 reputation · 20 Apr 2022, 00:45 UTC
0 reputation · 20 Apr 2022, 00:45 UTC
A single immutable client image is deployed against both self-hosted and managed Docker daemons that may expose different API version ranges. The client needs to reliably perform build, push, and container lifecycle operations without modification. Pinning DOCKER_API_VERSION locks the client to a specific API contract, ensuring identical behavior across daemon versions, but risks incompatibility when the daemon's minimum supported API version exceeds the pinned value. Automatic negotiation allows the client to use the highest mutually supported API version, maximizing feature availability on newer daemons, yet introduces non-deterministic code paths that can mask failures until an engine upgrade occurs.
The core constraint is maintaining operational consistency for CI workflows while supporting a range of daemon versions in production and managed CI environments. The decision hinges on whether compatibility guarantees should be enforced in the client image through explicit version pinning, or delegated to runtime negotiation with appropriate feature detection.
Two documented approaches exist: (1) setting DOCKER_API_VERSION explicitly in the client environment to a version known to be supported by all target daemons, or (2) omitting the variable and allowing the Docker client SDK to negotiate the highest compatible API version with each daemon. The trade-off centers on reproducibility versus feature access, with additional uncertainty around when pinned versions may be deprecated by the Docker ecosystem.
Which approach better balances the need for reproducible CI behavior against the requirement to support both legacy and modern Docker daemons without client image changes?
35775 reputation · 20 Apr 2022, 08:36 UTC
Do not bake DOCKER_API_VERSION into the immutable client image. Instead, omit the variable from the image and inject it per-deployment via CI/CD pipeline variables: pin explicitly for legacy self-hosted daemons, leave unset for managed CI environments. This preserves image immutability while matching the compatibility policy to each target daemon.
1.41 for 2020+ engines) guarantees identical request/response schemas, error codes, and feature sets across runs.N also supports all prior versions down to its minimum (typically 1.24 on Engine 20.x+).DOCKER_API_VERSION is unset) sends a versioned ping (/api/vX.Y/_ping) and falls back until success.>= 1.24 (Engine 1.12, 2016) remain supported on current Engine 27.x. The practical risk is feature gaps, not removal.1.24 will fail on modern daemons because the minimum supported version has risen.docker version --format '{{.Server.APIVersion}}' against the oldest self-hosted daemon in your fleet. Choose a version <= that daemon's maximum and >= 1.24. Example: 1.41 covers Engine 20.10+.DOCKER_API_VERSION=1.41 (or your chosen pin) as a pipeline/job variable.docker version --format 'Client: {{.Client.APIVersion}} Server: {{.Server.APIVersion}}' to log the negotiated version.What is the oldest Docker Engine version in your self-hosted fleet? Run docker version --format '{{.Server.Version}}' on that host. If it is older than 20.10 (API 1.41), the pin value must be lowered accordingly, and you should verify that the pinned version is still above that daemon's minimum supported API version.
DOCKER_API_VERSION affects the Docker CLI and Go SDK clients using envconfig; it does not affect containerd/nerdctl or podman-docker compatibility layers.client.ClientVersion() and client.ServerVersion() after connection.Use comments to ask for clarification. Post a solution as an answer.
35,775 reputation · 20 Apr 2022, 01:11 UTC
One clarification: omitting DOCKER_API_VERSION makes the official Docker client negotiate the highest API version supported by both the client binary and the daemon, not simply the daemon's maximum. An older client bundled in the image can therefore cap the negotiated contract even on newer daemons. Pinning below the daemon's minimum or above its maximum causes connection failures, and third-party SDKs may honor the variable differently. Verify with docker version and the daemon /version endpoint on the oldest and newest target hosts before choosing a pin or relying on negotiation.