Docker, 2015–2026  / field guide
Practitioner field guide · 28 September 2026

The parts that outlived the product

Docker spent a decade taking its own product apart, and the pieces it gave away are now the runtime under every major managed Kubernetes service while the product that produced them is one line on their adopter list. This guide reconstructs how that happened from the repositories the changes were made in, and turns it into rules for deciding which of your own components should be given a specified boundary and which should stay inside the product.

38 primary artefacts 22 repositories cloned and counted 5 published incidents Evidence through September 2026 Read: 35 min
01

The territory

A company ships an integrated product, extracts parts of it into components with published interfaces, gives two of them away, and then loses its place in the stack to the consumer that standardised those interfaces. Which parts survive that sequence?

−90%
Engine commits, 2016 to 2021: 10,133 falling to 1,065 a year
26
Named production adopters of containerd, the runtime Docker donated
6y 3m
From the 2018 authorization fix to its first mainline tagged release
26 mo
Between engine feature releases, v20.10.0 to v23.0.0

Start with the fact that reframes everything else. The file ADOPTERS.md in the containerd repository names twenty-six organisations running it in production, among them Google Kubernetes Engine, Amazon EKS, AWS Fargate, Azure Kubernetes Service, Bottlerocket, k3s, Kata Containers and Firecracker. One entry on that list reads "Docker/Moby engine". The component that Docker extracted from its own daemon in 2016 and handed to a foundation in 2017 is now the substrate of the industry, and the product it was extracted from is a peer consumer of it. In July 2025 a commit titled "Enable containerd snapshotters by default" made the donated component the engine's own image store, eight years after the donation, and in June 2026 another added an experimental mode that runs containerd inside the daemon process again.

That is the arc in one paragraph, and it is worth an architect's attention because the same sequence is available to anyone with an internal platform: bundle to win the workflow, extract to reduce the blast radius of your own releases, specify the boundary so others can implement it, and then discover that the specified boundary, not the product, is the thing that was durable. The decade divides cleanly into four phases, each of which is visible as a change in commit volume rather than as an announcement: bundling through 2016, decomposition in 2017, eviction and stall from 2018 to 2022, and consolidation around what was left from 2023 onward.

The finding that surprised me sits inside the third phase. In November 2018 a Docker engineer committed a fix for an authorization bypass: a request with a zero-length body was forwarded to authorization plugins without the body, so a plugin approved what it had never seen. That fix shipped in Docker Engine 18.09.1 in January 2019. The commit itself, 2ac8a479c5, is not an ancestor of v19.03, v20.10, v23.0 or even v27.0.3. It first appears in a tagged mainline release in v28.0.0-rc.1, dated 6 February 2025, because the patch reached master only through a merge on 23 July 2024 whose second parent is the December 2018 commit. Six years and three months, in a repository with thousands of contributors, for a known security fix to cross a branch line created by a restructure.

Scope. This guide covers the container engine and the components around it: the daemon in moby/moby, containerd, runc and the OCI specifications, BuildKit, Compose, the shim Kubernetes removed, and the registry. It deliberately does not cover Docker Desktop's internals (closed source, so the public record is the issue tracker rather than the code), Windows containers, Kubernetes' own architecture, or Docker Inc.'s finances. It is an architecture guide, not a company history.

What this evidence base is missing

The network policy in the session this guide was researched in allowed six hosts: github.com, raw.githubusercontent.com, gitlab.com, pkg.go.dev, pypi.org and hub.docker.com. Every engineering blog, conference video, academic paper and vendor announcement was refused by the proxy. So this page has no blog, talk or paper tier at all, and it cannot quote what anyone said about these decisions at the time. What it can do is read the artefacts the decisions were executed in, which are the three highest tiers of evidence: source code, design records and incident advisories. Where motive is not recorded in a repository, this page says the motive is unknown rather than supplying one.

Figure 1 · Four products, two interfaces

OCI runtime spec

docker CLI
+ dockerd

kubelet
via CRI

podman
no daemon

k3s, Bottlerocket,
nerdctl

containerd
26 named adopters

CRI-O

runc

crun, Kata,
gVisor

Linux kernel
namespaces, cgroups

OCI runtime spec

docker CLI
+ dockerd

kubelet
via CRI

podman
no daemon

k3s, Bottlerocket,
nerdctl

containerd
26 named adopters

CRI-O

runc

crun, Kata,
gVisor

Linux kernel
namespaces, cgroups

Every box above the runtime layer is a separate product with its own release cycle, and all of them meet at two specifications that stopped changing in 2017. Adopters from containerd ADOPTERS.md; daemonless design from podman's README.
Diagram source
02

How it is actually built, then and now

Two shapes, ten years apart, both reconstructed from the tree rather than from documentation: one process that did everything, and a chain of four processes in which the product owns the first and the last is a specification.

Figure 2 · Where the work moved

2k4k6k8k10k 2016: 10133 commits2016: 589 commits162017: 4481 commits2017: 3152 commits172018: 2339 commits2018: 1806 commits182019: 2135 commits2019: 1568 commits192020: 1215 commits2020: 1356 commits202021: 1065 commits2021: 1500 commits212022: 2051 commits2022: 1481 commits222023: 2820 commits2023: 1659 commits232024: 2657 commits2024: 1433 commits242025: 4410 commits2025: 1366 commits252026: 2676 commits2026: 1355 commits26 moby/moby (the engine) containerd (the donated runtime)
Commits per year, counted on 2026-09-28 from bare clones of both repositories. The engine falls by 90% between 2016 and 2021 while the component it extracted holds a flat 1,300 to 1,700 commits a year for a decade. The engine's recovery from 2023 is the containerd integration work, which is the product rebuilding itself on the component. 2026 is a partial year.

