Skip to content

What is Docker?

Measured on Docker Desktop 4.81.0, Engine 29.6.1 (API 1.55), macOS on Apple Silicon, 2026-09-30.

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 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 ls
NAME DESCRIPTION DOCKER ENDPOINT
default Current DOCKER_HOST based configuration unix:///var/run/docker.sock
desktop-linux * Docker Desktop unix:///Users/avetavos/.docker/run/docker.sock
orbstack OrbStack unix:///Users/avetavos/.orbstack/run/docker.sock
podman Podman unix:///var/folders/.../podman-machine-default-api.sock

On 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 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
From the CLI down to your process
$ docker version --format '{{json .Server.Components}}' # names and versions only
Engine 29.6.1
containerd v2.2.5
runc 1.3.6
docker-init 0.19.0

docker-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:

scripts/api-version.sh
#!/bin/sh
# Docker Desktop for Mac socket path; on Linux use /var/run/docker.sock
curl -s --unix-socket "$HOME/.docker/run/docker.sock" http://localhost/version

Reduced 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 arm64

The 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:

scripts/api-run.sh
#!/bin/sh
S="$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/start
curl -s --unix-socket "$S" -X POST http://localhost/containers/docker-demo-api/wait
docker logs docker-demo-api
docker rm docker-demo-api

Real output:

{"Id":"31bdcccd9aea8df38688f779d2f4c40d3bf0aac7fb14fecc6941a61ac4db7f60","Warnings":[]} # create -> 201
# start -> 204 No Content
{"StatusCode":0} # wait -> 200
hello from the API
docker-demo-api

That 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).

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.

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"]
Anatomy of an image reference

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-alpine
Name: docker.io/library/nginx:1.31-alpine
MediaType: application/vnd.oci.image.index.v1+json
Digest: 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.

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
What happens when you run docker run
  1. You type docker run nginx.
  2. The CLI sends the requests to dockerd over the Unix socket: if the image is absent, a pull (POST /images/create); then POST /containers/create and POST /containers/{id}/start; and an attach to stream output.
  3. dockerd checks the local image cache for nginx:latest.
  4. If the image is absent, dockerd contacts Docker Hub, authenticates if needed, and pulls the image layers.
  5. The layers are stored in the local cache (Docker Root Dir: /var/lib/docker in docker info — inside the Desktop VM on a Mac).
  6. dockerd asks containerd/runc to create the container from the image and start the process.
  7. Standard output from the container is streamed back through dockerd to 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 create
container attach
container start
container die
container destroy

destroy appears because the container was started with --rm; without it the sequence stops at die and the container stays in docker ps -a.

An AI-written “production” Compose file for a static site:

🐛 Spot the bug

compose.yaml
services:
web:
image: nginx:latest # "always get the newest, most secure nginx"
ports:
- "80:80"
restart: unless-stopped
Show 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 d2482869a6b8

The 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):

compose.yaml
services:
web:
image: nginx:1.31-alpine
ports:
- "127.0.0.1:8080:80"
restart: unless-stopped

Pinning 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).

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.

OptionBenefitCost
ContainerLightweight, shares the host kernel, starts fast, good fit for running many instancesWeaker isolation than a VM because the kernel is shared
VMStrong isolation via a full hypervisorHeavy — needs its own guest OS and uses far more resources
Daemon with a socket APIAny client (CLI, curl, CI, IDE) can drive it; remote engines are easySocket access is effectively root on the engine host
Tag (nginx:1.31-alpine) vs digest (@sha256:…)Tags are readable and receive patch updatesTags move; only the digest pins bytes
  • Treating an image and a container as the same thing — an image is a static template, a container is a running instance.
  • Using the latest tag in production, which makes builds non-reproducible because latest can 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.
  • “The docker command 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.sock on 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.
  • “nginx is a different image from docker.io/library/nginx:latest.” They are the same reference; the CLI fills in the registry, namespace and latest defaults.
What is the role of the Docker daemon (dockerd)?
Where does the Docker CLI send its commands by default?
What is a Docker image?
In the image reference docker.io/library/nginx:1.31-alpine, what does 1.31-alpine represent?
Which component actually sets up namespaces and cgroups and starts your process?
A Compose file says image: nginx:latest. What is the problem?