What is Docker?
Measured on Docker Desktop 4.81.0, Engine 29.6.1 (API 1.55), macOS on Apple Silicon, 2026-09-30.
The four pieces of Docker
Section titled “The four pieces of Docker”Docker is not a single program — it is an ecosystem of components that work together. Understanding each piece makes it much easier to reason about what happens when you run a docker command.
The Docker client (docker CLI)
Section titled “The Docker client (docker CLI)”The docker CLI is the program you interact with directly. When you type a command such as docker run, docker build, or docker pull, the CLI translates that instruction into a REST API request and forwards it to the Docker daemon. The client itself does no heavy lifting — it is a thin API client.
The client communicates with the daemon over a Unix socket at /var/run/docker.sock by default. For remote daemons (for example, a Docker host running on a cloud VM), the client can connect over SSH, or over TCP with TLS.
Which daemon the CLI talks to is called a context. On this Mac other engines are installed as well, so docker context ls has four entries; the * marks the active one:
$ docker context lsNAME DESCRIPTION DOCKER ENDPOINTdefault Current DOCKER_HOST based configuration unix:///var/run/docker.sockdesktop-linux * Docker Desktop unix:///Users/avetavos/.docker/run/docker.sockorbstack OrbStack unix:///Users/avetavos/.orbstack/run/docker.sockpodman Podman unix:///var/folders/.../podman-machine-default-api.sockOn Docker Desktop the socket is ~/.docker/run/docker.sock (and /var/run/docker.sock is a symlink to it on this Mac). On a Linux server it is a plain /var/run/docker.sock owned by the docker group.
The Docker daemon (dockerd)
Section titled “The Docker daemon (dockerd)”The daemon is the long-running background process that does the actual work. It listens on the Unix socket (or TCP port) and handles every API call the client sends. The daemon is responsible for:
- Pulling and caching images from registries.
- Creating and running containers from those images.
- Managing networks so containers can communicate with each other and with the outside world.
- Managing volumes for persistent data that outlives a container’s lifetime.
dockerd itself does not run your process. It delegates to containerd (image and container lifecycle) which delegates to runc (the low-level tool that sets up namespaces and cgroups and execs your process). Both versions are listed in docker version:
flowchart TB cli["docker CLI"] dockerd["dockerd (Engine 29.6.1)"] containerd["containerd v2.2.5"] runc["runc 1.3.6"] proc["your process"] cli -->|REST over socket| dockerd --> containerd --> runc --> proc
$ docker version --format '{{json .Server.Components}}' # names and versions onlyEngine 29.6.1containerd v2.2.5runc 1.3.6docker-init 0.19.0docker-init is the tiny init process you get with docker run --init (the first-container lesson shows why it matters).
Prove it: talk to the daemon without the CLI
Section titled “Prove it: talk to the daemon without the CLI”Because the CLI is only a REST client, curl can do the same job. /version is the API behind docker version:
#!/bin/sh# Docker Desktop for Mac socket path; on Linux use /var/run/docker.sockcurl -s --unix-socket "$HOME/.docker/run/docker.sock" http://localhost/versionReduced to the fields we care about (Version, ApiVersion, MinAPIVersion, KernelVersion, Os, Arch):
Version 29.6.1 | ApiVersion 1.55 | MinAPIVersion 1.40 | KernelVersion 6.12.76-linuxkit | Os linux | Arch arm64The minimum API version 1.40 is how the engine stays compatible with older clients. Now create, start and read a container with nothing but HTTP:
#!/bin/shS="$HOME/.docker/run/docker.sock"curl -s --unix-socket "$S" -H 'Content-Type: application/json' -X POST \ -d '{"Image":"alpine:3.24","Cmd":["echo","hello from the API"]}' \ 'http://localhost/containers/create?name=docker-demo-api'curl -s --unix-socket "$S" -X POST http://localhost/containers/docker-demo-api/startcurl -s --unix-socket "$S" -X POST http://localhost/containers/docker-demo-api/waitdocker logs docker-demo-apidocker rm docker-demo-apiReal output:
{"Id":"31bdcccd9aea8df38688f779d2f4c40d3bf0aac7fb14fecc6941a61ac4db7f60","Warnings":[]} # create -> 201 # start -> 204 No Content{"StatusCode":0} # wait -> 200hello from the APIdocker-demo-apiThat is exactly what docker run does, plus an attach so the output streams to your terminal. The reference for every endpoint is the Engine API (the docs list the API version you are on).
Images
Section titled “Images”An image is a read-only, layered package that contains everything needed to run a process: the application code, a base OS filesystem, runtime libraries, environment variables, and the default command to execute. Images are built from a Dockerfile and are identified by a name and tag such as nginx:1.31-alpine.
Each RUN, COPY and ADD instruction in the Dockerfile creates a filesystem layer (metadata-only instructions such as ENV or CMD add none — the images module proves it). Layers are cached and shared across images, which keeps disk usage low and rebuilds fast.
Registries
Section titled “Registries”A registry is a server that stores and distributes images. The default public registry is Docker Hub (docker.io). When you run docker pull nginx, the daemon contacts docker.io, finds the nginx repository, and downloads the image to the local cache.
Registries organise images into repositories. A repository can hold many versions of the same image, distinguished by tags:
flowchart TB ref["docker.io/library/nginx:1.31-alpine"] ref --> host["registry host: docker.io"] ref --> ns["namespace: library (official images)"] ref --> repo["repository name: nginx"] ref --> tag["tag: 1.31-alpine"]
docker buildx imagetools inspect prints the fully qualified name the daemon resolves, plus the digest and one manifest per platform:
$ docker buildx imagetools inspect nginx:1.31-alpineName: docker.io/library/nginx:1.31-alpineMediaType: application/vnd.oci.image.index.v1+jsonDigest: sha256:df221db836e1754089190208cee7eeda94f233197056426eda74a43ab1abeac2
Manifests: Name: docker.io/library/nginx:1.31-alpine@sha256:0530961f... Platform: linux/amd64 ...The docker.io/library/ prefix that the CLI hides when you type nginx is the registry host and the library namespace for official images. The index lists one manifest per platform, which is how the same tag serves amd64 and arm64; the local copy on this Mac is arm64.
Private registries (Amazon ECR, GitHub Container Registry, a self-hosted registry) work the same way — you just docker login to authenticate before pushing or pulling.
How the pieces interact
Section titled “How the pieces interact”The flow every docker run command follows:
flowchart TB you["You (terminal): docker run nginx"] cli["docker CLI"] daemon["dockerd (daemon)"] registry["Docker Hub (or other registry)"] cache["Local image cache"] container["Running container (isolated process on host kernel)"] you --> cli cli -->|REST API over Unix socket| daemon daemon -->|Image not in local cache| registry registry -->|Pull image layers| cache cache -->|Create container from image| container
- You type
docker run nginx. - The CLI sends the requests to
dockerdover the Unix socket: if the image is absent, a pull (POST /images/create); thenPOST /containers/createandPOST /containers/{id}/start; and an attach to stream output. dockerdchecks the local image cache fornginx:latest.- If the image is absent,
dockerdcontacts Docker Hub, authenticates if needed, and pulls the image layers. - The layers are stored in the local cache (
Docker Root Dir: /var/lib/dockerindocker info— inside the Desktop VM on a Mac). dockerdasks containerd/runc to create the container from the image and start the process.- Standard output from the container is streamed back through
dockerdto your terminal via the CLI.
You can watch the daemon’s side of steps 6-7 with docker events while running a throw-away container in another terminal:
$ docker events --filter container=docker-demo-ev --format '{{.Type}} {{.Action}}'container createcontainer attachcontainer startcontainer diecontainer destroydestroy appears because the container was started with --rm; without it the sequence stops at die and the container stays in docker ps -a.
Reading AI-generated code
Section titled “Reading AI-generated code”An AI-written “production” Compose file for a static site:
🐛 Spot the bug
services:web: image: nginx:latest # "always get the newest, most secure nginx" ports: - "80:80" restart: unless-stoppedShow the answer
latest is only a tag name; it is a movable pointer, not a promise about age or security, and the file gives no way to know what will run next month. Proven here, first by re-pointing a tag between two different local images:
$ docker tag alpine:3.24 demo-app:latest && docker image ls demo-app --format '{{.Tag}} {{.ID}}'latest 33bee74c45f3$ docker tag busybox:latest demo-app:latest && docker image ls demo-app --format '{{.Tag}} {{.ID}}'latest d2482869a6b8The same tag now means a different image. On the registry, on 2026-09-30, docker buildx imagetools inspect shows nginx:latest, nginx:1, nginx:1.31 and nginx:mainline all resolving to digest sha256:abe47724… — and nginx:stable to a different one (sha256:b972f831…). Which line you get changes the day the maintainers move the tag, and a docker compose pull will silently upgrade you. Pin a version tag (and, for reproducibility, the digest):
services: web: image: nginx:1.31-alpine ports: - "127.0.0.1:8080:80" restart: unless-stoppedPinning trades automatic upgrades for reproducibility: you now own the job of bumping the tag. The fix also binds the published port to 127.0.0.1, because "80:80" publishes on every interface (see the networking module).
Mental Model
Section titled “Mental Model”docker is a remote control. dockerd is the office that owns every image, container, network and volume; it hands the low-level work to containerd and runc, which ask the kernel for namespaces and cgroups and start your process. An image is a read-only template addressed by name, tag, and — the only immutable part — digest. A container is that template plus a writable layer plus a running process.
Trade-offs
Section titled “Trade-offs”| Option | Benefit | Cost |
|---|---|---|
| Container | Lightweight, shares the host kernel, starts fast, good fit for running many instances | Weaker isolation than a VM because the kernel is shared |
| VM | Strong isolation via a full hypervisor | Heavy — needs its own guest OS and uses far more resources |
| Daemon with a socket API | Any client (CLI, curl, CI, IDE) can drive it; remote engines are easy | Socket access is effectively root on the engine host |
Tag (nginx:1.31-alpine) vs digest (@sha256:…) | Tags are readable and receive patch updates | Tags move; only the digest pins bytes |
Common mistakes
Section titled “Common mistakes”- Treating an image and a container as the same thing — an image is a static template, a container is a running instance.
- Using the
latesttag in production, which makes builds non-reproducible becauselatestcan point to a different version over time. - Assuming a container is as secure as a VM without extra hardening, such as leaving the process inside the container running as root.
Common Misconceptions
Section titled “Common Misconceptions”- “The
dockercommand runs my container.” It sends an HTTP request;dockerd(via containerd and runc) runs it. Kill the CLI and a detached container keeps running. - “
/var/run/docker.sockon my Mac is the Linux daemon’s socket.” On Docker Desktop it is a symlink to a socket that the Desktop proxies into the VM. The daemon’s own filesystem, including/var/lib/docker, is inside the VM. - “
nginxis a different image fromdocker.io/library/nginx:latest.” They are the same reference; the CLI fills in the registry, namespace andlatestdefaults.