In 2016 the shape was one long-running process. dockerd built images, stored them through its own graph drivers, created networks through libnetwork, managed volumes, talked to registries, executed containers through libcontainer, and, from v1.12 on 28 July 2016, ran a Raft-based cluster scheduler in the same binary. That release is the high-water mark of the bundled design: swarmkit, the orchestration library behind it, took 2,601 commits in its first year. The argument for bundling was the one that always wins early in a category, which is that the workflow is the product; a developer typed one command and got a cluster. The cost was equally structural, because everything in that process shared one release cadence, one failure domain and one security boundary.

The 2026 shape is a chain. The CLI talks to dockerd over the same HTTP API it used in 2015, which is the one interface that never broke. The daemon delegates image storage and execution to containerd over gRPC, delegates builds to BuildKit, and containerd starts one shim process per container which in turn executes runc, which configures namespaces and cgroups and then replaces itself with the container's process. The registry is a separate protocol implemented by dozens of servers. Four processes, three trust boundaries, and two written specifications where there used to be method calls.

Figure 3 · 2016: one process, seven responsibilities

docker CLI

dockerd, one process

builder

image store
graph drivers

libnetwork

volumes

registry client

swarm mode
v1.12, July 2016

libcontainer
execution

kernel

docker CLI

dockerd, one process

builder

image store
graph drivers

libnetwork

volumes

registry client

swarm mode
v1.12, July 2016

libcontainer
execution

kernel

Every responsibility below the daemon box shipped on one release cadence, in one process, behind one security boundary. Reconstructed from the tree at v1.12.0, 28 July 2016.
Diagram source

Figure 4 · 2026: the same command, four processes

HTTP API,
unchanged since 2015

build

gRPC

pull

one shim
per container

docker CLI

dockerd

BuildKit

containerd

registry

containerd-shim-runc-v2

runc

kernel

HTTP API,
unchanged since 2015

build

gRPC

pull

one shim
per container

docker CLI

dockerd

BuildKit

containerd

registry

containerd-shim-runc-v2

runc

kernel

The interesting boundary is the gRPC call in the middle: on the other side of it the daemon is replaceable, and for every managed Kubernetes service it already has been. Shim model from containerd's README; default image store from commit 632de98f75, 9 July 2025.
Diagram source

Three things about that chain are worth copying, and one is worth avoiding.

A component defined by refusal

containerd's SCOPE.md is an allow-list. Networking is out: "Networking will be handled and provided to containerd via higher level systems." Build is out. Logging is out. Volumes are out. Anything not listed is out by default. That refusal is why four unrelated products could adopt it without inheriting a philosophy, and it is the part most internal platform teams skip.

Read at: containerd SCOPE.md

A shim process per container

The shim exists so the thing that supervises a container is not the thing that gets upgraded. containerd can restart without killing running containers because the shim holds the container's file descriptors. This is the mechanism that made "upgrade the runtime" a routine operation rather than an outage, and it is the reason a Kubernetes node can change container runtimes at all.

Read at: containerd/containerd

An interface that stopped moving

The OCI image and runtime specifications reached v1.0 on 19 July 2017 and then went quiet: the image spec took 642 commits in 2016, 16 in 2018 and 8 in 2020, and waited six years and seven months for v1.1.0. A frozen interface is what let the implementations on both sides be replaced independently.

Read at: opencontainers/image-spec tags

The thing worth avoiding is the drift back. Networking was pulled out into docker/libnetwork in February 2015 and developed there until its last commit on 20 October 2023; on 14 July 2025 it was moved into daemon/libnetwork inside the engine and on 31 July 2025 the separate tree was removed. In June 2026 the engine gained an experimental mode that runs containerd inside the daemon process instead of as a separate managed process. Both moves are defensible in isolation, since a component whose only consumer is you has all of the coordination cost of a boundary and none of the benefit. Both are also the exact inverse of the change that made the estate survive 2020, so an architecture that re-absorbs its components should be able to say which consumers it expects to lose and be content to lose them.

03

The decisions that matter

Four forks where the repository record shows both the choice and the cost, each with the condition that flips it for a reader in a different position.

Decision: put the orchestrator inside the engine, or leave it outside?

Chosen
  • Swarm mode shipped inside the daemon in v1.12, 28 July 2016
  • One binary, one command, no second control plane to install
Rejected
  • The standalone scheduler in docker/classicswarm, which was archived by commit on 11 June 2020
  • Adopting the competing control plane's interface early
Flips when
  • A competing control plane publishes an interface that more than one runtime implements. From that moment the bundled scheduler is a feature your consumers must opt out of, and swarmkit's commit volume fell from 2,601 in 2016 to 32 in 2025 without anyone ever cancelling it

Decision: donate the runtime, or keep it?

Chosen
  • containerd was extracted, given a written scope, and donated; v1.0.0 landed 4 December 2017
  • The scope document refuses networking, build, logging and volumes by default
Rejected
  • Keeping execution as an internal package of the daemon
  • A component that ships whatever its owner's product needs next
Flips when
  • Your consumers must trust the component without trusting you. If nobody outside your company would run it as a dependency, extraction buys coordination cost and no adoption; if they would, the refusal list is what makes it adoptable

Decision: keep a special-case integration in the consumer, or make it implement the interface?

Chosen
  • Kubernetes deprecated its built-in Docker shim in v1.20, December 2020, and removed it in v1.24
  • Stated reason: "kubelet then has dependency on specific container runtime which leads to maintenance burden"
Rejected
  • Continuing to carry inconsistent behaviour for one runtime
  • Waiting for the runtime's owner to converge
Flips when
  • The special case is the only implementation. Removal became safe precisely because containerd and CRI-O already implemented the interface, and the removed code survived as a separate product, cri-dockerd, maintained by the company that had bought the enterprise business

Decision: defer a feature across a component boundary, or merge it in the product?

Chosen
  • Checkpoint and restore was deferred in February 2016 to "after the containerd integration"
  • Pull request #13602 stayed open while the boundary was built
Rejected
  • Merging the working implementation into the daemon and reworking it later
