Skip to main content

Why Docker Moved From LXC to Its Own libcontainer

Early Docker is often remembered as a thin wrapper around LXC, and that is roughly true for the first year or so. When Solomon Hykes and the dotCloud team demonstrated Docker in 2013, the tool leaned on the Linux Containers project to do the heavy lifting of isolating processes. On the surface the demo was magical: a developer wraps an app in a container and it runs anywhere. Underneath, LXC handled the namespaces and cgroups that made the isolation real.

Relying on LXC was a pragmatic choice for a young project with a tiny team and a big idea. Why reimplement kernel plumbing when a mature, battle tested abstraction already sat in most Linux distributions? But LXC was designed as a general purpose system administration tool, with its own command surface, its own lifecycle model, and its own opinionated defaults. Docker wanted something narrower and more portable, a lightweight runtime tuned precisely to the container as an application artifact rather than as a lightweight virtual machine.

The friction became hard to ignore as Docker's ambitions grew. LXC tied Docker to host level setup and made it awkward to run containers in the less predictable environments that users started demanding, from CI runners to minimal cloud hosts. The team began building libcontainer, a native Go library that talks to the kernel directly through the essential building blocks of isolation: namespaces for process, network, and filesystem visibility, plus cgroups for resource limits. Around 2014 Docker migrated its default execution engine away from LXC and onto libcontainer.

That decision is a textbook example of when to stop leveraging an existing dependency and start owning the critical layer. Docker's whole value proposition was a consistent, repeatable way to package and run applications. When the component that determines how a container is created sits outside your control, every behavior difference, every version skew, every platform quirk becomes a support problem you inherit without being able to fix. Owning libcontainer let Docker guarantee identical behavior across the environments its users cared about most.

The knock on effect of this migration echoes into today's ecosystem. libcontainer did not stay a private Docker library for long. It became the foundation of runc, the reference implementation of the OCI runtime specification, and the default runtime used across Docker, Kubernetes, and countless other container platforms. When you see runc named in a runtime error or inspect the containers you run, you are looking at the descendants of that early design decision to bring container creation in house.

For working developers the lesson is directly practical: know which layer is your product and which layer is just plumbing. LXC was never the point of Docker, it was scaffolding. As soon as scaffolding constrained the product, it had to go, even though it was good and proven software. A dependency can be both excellent and wrong for your specific goals, and recognizing that distinction early saves years of fighting upstream behavior you do not control.

The migration also shows how abstraction layers compound over time. Docker built on LXC, then built its own libcontainer, then standardized on runc and the OCI spec. Each step traded short term convenience for long term control and interoperability. That is the arc of most great infrastructure: a series of choices to own the parts that really matter, while still standing on the shoulders of the kernel and the open source ecosystem that made containers possible in the first place.

We use cookies for ads and analytics.