Suggest an editImprove this articleRefine the answer for “Docker daemon and Docker client: how do they interact?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**The Docker daemon (`dockerd`)** is a background service that owns all Docker state: images, containers, networks, volumes. **The Docker client (`docker` CLI)** is a thin frontend that converts your commands into REST calls and sends them to the daemon over a Unix socket or TCP. ```bash # CLI -> daemon over /var/run/docker.sock $ docker ps # Translates to: # GET http://docker/v1.46/containers/json ``` **Key:** the client is stateless; the daemon holds everything. You can point the client at a remote daemon with `DOCKER_HOST=tcp://...` for remote management, but the daemon and what it manages are always together.Shown above the full answer for quick recall.Answer (EN)Image**The Docker daemon and Docker client** make up a classic client-server pair. The daemon does the actual work; the client is a frontend that speaks REST. Understanding this split is what lets you debug "why is the CLI hanging" or "why is my Mac talking to dockerd inside a VM". ## Theory ### TL;DR - **Daemon (`dockerd`)** = long-running root process. Manages images, containers, networks, volumes. Owns ALL the state. - **Client (`docker` CLI)** = stateless frontend. Translates your commands into REST API calls. - **Transport** = Unix socket (`/var/run/docker.sock`) by default; TCP socket if you set `DOCKER_HOST=tcp://...`. - **Wire format** = HTTP + JSON. The Engine API is versioned (`/v1.46/...`); the daemon negotiates the version with the client. - **One-to-many** = you can point one CLI at multiple daemons via Docker contexts (`docker context use ...`). ### Quick example ```bash # What `docker ps` actually does: $ curl --unix-socket /var/run/docker.sock \ http://docker/v1.46/containers/json | jq '.[0]' { "Id": "a3f9d2b8c1e4...", "Names": ["/web"], "Image": "nginx:1.27-alpine", "Status": "Up 5 minutes", "Ports": [{ "PrivatePort": 80, "PublicPort": 8080, "Type": "tcp" }] } ``` That is exactly what the CLI did under the hood when you typed `docker ps`. A GET against the socket; the daemon returns JSON; the CLI formats it into a table. ### Daemon (`dockerd`) A Linux service, usually started by systemd as root. Responsibilities: - **Image management:** pulls from registries, builds via BuildKit, stores under `/var/lib/docker/`. - **Container lifecycle:** delegates to `containerd` and `runc`, but owns the user-facing container API. - **Networking:** creates virtual bridges, manages iptables rules, allocates IP addresses for containers. - **Volumes:** named volumes, bind mounts, plugin-based storage drivers. - **API endpoint:** listens on the configured socket(s). Configuration lives in `/etc/docker/daemon.json` and command-line flags. Typical settings: `data-root` (where to store images), `storage-driver` (overlay2 by default), `log-driver`, `dns`, `registry-mirrors`. ### Client (`docker` CLI) A single binary, no daemon, no state of its own. When you type a command: 1. Parses arguments (`docker run -p 8080:80 nginx`). 2. Looks up the active context (which daemon to talk to). 3. Builds an HTTP request with appropriate headers (`Content-Type: application/json`, API version). 4. Sends it over the socket; reads the response. 5. Formats the response (table, JSON, custom template via `--format`). The CLI is so thin that you can replace it with `curl` for any operation, and many docs do exactly that. ### Transport: Unix socket vs TCP By default, `dockerd` listens on a Unix socket: `/var/run/docker.sock`. Local-only, fast, secured by file permissions. ```bash $ ls -l /var/run/docker.sock srw-rw---- 1 root docker 0 Apr 30 10:00 /var/run/docker.sock # Mode 660: owner=root, group=docker. To use docker without sudo, # add yourself to the `docker` group. ``` For remote management, the daemon can also listen on TCP. Configure in `daemon.json`: ```json { "hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2376"], "tls": true, "tlsverify": true, "tlscacert": "/etc/docker/ca.pem", "tlscert": "/etc/docker/server-cert.pem", "tlskey": "/etc/docker/server-key.pem" } ``` Then point a remote client at it: ```bash $ DOCKER_HOST=tcp://docker.example.com:2376 docker ps ``` **Always use TLS for TCP exposure.** An unprotected `tcp://0.0.0.0:2376` is a remote root shell waiting to happen - the Docker API can run any container with `--privileged`, mount host paths, and effectively own the box. ### Docker contexts One CLI, multiple daemons. Useful when you manage local dev plus a remote prod or staging. ```bash $ docker context create staging \ --docker "host=ssh://user@staging.example.com" $ docker context ls NAME DESCRIPTION DOCKER ENDPOINT default Local Docker unix:///var/run/docker.sock staging ssh://user@staging.example.com $ docker context use staging $ docker ps # now lists containers on staging, over SSH ``` No more `DOCKER_HOST=...` env-var juggling. Switch with one command. ### API versioning The Engine API is versioned. The CLI sends a version like `/v1.46/...`; the daemon either accepts it or, if too old, returns a downgrade hint. Forward compatibility means a v25 client can usually talk to a v23 daemon (and vice versa within reason). Mismatch: ```bash $ docker ps Error response from daemon: client version 1.47 is too new. Maximum supported API version is 1.45. ``` Fix: upgrade the daemon, or pin the client with `DOCKER_API_VERSION=1.45`. ### Common mistakes **Adding yourself to the `docker` group as a security "convenience"** ```bash $ sudo usermod -aG docker $USER # Now you can run docker without sudo. Convenient. Also dangerous. ``` Being in the `docker` group is **equivalent to root**. The daemon API can mount host paths into a container and chmod files - any unprivileged user with socket access has a path to root. For a developer machine, fine. For a multi-user server, no. **Exposing TCP without TLS** Found on countless internet-facing servers. `dockerd -H tcp://0.0.0.0:2375` (note the unencrypted port) gives any internet user the ability to run privileged containers and root the host. Always TLS, always client cert auth. **Confusing the CLI being broken with the daemon being broken** ```bash $ docker ps Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running? ``` The error is honest: it could be the daemon stopped (`systemctl status docker`), the socket file missing, or a permissions issue (you are not in the `docker` group). The CLI itself is rarely broken; the message points at the daemon. **Restarting `dockerd` to fix something and accidentally killing all containers** With default settings on older Docker, `systemctl restart docker` killed containers. Modern Docker has `live-restore`: ```json // /etc/docker/daemon.json { "live-restore": true } ``` With this on, `systemctl restart docker` keeps containers running across daemon restarts. The containerd-shim per container is what makes this work. ### Real-world usage - **Docker Desktop on Mac/Windows:** the daemon lives inside a small Linux VM. Your `docker` CLI on the host talks to it via a forwarded socket. You see this in Docker Desktop > Settings > Resources where you allocate the VM's RAM and CPU. - **CI/CD runners:** each runner has its own dockerd. Jobs use the local daemon to build and push images. "Docker-in-Docker" is a pattern where a job runs `dockerd` itself inside a privileged container, but it has security tradeoffs. - **Remote-host management:** ops teams use `docker context` over SSH to admin a remote staging or prod host without giving every admin a TCP-exposed daemon. - **Rootless Docker:** `dockerd` running as a non-root user. Each user gets their own daemon and socket. Used in HPC clusters and security-sensitive environments. ### Follow-up questions **Q:** What happens if the daemon dies but containers are running? **A:** With `live-restore` enabled (default in modern Docker), containers keep running. The containerd-shim per container holds them. The CLI cannot interact with the daemon during this window, but the containers themselves and their network keep working. When `dockerd` restarts, it reattaches. **Q:** Can two clients talk to the same daemon at once? **A:** Yes. The daemon serializes operations on shared state but happily handles many concurrent clients. CI runs and your local `docker ps` against the same daemon work fine in parallel. **Q:** Why does my command line hang on `docker ps`? **A:** Almost always a daemon issue. Either it is overloaded (many concurrent operations), deadlocked on a storage driver issue, or stuck waiting on a hung containerd. `journalctl -u docker` and `journalctl -u containerd` are your first stops. **Q:** Can I write my own Docker client? **A:** Yes - the Engine API is documented and stable. The official Go SDK (`github.com/docker/docker/client`) is what the `docker` CLI itself uses. Plenty of third-party tools (Lazydocker, Portainer, Dockge) are essentially custom clients on top of the same API. **Q:** (Senior) What is the security exposure of mounting `/var/run/docker.sock` into a container? **A:** Mounting the socket gives that container full control over the host's Docker daemon. From inside, it can launch any container with `--privileged`, bind-mount `/`, and effectively become root on the host. Common in CI tools that need to spawn build containers (DinD alternative), but a known supply-chain risk: a malicious image with socket access escalates to host root. Use rootless Docker, or `sysbox`, or per-runner dockerds to limit the blast radius. ## Examples ### Hitting the Engine API directly ```bash # List all images $ curl --unix-socket /var/run/docker.sock \ http://docker/v1.46/images/json | jq '.[].RepoTags' ["nginx:1.27-alpine"] ["postgres:16", "postgres:latest"] # Create and start a container in two API calls $ CID=$(curl --unix-socket /var/run/docker.sock \ -H 'Content-Type: application/json' \ -X POST "http://docker/v1.46/containers/create?name=test" \ -d '{"Image":"alpine","Cmd":["echo","hi"]}' | jq -r .Id) $ curl --unix-socket /var/run/docker.sock \ -X POST http://docker/v1.46/containers/$CID/start ``` No `docker` CLI involved. The CLI is convenience; the API is authoritative. ### Switching between local and remote ```bash $ docker context create prod-eu --docker "host=ssh://ops@prod-eu.example.com" $ docker context create prod-us --docker "host=ssh://ops@prod-us.example.com" $ docker context use prod-eu $ docker ps # containers in EU $ docker --context prod-us ps # one-off command against US ``` One CLI, three daemons (local, EU, US). The contexts file at `~/.docker/contexts/` holds the routing. ### Tracing a CLI call to confirm the wire protocol ```bash $ DOCKER_DEBUG=true docker ps 2>&1 | head -10 GET /v1.46/containers/json HTTP/1.1 Host: docker User-Agent: Docker-Client/26.1.0 (linux) Content-Type: application/json HTTP/1.1 200 OK Api-Version: 1.46 Content-Type: application/json Docker-Experimental: false Server: Docker/26.1.0 (linux) ``` GET request, JSON response, version negotiation in headers. That is all there is to the protocol.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.