Flips when
  • The component's owner has no incentive to carry your feature. The pull request was closed unmerged on 4 March 2024, eight years and nine months after it opened, with 320 comments. Deferral across a boundary is a decision to drop the feature unless someone on the other side is accountable for it

Figure 5 · Should this component leave the product?

no

yes

no

yes

no

yes

no

yes

Would anyone outside
run it as a dependency?

Keep it in the tree.
Split the module, not the repo

Can you write down
what it refuses to do?

Not extractable yet.
Write the scope first

Can you stop
changing the interface?

Publish it, keep ownership,
expect one implementation

Do you need adopters
who distrust you?

Specify and version it.
Keep it in-house

Donate it and staff it.
Budget for losing the layer

no

yes

no

yes

no

yes

no

yes

Would anyone outside
run it as a dependency?

Keep it in the tree.
Split the module, not the repo

Can you write down
what it refuses to do?

Not extractable yet.
Write the scope first

Can you stop
changing the interface?

Publish it, keep ownership,
expect one implementation

Do you need adopters
who distrust you?

Specify and version it.
Keep it in-house

Donate it and staff it.
Budget for losing the layer

The terminal nodes are actions, and the left branch is the one most internal platform teams take by accident: a repository split with no specification, which pays the coordination cost of a boundary without buying the optionality.
Diagram source
DecisionChosenRejectedBecause, as recordedEvidence
Split the product into components (2017)Upstream project holding containerd, LinuxKit, BuildKit, SwarmKitOne repository shipping one product"upstream components (containerd, linuxkit etc) -> Moby -> Docker CE -> Docker EE"Pull request #32691, 2017-04-18
Rename the repository, keep the import pathRepository renamed, Go module left as github.com/docker/dockerMoving both at onceObjection in thread: "This is not gonna work nice for all the projects that depend on github.com/docker/docker"Module deprecated with v29, November 2025
Freeze the interface at v1.0 (2017)Image and runtime specs effectively stable for six yearsContinuous evolution of the formatNot stated in the repository; visible as 642 commits in 2016 falling to 8 in 2020image-spec tags
Meter the registry (2020)Pull limits, first as abuse control, then per identityUnmetered anonymous pulls"certain IPs pulling in excess of 60,000 times per 6 hour window"roadmap #87, 2020-05-07
Charge for the developer client (2021)Paid subscription above 250 employees or $10M revenueFree for all commercial useTerms as published; the response is datable: colima's first commit is 2021-09-04Desktop licence terms
Rebuild the engine on the donated component (2022 onward)containerd image store, default from the v29 lineMaintaining graph drivers indefinitelyNot stated; 282 commits to daemon/containerd in 2023 aloneCommit 7d74269c0d, 2022-07-05
Re-absorb networking (2025)libnetwork moved into the daemon tree, separate tree deletedKeeping a component with one consumerNot stated; libnetwork's own commits had fallen to 18 in 2023Commit 53bd828853, 2025-07-31
Own the build path (2018 onward)BuildKit, default builder from Engine 23.0Extending the legacy builder"docker build uses Buildx and BuildKit by default since Docker Engine 23.0"moby/buildkit
04

What broke, and which class it belongs to

Five published incidents from the decade, grouped by the class they belong to rather than by the component they happened in. Three of the five are consequences of the decomposition itself.

Decomposition does not remove defects, it relocates them into the seams. The seams in this estate are of three kinds, and each has produced its own incidents. The first is the seam between branch lines, which appeared when one repository started shipping several product versions: a fix landed on the branch that needed it and did not travel. The second is the seam between the runtime and the kernel, which is not a Docker artefact at all but the property that a container boundary is a convention assembled from namespaces, mounts and masked paths, and therefore keeps producing escapes: runc has published advisories in 2019, 2021, 2022, 2023, 2024, three on a single day in November 2025, and again in June 2026. The third is the seam between a product and the components it no longer maintains, which is where a consumer discovers it now owns a fork.

Figure 6 · The authorization bypass, as the plugin sees it

AuthZ plugindockerdAttackerAuthZ plugindockerdAttackerthe 2018 fix was on the 18.09branch, not on this oneAPI request, Content-Length 0,body presentforward request without the bodyallow, nothing to object toexecute the request,body included
AuthZ plugindockerdAttackerAuthZ plugindockerdAttackerthe 2018 fix was on the 18.09branch, not on this oneAPI request, Content-Length 0,body presentforward request without the bodyallow, nothing to object toexecute the request,body included
The plugin approves a request whose body it was never shown. Sequence reconstructed from the advisory's description of the Content-Length handling in CVE-2024-41110.
Diagram source
Postmortem

A security fix that did not cross the branch line

AssumptionA fix released in a patch version is a fix in the product.
What happenedA 2018 bypass, in which a zero-length Content-Length caused the daemon to forward a request to authorization plugins without its body, was fixed in Engine 18.09.1 in January 2019. The advisory records that "the fix was not carried forward to later major versions, resulting in a regression" affecting every line from 19.03 to 27.1.0.
Blast radiusFive and a half years of releases; any deployment relying on an authorization plugin as its access control. The commit first becomes an ancestor of a mainline tag at v28.0.0-rc.1 on 6 February 2025.
FixA merge on 23 July 2024 whose second parent is the December 2018 commit, plus a body-size limit added in February 2026.
Design ruleAfter a repository restructure, run a containment audit rather than a code audit: for every security commit, ask which tags it is an ancestor of. git tag --contains is a two-second query and it is the only thing that distinguishes "fixed" from "fixed somewhere".
Postmortem

The escape that rewrote the runtime binary

AssumptionA process inside a container cannot reach the binary that created it.
What happenedThrough mishandling of /proc/self/exe, a container with root inside it could overwrite the host's runc binary and gain host root, either from a hostile image or through an exec into a container the attacker already controlled.
Blast radiusEverything running runc through 1.0-rc6, which in February 2019 was substantially every container platform; Docker patched in 18.09.2. CVSS 8.6.
FixCopy the runtime binary into a memfd before executing it, so the file the container can reach is not the file the host runs.
Design ruleThe container boundary is not a security boundary you can buy once. Where the workload is untrusted, put a second boundary underneath it, which is precisely what AWS did by running Fargate on containerd plus Firecracker.
Postmortem

