Under the Hood: How the Docker CLI Talks to the Docker Engine REST API
Every docker command is an HTTP request. Learn how the Docker CLI communicates with the dockerd REST API over the Unix socket, and how to call the API directly with curl.
Published on • October 28, 2026
AI Assistant

There’s a moment in every Docker user’s journey when the CLI stops feeling like a tool and starts feeling like magic: docker run spins up a container, docker ps lists them, docker build layers an image — how does a user-space binary do all that? The answer, documented in the Docker Engine API reference, is refreshingly mundane: the Docker CLI is just an HTTP client, and the Engine (dockerd) is a REST server.
Once you internalize that, a lot of Docker’s behavior stops being mysterious, and a lot of automation becomes possible without shelling out to the CLI at all.
The architecture: one socket, two processes
When you install Docker, you get (at least) two pieces:
dockerd— the Docker daemon, a long-running privileged process that manages images, containers, networks, and volumes on the host.docker— the CLI, a stateless binary that knows nothing about containers itself.
The CLI never touches containers. It serializes your command into an HTTP request and sends it over a socket to the daemon, which does the actual work and returns a response the CLI renders for you. By default that socket is a Unix domain socket at /var/run/docker.sock on Linux and macOS, and a named pipe (//./pipe/docker_engine) on Windows.
The API itself is versioned via the URL path — every request lands on something like /v1.43/containers/json. The CLI negotiates a version with the daemon at startup; you can pin one yourself with DOCKER_API_VERSION=1.43, which is valuable when you have mixed-version hosts and don’t want a CLI upgrade silently changing request semantics.
Proving it with curl
You don’t have to take this on faith. With the Docker socket mounted, the API is one curl away:
# What containers exist? (docker ps)
curl --unix-socket /var/run/docker.sock \
http://localhost/v1.43/containers/json
# Start a container (docker start <id>)
curl --unix-socket /var/run/docker.sock \
-X POST http://localhost/v1.43/containers/<id>/start
# Engine-wide info (docker info)
curl --unix-socket /var/run/docker.sock http://localhost/v1.43/info
That’s the entire contract. docker ps -a is GET /containers/json?all=true; docker rm is DELETE /containers/{id}; docker logs -f is GET /containers/{id}/logs with follow=1 and chunked transfer encoding for the stream. Mapping commands you already know to their endpoints makes the reference documentation immediately readable — and makes the CLI’s flags legible, since most of them are just query parameters.
Where this knowledge pays off
Debugging becomes direct. When the CLI misbehaves, hitting the endpoint with curl tells you whether the problem is the CLI, the transport, or the daemon. A curl that works while docker ps fails narrows your search instantly.
Streaming and long operations make sense. Build and pull responses stream JSON progress frames; exec attaches use hijacked connections. Knowing these are plain HTTP streaming patterns (rather than some proprietary protocol) makes timeouts and proxies far less surprising.
Non-Node integrations get easy. Anything that can speak HTTP — Go, Python, Node, shell scripts, CI jobs — can drive the Engine without the CLI binary. Language-agnostic Docker clients are, under the hood, generated from the same OpenAPI spec Docker publishes.
Security conversations get concrete. The same curl that impressed you should worry you: anything with access to /var/run/docker.sock effectively has root on the host. That’s why the API supports TLS with client-certificate authentication for remote access (dockerd -H tcp://... with --tlsverify), and why socket-exposing sidecars (like the Socket Proxy project) exist. If your threat model includes anything inside the network, the socket is the first thing to lock down.
A note on the “API version” myth
People often assume the Engine API and the Docker Engine product versions are tightly coupled — and they are related (Engine 27.x serves API v1.46), but the API is intentionally stable and backward-compatible within its versioning scheme. The practical rule: don’t infer capabilities from the CLI version; check which API version your daemon serves (curl .../version) and consult the reference for that version.
The bottom line
The Docker CLI’s superpower is that it has none — it’s a thin, readable HTTP client over a well-documented REST API served by dockerd. Internalize the socket, the versioned paths, and the request/response shapes, and you gain three things: a mental model that demystifies every command, a debugging tool (curl) that bypasses layers of abstraction, and a direct integration path for any system that can make an HTTP request.