skip to content

questions

4

What is the Docker Engine API, and how does the docker CLI use it?

level: juniorimportance: must knowfreq 62%

answer

  1. The CLI is not where the work happens
  2. Something is listening on a socket
  3. Ordinary HTTP over /var/run/docker.sock
  4. Version-prefixed paths, JSON bodies
  5. docker run is create plus start

basics

~20 s

The Docker Engine API is the JSON-over-HTTP API that dockerd serves, by default on the unix socket /var/run/docker.sock. The docker CLI holds no container logic: each command becomes an HTTP request such as POST /containers/create.

solid answer

~50 s

`dockerd` is an HTTP server. The Docker Engine API is the REST-style API it serves, by default on the unix socket `/var/run/docker.sock`, and the `docker` CLI is a thin client over it: `docker ps` is `GET /containers/json`, and `docker run` is really `POST /containers/create` followed by `POST /containers/{id}/start`. Paths carry a version prefix (`/v1.43/containers/json`), request and response bodies are JSON, and long-running calls such as `/events`, `/containers/{id}/logs?follow=1`, image pulls and builds stream instead of returning once. You can call it by hand: `curl --unix-socket /var/run/docker.sock http://localhost/v1.43/containers/json` — the hostname is ignored because the transport is the socket. There is no login on that API; on Linux the socket's ownership (`root:docker`) is the access control. Everything that automates Docker — Compose, CI agents, monitoring agents, the Go and Python SDKs — is another client of the same API.

code

bash · 14 lines
bash
# List running containers
curl -s --unix-socket /var/run/docker.sock \
  http://localhost/v1.43/containers/json

# Create and start a PHP-FPM container, the two calls the CLI makes
id=$(curl -s --unix-socket /var/run/docker.sock \
  -H 'Content-Type: application/json' \
  -d '{"Image":"php:8.3-fpm","Cmd":["php-fpm"]}' \
  -X POST 'http://localhost/v1.43/containers/create?name=pdfsign' \
  | sed 's/.*"Id":"\([^"]*\)".*/\1/')

curl -s -o /dev/null -w '%{http_code}\n' -X POST \
  --unix-socket /var/run/docker.sock \
  "http://localhost/v1.43/containers/$id/start"

go deeper

for a junior

Be able to say in one sentence that dockerd serves an HTTP API on /var/run/docker.sock and the docker command is just a client of it. Naming one endpoint, such as GET /containers/json, is enough at this level.

for a middle

Be ready to decompose a CLI command into its calls — create, then start, with a pull in between when the image is missing — and to run one of them with curl --unix-socket, including the JSON body and Content-Type header.

for a senior

Show that you reach for the API when the CLI is misleading: reading the exact HostConfig a tool sent, or handling the streaming endpoints correctly so a build that failed mid-stream is not reported as a success.

for a principal

Own the decision of how your platform's tooling talks to the engine — an SDK against a pinned API version versus shelling out to a CLI — and the blast radius that follows from any process being able to open that socket.