Five years later, the same class through leaked descriptors

AssumptionThe initialisation path hands the container only what it is supposed to have.
What happened"Several file descriptors were inadvertently leaked internally within runc into runc init, including a handle to the host's /sys/fs/cgroup", and runc did not verify that the working directory stayed inside the container's mount namespace.
Blast radiusrunc v1.0.0-rc93 to v1.1.11, patched in v1.1.12 on 31 January 2024. Exploitable from a hostile image or a hostile Dockerfile, which put it inside build systems as well as runtimes.
FixClose descriptors across the boundary and verify the final working directory.
Design ruleEvery process boundary you add is also a descriptor-passing boundary. When you split a component out, enumerate what crosses the fork and exec, because that list is the new attack surface and it is not in the interface definition.
Postmortem

Three escapes published on one day, in 2025

AssumptionMasking a sensitive path with a bind mount of /dev/null hides it.
What happened"When using the container's /dev/null to mask files, runc would not perform sufficient verification that the source of the bind-mount was actually a real /dev/null inode", so an attacker who controls container creation can substitute a symlink and mount host files, or delete the node entirely and bypass masking of /proc paths.
Blast radiusrunc up to 1.2.7, 1.3.2 and 1.4.0-rc.2; CVSS 7.3, one of three High advisories dated 5 November 2025.
FixVerify the mount source, and stop ignoring ENOENT during masking.
Design ruleDefences assembled from filesystem conventions fail on races and on missing files. Treat the presence of a "masked path" list in a runtime configuration as a hint about what an attacker will aim at, not as a control you can rely on.
Source

The component stopped, the consumer did not

AssumptionA widely used tool from a well-funded vendor will be maintained for as long as you need it.
What happenedDocker Machine's upstream repository has no commit after 2 September 2019. GitLab's CI autoscaling depended on it, so GitLab now runs a fork whose tags read v0.16.2-gitlab.9, and in April 2026 its runner was still shipping documentation for "docker machine version 46".
Blast radiusSeven years of maintenance carried by a consumer, for a component it did not choose to own. The same pattern appears in cri-dockerd, where Mirantis carries the shim Kubernetes removed, and its commit count has fallen to 17 so far in 2026.
FixGitLab is wiring Podman integration tests into CI as of September 2026, which is the exit rather than the fix.
Design ruleWhen you depend on a component that is not the vendor's product, price the fork into the decision on day one. The signal to watch is not announcements, it is commits per year: Docker Machine went from 2,135 in 2015 to 21 in 2019 before it stopped.
The class nobody files as an incident

The fifth class is a stall. Between v20.10.0 on 9 December 2020 and v23.0.0 on 2 February 2023 the engine shipped only patch releases, and the one attempt at a feature line in between, tagged v22.06.0-beta.0 on 3 June 2022, has no final release. The published roadmap in ROADMAP.md had its last substantive edit on 28 October 2018, titled "Update roadmap to reflect reality". No postmortem is ever written for this, and for a consumer choosing a dependency it matters more than most outages.

05

Numbers you can plan against

Most of these are durations rather than latencies, because the question a decade of one estate answers is how long things take: how long an interface holds, how long a fix takes to propagate, how long a component keeps moving after its owner stops caring.

Figure 7 · The decade, by dated artefact

Bundle, to 2016Engine peaks at10,133 commitsOrchestration shipsinside the daemon,v1.12Decompose, 2017Repository split intocomponents, AprilOCI specs reach v1.0,Julycontainerd v1.0.0,DecemberEvict and stall, 2018to 2022Kubernetesdeprecates the built-inshim, 2020Registry meteringbegins, 2020A feature line isabandoned at beta,2022Consolidate, 2023 to2026v23.0.0 ends 26months without afeature releasecontainerd becomesthe default imagestore, 2025libnetwork folded backin, 2025Embedded containerdmode, 2026Four phases, dated by tags, commits and advisories
Bundle, to 2016Engine peaks at10,133 commitsOrchestration shipsinside the daemon,v1.12Decompose, 2017Repository split intocomponents, AprilOCI specs reach v1.0,Julycontainerd v1.0.0,DecemberEvict and stall, 2018to 2022Kubernetesdeprecates the built-inshim, 2020Registry meteringbegins, 2020A feature line isabandoned at beta,2022Consolidate, 2023 to2026v23.0.0 ends 26months without afeature releasecontainerd becomesthe default imagestore, 2025libnetwork folded backin, 2025Embedded containerdmode, 2026Four phases, dated by tags, commits and advisories
Every entry is a tag date, a commit date or an advisory publication date, not a press announcement. The 2020 to 2022 band is the one with no feature releases in it.
Diagram source
MetricValueWhereContextAs ofSource
Engine commits, peak year10,133moby/mobyThe bundled design at its most active2016git, counted 2026-09-28
Engine commits, trough year1,065moby/mobyTwo years after the enterprise business was sold2021git
Engine commits, recovery4,410moby/mobyRebuilding the image path on containerd2025git
Runtime commits, decade range1,355–3,152containerdFlat for ten years, peak in the donation year2016–2026git
Orchestrator commits, decay2,601 → 32moby/swarmkitNever cancelled, never archived, still tagged in 20262016 → 2025git
Specification commits, freeze642 → 8OCI image-specThe interface stops moving after v1.02016 → 2020git
Gap between engine feature releases26 monthsmoby/mobyv20.10.0 (2020-12-09) to v23.0.0 (2023-02-02)2023git
Time from first release candidate to 1.05 yearsruncrc1 2016-06-03, v1.0.0 2021-06-22, in production throughout2021git
Time between runtime majors6y 11mcontainerdv1.0.0 2017-12-04 to v2.0.0 2024-11-052024git
Time between image-spec releases6y 7mOCI image-specv1.0.0 2017-07-19 to v1.1.0 2024-02-152024git
Security fix to mainline tagged release6y 2mmoby/mobyCommit 2018-11-26, first contained in v28.0.0-rc.1 2025-02-062025git
Longest open pull request before closure8y 9mmoby/moby #13602Checkpoint and restore, deferred to a component in 20162024GitHub
Registry pull limit, unauthenticated100 / 6hDocker HubPer IPv4 address or IPv6 /64 subnet2026-09Docker docs
Registry pull limit, free account200 / 6hDocker HubUnlimited on paid plans2026-09Docker docs
The abuse that started metering60,000 / 6hDocker HubObserved per-IP pull rate; first limit set at 20,0002020-05roadmap #87
Weekly pulls, one official image21,498,949Docker Hub, nginxWeek of 14–20 September; alpine 25,142,478 in the same week2026-09Docker Hub
Named production adopters26containerdIncludes GKE, EKS, AKS, Fargate, k3s, Kata, Firecracker2026-09ADOPTERS.md
Developer client, free tier ceiling250 staffDocker DesktopAnd under $10M revenue; paid subscription above either2026-09Docker docs
Import path change, lag after rename8 yearsGo moduleRepository renamed 2017; old module deprecated with v29, November 20252026-09pkg.go.dev
Read these carefully

