How does a Docker Engine API client negotiate which API version it uses?
answer
- The version travels with every request
- One cheap call before the real work
- A response header names the daemon's maximum
- Compatibility flows only one direction
- GET /_ping returns Api-Version
basics
~20 sDocker Engine API paths carry the version, as in /v1.43/containers/json. A client calls GET /_ping first; the daemon answers with an Api-Version response header, and the client downgrades to that version when its own is newer. Negotiation never upgrades.
solid answer
~50 sEvery Docker Engine API endpoint is reachable under a version prefix — `/v1.43/containers/json` — and the daemon keeps serving older versions back to a documented minimum, shaping each response to the version that was asked for so old clients keep working after an engine upgrade. A client discovers what the daemon supports with `GET /_ping`, a cheap call whose response headers include `Api-Version`, `Builder-Version` and `Ostype`. The official SDKs do this for you: the Go client with `client.WithAPIVersionNegotiation()` pings on its first call and pins the lower of the two versions for the rest of the session. Ask for a version above the daemon's maximum and the request is refused with a message of the form `client version 1.44 is too new. Maximum supported API version is 1.43`. Negotiation only ever downgrades — an old daemon cannot be talked into new behaviour.
code
bash · 6 lines# Api-Version, Builder-Version and Ostype come back as response headers
curl -s -I --unix-socket /var/run/docker.sock http://localhost/_ping
# Ask an explicit version rather than letting the daemon pick its newest
curl -s --unix-socket /var/run/docker.sock \
http://localhost/v1.43/versiongo deeper
Know that Docker Engine API URLs contain a version, like /v1.43/containers/json, and that the daemon publishes the newest version it supports. You are not expected to have written a client yet.
Be ready to explain the mechanics: /_ping and its Api-Version response header, the daemon serving a range of versions, and the SDK option that pins the lower of client and daemon on the first call.
Show judgement about pinning: explicit version prefixes in tooling so an engine upgrade cannot reshape a response, negotiation enabled on clients that run on hosts you do not control, and reading the too-new error message correctly.
Own the compatibility policy across a fleet: which API version your platform's tooling targets, how long you keep supporting old clients, and how engine upgrades are rolled out without a coordinated upgrade of everything that drives them.
### The version is in the URL The Docker Engine API is versioned in the path. Every endpoint exists under a prefix — `/v1.43/containers/json`, `/v1.41/images/json` — and the version you put there is a request: *serve me the behaviour of that API version*. The daemon accepts a range, from a documented minimum up to the maximum its release implements, and shapes the response accordingly. That is why an old client library keeps working against a much newer engine: it asks for its own old version, and the daemon answers in that dialect, omitting fields introduced later and keeping field names and defaults the way that version defined them. Omitting the prefix entirely also works — `/containers/json` is treated as the daemon's newest supported version. That is convenient at a shell prompt and a trap in tooling, because the shape of the response then changes the day the host's engine is upgraded. Anything long-lived should name a version. ### /_ping is the negotiation call `GET /_ping` (or `HEAD /_ping`) is deliberately tiny: a short body and a set of response headers describing the daemon. `Api-Version` carries the maximum API version this daemon speaks, `Builder-Version` says which builder is the default, `Ostype` says whether it runs Linux or Windows containers, and an experimental flag says whether experimental features are on. It is the cheapest way to answer both "is the daemon alive?" and "what does it speak?", which is why health checks and clients alike use it. ### What the SDKs do Negotiation in the official SDKs is a one-liner and is not on by default in every client constructor. The Go SDK's `client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())` defers the decision until the first real call: it pings, compares the daemon's `Api-Version` with the version the library was built against, and pins the *lower* of the two for the life of the client. Without that option a client uses the version it was compiled with and will fail against an older daemon. The rule to remember is that negotiation is one-directional. If the client is newer, it steps down to what the daemon offers, losing access to endpoints and fields that only exist in later versions. If the client is *older*, nothing happens at all — it simply asks for its own version, which the daemon still serves. There is no mechanism by which a client makes an old daemon behave like a new one. ### The rejection When a client insists on a version the daemon does not implement, the daemon refuses with a `400` and a message of the recognisable form: ``` Error response from daemon: client version 1.44 is too new. Maximum supported API version is 1.43 ``` That string is diagnostic: it names both halves of the mismatch, so you can see immediately which side is ahead. The symmetric case — a request for a version *below* the daemon's minimum — is also refused, which is how support for very old clients is eventually dropped. ### Why any of this exists A container engine is infrastructure: the daemon under it is upgraded on a host's schedule, while the clients driving it — CI images, agents, libraries vendored into applications, a laptop's CLI — are upgraded on many other schedules. Without per-request versioning, every engine upgrade would be a coordinated fleet-wide upgrade of everything that talks to it. Versioned paths turn that into a compatibility promise the daemon keeps on its own side. ### Practical consequences A few habits follow. Pin an explicit version prefix in scripts and internal tools so an engine upgrade cannot silently change a response shape under you. Turn on negotiation in SDK clients that run on machines you do not control, so a client built against a new engine still works on an older host. Treat `GET /_ping` as the first call any new integration makes, both as a liveness probe and to record what the far side supports. And when you read someone else's `curl` recipe from a blog post, notice the version in the path: a recipe written against an older API version can be missing query parameters or fields you need, or can rely on ones that were later removed. ### What an interviewer is testing That you know the version lives in the request rather than in a handshake at connect time, that `/_ping` is how a client learns the daemon's maximum, and that compatibility flows one way — the daemon meets old clients where they are, and no client can conjure behaviour an old daemon does not have.
- What do you get if you call an endpoint without any version prefix?The daemon serves it as its newest supported API version. That is fine at a shell prompt and risky in tooling, because the response shape then changes the day the host's engine is upgraded — new fields appear, defaults can differ, and a parser written against the old shape may break. Long-lived scripts and integrations should name a version explicitly.
- Why does the daemon keep serving old API versions instead of just the newest?Because the daemon and its clients upgrade on different schedules: agents, CI images and vendored libraries all drive the same engine. Serving a range of versions, and shaping each response to the version requested, means a host upgrade is not a coordinated upgrade of everything that talks to it. Below a documented minimum, support is eventually dropped.
saying these in an interview costs you the question
- Thinks the Engine API is unversioned
- Says a new client silently gets new behaviour from an old daemon
- Believes /_ping returns the version in its body
- Assumes negotiation can upgrade the daemon
- Thinks the version is sent in a custom header