### The daemon is a web server Docker is a client/server system. `dockerd` is the long-running daemon that owns images, containers, networks and volumes; the `docker` binary you type at is a *client* that holds none of that logic. Between them sits the Docker Engine API: a JSON-over-HTTP API that `dockerd` serves, by default, on the unix socket `/var/run/docker.sock`. Every container you have ever started was started by an HTTP request. Two consequences follow immediately. First, the `docker` binary is not where the interesting behaviour lives — it is a small HTTP client, and its power comes entirely from being able to open that socket. Second, anything else that can open the socket has the same power: Compose, build tooling, CI agents, monitoring agents, IDE plugins and your own shell scripts are all peers of the CLI, speaking one API. ### The shape of a request Because the transport is a unix socket there is no host to address. `curl` needs `--unix-socket`, and then any hostname at all, which it ignores: ``` curl -s --unix-socket /var/run/docker.sock http://localhost/v1.43/containers/json ``` Paths are prefixed with an API version. Bodies are JSON and a POST needs `Content-Type: application/json` — omit it and the daemon rejects the request rather than guessing. Options a CLI user passes as flags appear as JSON fields or query parameters: `docker ps -a` is `GET /containers/json?all=true`, and a status filter is a URL-encoded JSON map in the `filters` parameter. The endpoint families track the CLI's nouns — `/containers`, `/images`, `/networks`, `/volumes`, `/exec` — plus `/version`, `/info`, `/_ping`, `/events` and `/build`. ### What `docker run` actually is `docker run -d php:8.3-fpm` looks atomic and is not. The client performs: 1. `POST /containers/create`, with the image, command, environment and a `HostConfig` object carrying the host-level settings (mounts, port bindings, resource limits). Success is `201 Created` with an `Id`. 2. If that returns `404` because the image is not present locally, the CLI issues `POST /images/create?fromImage=php&tag=8.3-fpm` and retries. The API never pulls implicitly; the automatic pull is a client convenience. 3. `POST /containers/{id}/start`. 4. In the foreground case, an attach or logs call plus `POST /containers/{id}/wait` to learn the exit code. Knowing this decomposition is what makes API-level debugging possible. When `docker run` fails you can say whether creation, the pull or the start failed, because they are separate calls with separate status codes — `201` created, `404` no such image, `409` name already in use, `500` when the runtime refuses to start the process. ### Streaming endpoints Some endpoints do not return once. `GET /events`, `GET /containers/{id}/logs?follow=1`, `GET /containers/{id}/stats`, `POST /images/create` and `POST /build` hold the response open and stream — newline-delimited JSON progress objects for pulls and builds, framed byte chunks for logs and attach. A subtlety that catches people writing their own clients: a pull or a build answers `200 OK` as soon as it *starts*, and a failure arrives later *inside* the stream, as an object carrying an `errorDetail` field. A client that only checks the HTTP status will report a failed build as a success. Attach and exec go further: after the response headers the connection is hijacked and stops being HTTP, becoming a bidirectional byte stream. ### Clients and SDKs Docker publishes SDKs for Go (`github.com/docker/docker/client`) and Python (`docker`), and community libraries exist for other languages; all of them are wrappers over these HTTP calls. Choosing an SDK over shelling out to the CLI buys structured errors, no output parsing, correct handling of the streaming endpoints, and version negotiation. Shelling out buys familiarity and adds a dependency on human-readable output that changes between releases. ### There is no login The API has no username and password. On a default Linux install the socket file is owned by `root` with group `docker` and group write permission, so group membership is the whole access-control story and requests arrive at the daemon unauthenticated. That is exactly why exposing this API is treated as a security subject in its own right. ### What an interviewer is testing That you know the CLI is a thin client, that the socket carries ordinary HTTP, and that you could reach for `curl` when the CLI is unavailable or is hiding something — for example to see the exact `HostConfig` JSON a tool sent, or when writing an agent that must not shell out to a binary.

  • What does POST /containers/create return, and what does a 404 from it mean?
    `201 Created` with a body containing `Id` and any `Warnings`. A `404` means the image is not present on that daemon — the API does not pull implicitly, so the CLI reacts by calling `POST /images/create` and retrying. `409` means the name is already taken, and `400` means the JSON body was malformed or the `Content-Type` header was missing.
  • Why can an image pull or a build return HTTP 200 and still have failed?
    Those endpoints stream. The daemon sends `200 OK` when the operation begins, then emits newline-delimited JSON progress objects while it runs; a failure shows up as an object with an `errorDetail` field near the end of the stream. A correct client reads the whole stream and inspects each object rather than trusting the status line.
  • When would you use an SDK rather than shelling out to the docker CLI?
    Whenever the caller is a program rather than a person. An SDK gives typed requests, structured errors instead of parsed stderr, proper handling of the streaming and hijacked endpoints, and version negotiation. Shelling out couples you to human-readable output that changes between releases and forces you to reimplement stream handling badly.

The docker CLI is a browser for exactly one website: all the behaviour lives on the server, and the client only knows which URLs to open and how to print what comes back.

saying these in an interview costs you the question

  • Thinks the docker CLI creates containers itself
  • Believes docker run is one API call
  • Says the API needs a username and password
  • Thinks the socket speaks a custom binary protocol
  • Assumes container create pulls a missing image
  • Cannot name a single endpoint under /containers

context

open as a page

How does a Docker Engine API client negotiate which API version it uses?

level: middleimportance: should knowfreq 38%

basics

~20 s

Docker 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.

open as a page

How do you build a Docker Engine API /events client that misses no container deaths?

level: seniorimportance: should knowfreq 34%

basics

~20 s

Treat GET /events as a long-lived stream that will break: reconnect with backoff, pass since set to the last event's time so the daemon replays the gap, and reconcile against GET /containers/json on every reconnect because that replay buffer is bounded.

open as a page

Why does raw output from the Docker Engine API's /containers/{id}/logs look corrupted?

level: middleimportance: nice to knowfreq 16%

basics

~20 s

It is framed, not corrupted. A container created without a TTY has stdout and stderr multiplexed into one stream, each chunk preceded by an 8-byte header holding the stream number and payload length. The docker CLI demultiplexes it.

open as a page