Commit counts are measured, from bare clones taken on 2026-09-28, and they measure activity rather than value: a year of dependency bumps counts the same as a year of design work, which is why the swarmkit line should be read together with the fact that its 2026 commits are largely version bumps. Durations are derived from tag and commit dates, and two of them depend on which tag you call mainline. Pull counts and limits are the vendor's own published figures, not independent measurement. What nobody has published, and what this page therefore cannot tell you, is the money: not what the registry costs to run, not what the donation was worth in adoption, and not how many paid Desktop seats followed the licence change. Every claim in this guide about motive is an inference from artefacts unless a quoted sentence says otherwise.

06

The evidence wall

Every artefact behind this page, graded. There is no engineering-blog, talk or paper tier here, because the hunt could not reach those hosts; what follows is code, decision records, advisories and vendor documentation.

Postmortem Open Container Initiative2019-02

runc advisory: host binary overwrite via /proc/self/exe (CVE-2019-5736)

The first widely exploited escape in this stack. A container with root could overwrite the host runc binary through mishandled file-descriptor semantics, either from a hostile image or through an exec into a container it already controlled.

Carry forwardPut a second isolation boundary under untrusted workloads; the namespace boundary has been escapable in every year since.
https://github.com/advisories/GHSA-gxmr-w5mj-v8hh
Postmortem Open Container Initiative2024-01

runc advisory: container breakouts due to internally leaked fds (CVE-2024-21626)

Descriptors leaked into runc init, including a handle to the host cgroup filesystem, plus a missing check that the working directory stayed inside the mount namespace.

Carry forwardEnumerate what crosses every fork and exec you introduce; it is attack surface the interface definition does not describe.
https://github.com/opencontainers/runc/security/advisories/GHSA-xr7r-f8xq-vfvv
Postmortem Open Container Initiative2025-11

runc advisory: escape via masked path abuse (CVE-2025-31133)

Masking with a bind mount of /dev/null was not verified to be a real /dev/null, and ENOENT was ignored, so masking of sensitive /proc entries could be bypassed. One of three High advisories published on the same day.

Carry forwardTreat a runtime’s masked-path list as a map of what attackers aim at, not as a control.
https://github.com/opencontainers/runc/security/advisories/GHSA-9493-h29p-rfm2
Postmortem Open Container Initiative2021-2026

runc published advisories, index

Ten advisories across five years, clustering in mount and procfs handling. The index is the cheapest way to see that this is a recurring class rather than a run of unrelated bugs.

Carry forwardRead a dependency’s advisory index before its README; the pattern tells you what the design cannot protect.
https://github.com/opencontainers/runc/security/advisories
Postmortem Docker / Moby2024-07

moby advisory: AuthZ zero length regression (CVE-2024-41110)

States in writing that the January 2019 fix “was not carried forward to later major versions, resulting in a regression” across every line from 19.03 to 27.1.0.

Carry forwardA restructure creates branch lines; audit fix containment with git tag --contains, not with release notes.
https://github.com/moby/moby/security/advisories/GHSA-v23v-6jw2-98fq
Source code Docker / Moby2018-11

Commit 2ac8a479c5, authorization plugin fixes

The original fix. git merge-base shows it is not an ancestor of v19.03, v20.10, v23.0 or v27.1.0, and its first containing tag is v28.0.0-rc.1 in February 2025.

Carry forwardThe commit, not the changelog, is the unit of truth about whether a fix is in a release.
https://github.com/moby/moby/commit/2ac8a479c53d9b8e67c55f1e283da9d85d2b3415
Source code Docker / Moby2024-07

Commit bed37b6152, merge from a security fork

The 2024 remediation is a merge whose second parent is the December 2018 commit: the repository itself records the six-year detour.

Carry forwardSecurity forks are branch lines too; give them an owner and a merge deadline.
https://github.com/moby/moby/commit/bed37b6152327ae67f37cebf2690b7d746b99fb6
Source code Docker / Moby2017-04

Pull request #32691: a new upstream project to break up Docker

The restructure argued in public, including the production chain from upstream components to the commercial product, and the objection that dependent projects were given no notice.

Carry forwardAnnounce the import-path plan with the repository plan; the path outlived the rename by eight years.
https://github.com/moby/moby/pull/32691
Source code Docker / Moby2015-2024

Pull request #13602: checkpoint and restore

Deferred in February 2016 to land after the containerd integration, kept open through the whole decomposition, closed unmerged in March 2024 with 320 comments.

Carry forwardDeferring a feature across a boundary drops it unless someone on the other side is accountable.
https://github.com/moby/moby/pull/13602
Source code Docker / Moby2014-2024

Closed, unmerged pull requests by discussion volume

The most-argued proposals in the repository were closed rather than merged, including build-time environment variables (475 comments) and private registry mirrors, open from 2017 to 2024.

Carry forwardRead a project’s closed-unmerged queue to find the boundary its maintainers are defending.
https://github.com/moby/moby/pulls?q=is%3Apr+is%3Aclosed+is%3Aunmerged+sort%3Acomments-desc
Source code Docker / Moby2013-2026

moby/moby repository history

Commits per year from 10,133 in 2016 to 1,065 in 2021 and back to 4,410 in 2025, with 58,195 commits and 72.1k stars at the time of reading.

Carry forwardCommit volume by year is the cheapest health signal for any dependency, and it moves a year before announcements do.
https://github.com/moby/moby
Source code Docker / Moby2015-2025

Engine release tags

v1.12.0 in July 2016, a calendar-version experiment from February 2017, a 26-month gap after v20.10.0, an abandoned v22.06 beta, and a tag prefix change at docker-v29.0.0 in November 2025.

Carry forwardVersion schemes change when the business does; an abandoned beta line is the clearest public sign of a stalled plan.
https://github.com/moby/moby/tags
Source code Docker / Moby2022-07

Commit 7d74269c0d, the containerd image service

The first commit of daemon/containerd, which grew to 282 commits in 2023: the product rebuilding its own storage layer on the component it had donated five years earlier.

Carry forwardAdopting your own donated component is a multi-year programme, not a switch.
https://github.com/moby/moby/commit/7d74269c0dafb71a760d8c669fca3d2df5778d2e
Source code Docker / Moby2026-06

Commit a8a1cfd111, embedded containerd mode

An experimental mode that runs containerd inside the daemon process rather than as a separately managed one: partial re-bundling of the component that was extracted in 2016.

Carry forwardRe-absorption is a legitimate late move, but name the consumers you are willing to lose first.
https://github.com/moby/moby/commit/a8a1cfd1114e7472590206f5ebb8e088beee7a1c
Source code Docker / Moby2025-07

Commit 53bd828853, remove libnetwork

Networking, a separate repository since February 2015, is moved into daemon/libnetwork and the standalone tree is deleted, after its own commit volume had fallen to 18 in 2023.

Carry forwardA component with one consumer is a module, not a product; stop paying the boundary tax.
https://github.com/moby/moby/commit/53bd828853008b3187545849c1f2000f024861d0
Decision record containerd2017-2026

SCOPE.md: an allow-list of responsibilities

Networking, build, logging and volume management are explicitly refused, and anything not listed is out of scope by default.

Carry forwardWrite your component’s refusals before its features; that document is what makes it adoptable by people who do not trust you.
https://raw.githubusercontent.com/containerd/containerd/main/SCOPE.md
Source code containerd2026-09

ADOPTERS.md

Twenty-six named production adopters, including every major managed Kubernetes service, two AWS isolation platforms, k3s, Kata and the Docker engine itself as one entry among them.

Carry forwardThe adopter list is the clearest measure of whether an extraction succeeded.
https://raw.githubusercontent.com/containerd/containerd/main/ADOPTERS.md
Source code containerd2017-2026

Release tags and commit history

v1.0.0 in December 2017, v2.0.0 in November 2024, v2.4.0 in September 2026, with commit volume between 1,355 and 3,152 a year for a decade.

Carry forwardA component that holds flat activity for ten years is a dependency you can plan around; one that spikes and falls is not.
https://github.com/containerd/containerd/tags
Source code Mirantis2022-2026

cri-dockerd

The removed shim as a standalone product, carrying its original history from a 2016 commit inside Kubernetes, maintained by the company that bought Docker’s enterprise business. Commits fell to 17 so far in 2026.

Carry forwardRemoved code does not die if someone has customers; check who picked it up before assuming a migration is forced.
https://github.com/Mirantis/cri-dockerd
Source code Open Container Initiative2017-2025

image-spec and runtime-spec tags

v1.0 in July 2017 for both, then near-silence: 8 commits in 2020 for the image spec, and six years and seven months to v1.1.0.

Carry forwardFreezing an interface is a design act with a cost and a payoff; the payoff is that both sides become replaceable.
https://github.com/opencontainers/image-spec/tags
Decision record Docker2020-05

docker/roadmap issue #87: anti-abuse rate limits

Metering the registry begins as an abuse control at 20,000 pulls per six hours, justified by observed rates above 60,000.

Carry forwardFree infrastructure at internet scale ends as metering; plan a pull-through cache before the limit lands, not after.
https://github.com/docker/roadmap/issues/87
Decision record Docker2020-2024

docker/roadmap issue #7: improve Mac file system performance

The most-demanded item on the public roadmap for four years was the boundary between the developer’s machine and the Linux virtual machine, closed as shipped with Desktop 4.27 in February 2024.

Carry forwardWhen a product loses the server layer, its remaining architecture problem is the client boundary; the roadmap says so before the marketing does.
https://github.com/docker/roadmap/issues/7
Vendor doc Docker2026-09

Docker Hub official image counters

nginx at 21,498,949 pulls in the week of 14 to 20 September 2026 and over a billion in total; alpine at 25,142,478 in the same week.

Carry forwardThe registry kept its position while the runtime did not, which is what makes metering it viable.
https://hub.docker.com/_/nginx
Source code Red Hat / containers2017-2026

podman

A competing implementation with no manager daemon and a compatible command line, 28,467 commits, and roughly 3,000 to 4,400 commits a year through the period when the engine was at its quietest.

Carry forwardThe alternative to a daemon is not a better daemon; check whether your architecture needs the long-running process at all.
https://github.com/containers/podman
Source code abiosoft2021-09

colima

First commit on 4 September 2021, days after the Desktop subscription terms took effect; podman-desktop follows in March 2022.

Carry forwardPricing changes on developer tooling produce replacements within weeks, and their first commits are the timestamp.
https://github.com/abiosoft/colima
Case study Kubernetes2022-11

registry.k8s.io

The project moved its image hosting to a community-controlled domain because of “significant egress traffic costs from users on other cloud providers”, so it could serve AWS users from AWS-local storage.

Carry forwardAt sufficient scale the registry becomes an egress-cost problem, and the fix is topology, not caching policy.
https://github.com/kubernetes/registry.k8s.io
Source code GitLab2019-2026

gitlab-org/ci-cd/docker-machine

A fork carrying nine patch releases beyond the upstream tool’s final version, which has had no commit since September 2019, kept alive for CI autoscaling.

Carry forwardDepending on a vendor’s non-product component means owning a fork; price it on day one.
https://gitlab.com/gitlab-org/ci-cd/docker-machine
Source code GitLab2026-09

gitlab-runner

A decade-old consumer still shipping the forked machine driver in 2026 while wiring Podman integration tests into CI, which is the exit path being built in public.

Carry forwardWatch what large consumers add support for; it is the earliest reliable signal of a platform shift.
https://gitlab.com/gitlab-org/gitlab-runner
Source code Docker / Moby2025-11

Go module deprecation

The module github.com/docker/docker is deprecated from Docker v29 and replaced by github.com/moby/moby/v2, eight years after the repository was renamed.

Carry forwardImport paths are the slowest part of a rename; schedule them with the rename or expect to carry both for years.
https://pkg.go.dev/github.com/moby/moby/v2
Source code Docker / Moby2016-2026

swarmkit

From 2,601 commits in its first year to 32 in 2025, still tagged v2.1.2 in April 2026 and never archived.

Carry forwardA component can be both maintained and finished; distinguish maintenance from investment before you build on it.
https://github.com/moby/swarmkit
Decision record Docker / Moby2018-10

ROADMAP.md and its history

The last substantive edit is titled “Update roadmap to reflect reality” and is dated 28 October 2018; later commits fix a typo and a link.

Carry forwardThe age of a published roadmap is a dependency signal, and it is free to check.
https://github.com/moby/moby/blob/master/ROADMAP.md
Source code Docker / Moby2018-2026

buildkit

The build path as a separate, growing project: the default builder from Engine 23.0, and 1,449 commits in 2024 against the engine’s 2,657.

Carry forwardWhen the runtime commoditises, the differentiated work moves up to the build and down to the kernel.
https://github.com/moby/buildkit
Vendor doc Docker2026-07

Python SDK for the Engine API

Still current at 7.2.0 in July 2026: the client contract is the part of the product that never broke.

Carry forwardThe API you must never break is the one with third-party clients, and it is rarely the one you are rewriting.
https://pypi.org/project/docker/
07

Build a miniature, then productionise it

Two things are worth practising here: running the stack one layer at a time, so the boundaries stop being abstract, and running this excavation on a codebase you own.

Start a container without a daemon

Export a filesystem from an image, write a config.json to the OCI runtime specification, and start it with runc run. No engine, no registry, no network plugin.

Done when: a shell runs in your own bundle and runc list shows it.  Teaches: what the runtime layer actually is, which is a JSON document plus namespaces.

Put containerd under it, then take the daemon away

Pull the same image with ctr, run it, then kill and restart containerd while the container keeps running. Watch the shim process survive.

Done when: the workload outlives a restart of its supervisor.  Teaches: why a shim per container is the mechanism that made runtime upgrades routine.

Swap the implementation on both sides of the interface

Run the same image under a second runtime (crun, or a sandboxed one) and under a second engine such as podman, changing nothing about the image.

Done when: the identical digest runs under two runtimes and two engines.  Teaches: what a frozen specification buys, and where compatibility actually ends.

Audit fix containment in a repository you depend on

Pick a security commit in any dependency and run git tag --contains and git merge-base --is-ancestor against the versions you ship. Then do it for your own repository's last five security fixes.

Done when: you can name, for each fix, the releases that do not contain it.  Teaches: the failure that produced CVE-2024-41110, in your own tree.

Measure your dependencies the way this page measures Docker

Clone each critical dependency with --bare --filter=blob:none and produce commits per year, tag intervals, and the date of the last substantive roadmap edit.

Done when: you have a one-page table and at least one surprise on it.  Teaches: that abandonment is visible years before it is announced.

Write the refusals for one of your own components

Take an internal library other teams depend on and write its SCOPE.md: what it does, and the list of things it will never do. Circulate it and watch which teams object, because their objections are the dependencies you did not know you had.

Done when: the refusal list survives review without being widened.  Teaches: whether the component is extractable at all.

Price the fork

For the dependency with the worst activity trend, estimate what it costs to carry a fork for three years: build, patch, security triage, and the exit. Compare with the cost of the migration you are avoiding.

Done when: the number is written down and someone owns it.  Teaches: the decision GitLab has been making about Docker Machine since 2019.

08

Keep hunting

The queries and commands that produced this page. The repository ones are the durable part: they work on any project, and they do not depend on anyone having written a blog post.

Read a decade out of a repository

  • git clone --bare --filter=blob:none https://github.com/moby/moby.git
  • git log --date=format:%Y --format='%ad' | sort | uniq -c
  • git for-each-ref --sort=creatordate --format='%(creatordate:short) %(refname:short)' refs/tags
  • git log --reverse --date=short --format='%ad %s' -- path/to/component

Find out whether a fix is really in your release

  • git tag --contains <fix-commit>
  • git merge-base --is-ancestor <fix-commit> v27.1.0 && echo yes || echo no
  • git log --grep='Merge commit from fork' --date=short --format='%ad %h %s'
  • git show --stat <merge> # then read the second parent's date

Find the arguments, not the announcements

  • is:pr is:closed is:unmerged sort:comments-desc
  • is:issue sort:reactions-+1-desc label:roadmap
  • path:keps/ "remove" "deprecat" in:file
  • filename:SCOPE.md OR filename:ADOPTERS.md OR filename:ROADMAP.md

Watch a component die

  • git log -1 --date=short --format='%ad %s' # last commit, run on every dependency
  • git log --follow --date=short --format='%ad %s' -- ROADMAP.md
  • git log --grep='archive' -i --date=short --format='%ad %h %s'
  • git ls-remote --tags <vendor fork> | tail # who is patching past the last upstream release
09

The lesson, stated plainly

Interfaces outlive products, and they outlive them because they stop changing. The two artefacts from this decade that are still load-bearing in 2026 are the OCI specifications and the containerd API, and what both have in common is not quality, which is unmeasurable from outside, but stillness: the image specification took 642 commits in 2016, eight in 2020, and six years and seven months to reach its next release. Everything that moved fast around them, the bundled orchestrator, the graph drivers, the built-in shim, the standalone scheduler, either decayed to dependency bumps or was deleted. The product that wrote the specifications is now a peer consumer of them, one line in a file listing twenty-six adopters, and it spent 2022 to 2026 rebuilding its own storage layer on top of the component it had given away.

Three transfers for a reader with an internal platform. First, if you want a component of yours to outlive the product it came from, the test is not whether it is good, it is whether you can write down what it refuses to do and then stop changing the interface; extraction without those two things buys the coordination cost of a boundary and none of the optionality. Second, decomposition relocates defects into the seams, and the most expensive seam in this record was not technical but procedural, a security fix that lived on one branch line for six years because nothing in the process asked which tags contained it. Third, the layer that captures value is not the layer you are proud of: the runtime was donated, the orchestrator was lost, the developer client became a licence question, and the thing that still meters billions of pulls a week is the registry, which was never the interesting part of the architecture.

10

References

Every link was fetched or cloned on 28 September 2026. Repository facts were computed from bare, blobless clones taken the same day.

  1. Open Container Initiative, runc advisory: host binary overwrite via /proc/self/exe (CVE-2019-5736) 2019-02. Checked 2026-09-28.
  2. Open Container Initiative, runc advisory: container breakouts due to internally leaked fds (CVE-2024-21626) 2024-01. Checked 2026-09-28.
  3. Open Container Initiative, runc advisory: escape via masked path abuse (CVE-2025-31133) 2025-11. Checked 2026-09-28.
  4. Open Container Initiative, runc published advisories, index 2021-2026. Checked 2026-09-28.
  5. Docker / Moby, moby advisory: AuthZ zero length regression (CVE-2024-41110) 2024-07. Checked 2026-09-28.
  6. Docker / Moby, Commit 2ac8a479c5, authorization plugin fixes 2018-11. Checked 2026-09-28.
  7. Docker / Moby, Commit bed37b6152, merge from a security fork 2024-07. Checked 2026-09-28.
  8. Docker / Moby, Pull request #32691: a new upstream project to break up Docker 2017-04. Checked 2026-09-28.
  9. Docker / Moby, Pull request #13602: checkpoint and restore 2015-2024. Checked 2026-09-28.
  10. Docker / Moby, Closed, unmerged pull requests by discussion volume 2014-2024. Checked 2026-09-28.
  11. Docker / Moby, moby/moby repository history 2013-2026. Checked 2026-09-28.
  12. Docker / Moby, Engine release tags 2015-2025. Checked 2026-09-28.
  13. Docker / Moby, Commit 7d74269c0d, the containerd image service 2022-07. Checked 2026-09-28.
  14. Docker / Moby, Commit 632de98f75, containerd snapshotters by default 2025-07. Checked 2026-09-28.
  15. Docker / Moby, Commit a8a1cfd111, embedded containerd mode 2026-06. Checked 2026-09-28.
  16. Docker / Moby, Commit 53bd828853, remove libnetwork 2025-07. Checked 2026-09-28.
  17. containerd, SCOPE.md: an allow-list of responsibilities 2017-2026. Checked 2026-09-28.
  18. containerd, ADOPTERS.md 2026-09. Checked 2026-09-28.
  19. containerd, Release tags and commit history 2017-2026. Checked 2026-09-28.
  20. Kubernetes, KEP-2221: removing dockershim from kubelet 2020-12. Checked 2026-09-28.
  21. Mirantis, cri-dockerd 2022-2026. Checked 2026-09-28.
  22. Open Container Initiative, image-spec and runtime-spec tags 2017-2025. Checked 2026-09-28.
  23. Docker, docker/roadmap issue #87: anti-abuse rate limits 2020-05. Checked 2026-09-28.
  24. Docker, docker/roadmap issue #7: improve Mac file system performance 2020-2024. Checked 2026-09-28.
  25. Docker, Docker Hub pull limits 2026-09. Checked 2026-09-28.
  26. Docker, Docker Desktop licence terms 2026-09. Checked 2026-09-28.
  27. Docker, Docker Hub official image counters 2026-09. Checked 2026-09-28.
  28. Red Hat / containers, podman 2017-2026. Checked 2026-09-28.
  29. abiosoft, colima 2021-09. Checked 2026-09-28.
  30. Kubernetes, registry.k8s.io 2022-11. Checked 2026-09-28.
  31. GitLab, gitlab-org/ci-cd/docker-machine 2019-2026. Checked 2026-09-28.
  32. GitLab, gitlab-runner 2026-09. Checked 2026-09-28.
  33. Docker / Moby, Go module deprecation 2025-11. Checked 2026-09-28.
  34. Docker, classicswarm: commit 8653f6a, archive this project 2020-06. Checked 2026-09-28.
  35. Docker / Moby, swarmkit 2016-2026. Checked 2026-09-28.
  36. Docker / Moby, ROADMAP.md and its history 2018-10. Checked 2026-09-28.
  37. Docker / Moby, buildkit 2018-2026. Checked 2026-09-28.
  38. Docker, Python SDK for the Engine API 2026-07. Checked 2026-09-28.