Containers have become the foundational building block of modern infrastructure—from local development setups to sprawling Kubernetes clusters running tens of thousands of nodes. Yet, many engineers still think of containers as “miniature virtual machines” or treat tools like Docker as a monolithic black box.
In reality, a container is simply a standard Linux process wearing a pair of isolation glasses provided by the Linux kernel. To make these isolated processes portable across different cloud providers, operating systems, and toolchains, the industry unified under the Open Container Initiative (OCI).
In this article, we will unpack what a container actually is from the operating system’s perspective and explore how the three core OCI specifications ensure seamless interoperability across the entire ecosystem.
1. What Is a Container?
At its core:
A container is an isolated, resource-limited execution environment for running one or more Linux processes, packaged with everything those processes need: application code, runtime, system libraries, and configuration files.
Unlike a virtual machine, a container does not boot an operating system, does not run its own kernel, and does not emulate virtual hardware. All containers on a host share the exact same host Linux kernel.
graph TB
subgraph VM["Virtual Machine Architecture"]
AppVM["Application"] --> GuestOS["Guest OS (Kernel + Drivers)"]
GuestOS --> Hypervisor["Hypervisor (KVM / QEMU / VMware)"]
Hypervisor --> HostKernelVM["Host OS & Hardware"]
end
subgraph ContainerArch["Container Architecture"]
AppC1["App A (PID 1)"] --> Isolation["Namespaces & cgroups"]
AppC2["App B (PID 1)"] --> Isolation
Isolation --> HostKernel["Shared Host Linux Kernel"]
HostKernel --> Hardware["Physical Hardware"]
endFrom the Kernel’s Perspective: Four Pillars of Isolation
To the Linux kernel, there is no single kernel object called a “container”. A container is created by combining four native kernel mechanisms:
graph TD
subgraph ContainerBoundary["Containerized Process (e.g. Nginx)"]
Process["Container Process (PID 1)"]
end
NS["1. Linux Namespaces<br/>(What the process can SEE)"] --> ContainerBoundary
CG["2. Control Groups (cgroups)<br/>(How much resource it can USE)"] --> ContainerBoundary
FS["3. Layered Filesystem (pivot_root)<br/>(What filesystem it can ACCESS)"] --> ContainerBoundary
SEC["4. Security & Capabilities<br/>(What syscalls it can EXECUTE)"] --> ContainerBoundary1. Namespaces (What the Process Can See)
Linux namespaces partition kernel resources so that a process sees an isolated view of the system:
- PID Namespace (
CLONE_NEWPID): The container process sees itself asPID 1inside its namespace, even though it has a normal PID (e.g.,PID 14285) on the host. - NET Namespace (
CLONE_NEWNET): Gives the container its own independent network stack, including private interfaces (eth0,lo), IP addresses, routing tables, and firewall rules. - MNT Namespace (
CLONE_NEWNS): Isolates the filesystem mount points. Changes to mounts inside the container do not affect the host or other containers. - UTS Namespace (
CLONE_NEWUTS): Allows the container to have its own hostname and domain name without modifying the host’s hostname. - IPC Namespace (
CLONE_NEWIPC): Isolates System V IPC and POSIX message queues, preventing inter-process communication across container boundaries. - USER Namespace (
CLONE_NEWUSER): Maps UID/GID inside the container to different UID/GID on the host (e.g. rootUID 0in the container maps to unprivilegedUID 10000on the host).
# View the namespace IDs of your current processls -l /proc/$$/ns/2. Control Groups (cgroups) (What the Process Can Use)
While namespaces prevent processes from seeing each other, cgroups prevent a rogue process from starving the host of resources (noisy neighbor problem).
Configured through the virtual filesystem under /sys/fs/cgroup/, cgroups enforce:
- CPU: Quotas and CPU shares (e.g., limit to 1.5 cores).
- Memory: Hard and soft memory limits with Out-Of-Memory (OOM) killer controls.
- Block I/O: Read/write throttling for storage devices.
- PIDs: Maximum number of processes to prevent fork bombs.
3. Root Filesystem & pivot_root
A container does not see the host’s root directory (/). Instead, the container runtime sets up an isolated directory tree—usually created by stacking read-only image layers and a writable layer via OverlayFS—and calls pivot_root() (or chroot()) to switch the process’s apparent root directory to this tree.
4. Security Boundaries
To restrict access even further, containers enforce defense-in-depth security:
- Linux Capabilities: Linux splits traditional
rootpowers into distinct flags (e.g.,CAP_NET_BIND_SERVICE,CAP_SYS_ADMIN,CAP_NET_RAW). Containers drop most dangerous capabilities by default. - seccomp (Secure Computing Mode): Filters and blocks dangerous system calls (e.g.,
reboot(),keyctl()). - AppArmor / SELinux: Mandatory Access Control (MAC) policies restricting file, network, and device access.
Containers vs. Virtual Machines
| Attribute | Virtual Machine | Container |
|---|---|---|
| Virtualization Level | Hardware / Hypervisor | Operating System Kernel |
| Startup Time | Minutes / Tens of seconds | Milliseconds |
| Resource Overhead | High (each VM runs a full OS kernel) | Near zero (native OS process) |
| Portability | Heavy VM images (GBs) | Layered OCI images (MBs) |
| Isolation Strength | Hardware-enforced (strong) | Kernel-enforced (software boundaries) |
2. Why Did We Need Standards? The Birth of OCI
In 2013, Docker popularized containerization by combining these kernel primitives into an intuitive developer workflow: write a Dockerfile, run docker build, and ship an image.
However, as containers exploded in popularity, two major risks emerged:
- Fragmentation: Competing vendors began creating incompatible container formats (e.g., CoreOS developed
rkt, while Docker had its proprietary image format). - Vendor Lock-in: The entire ecosystem risked depending on a single commercial entity’s runtime and registry protocols.
To solve this, in 2015 Docker, Red Hat, Google, Microsoft, and other industry leaders formed the Open Container Initiative (OCI) under the governance of the Linux Foundation.
3. The Three Core OCI Specifications
The OCI governs three distinct, interlocking specifications that cover the entire lifecycle of a container:
flowchart LR
subgraph BuildTime["1. Packaging"]
Builder["Build Tool<br/>(Docker, Buildah, Kaniko)"] -->|Produces| ImageSpec["OCI Image Spec<br/>(layers + config + manifest)"]
end
subgraph DistributeTime["2. Distribution"]
ImageSpec -->|Pushed via| DistSpec["OCI Distribution Spec<br/>(Registry HTTP API)"]
DistSpec --> Registry[("Container Registry<br/>(GHCR, Docker Hub, Harbor)")]
end
subgraph RunTime["3. Execution"]
Registry -->|Pulled & Unpacked to| RuntimeSpec["OCI Runtime Spec<br/>(rootfs/ + config.json bundle)"]
RuntimeSpec --> Runtime["OCI Runtime<br/>(runc, crun, gVisor)"]
Runtime --> LinuxKernel["Linux Kernel Process"]
end1. OCI Image Specification
The OCI Image Specification standardizes how an image is represented on disk and in registries.
An OCI image is not a single binary; it is a collection of content-addressed cryptographic blobs tied together by JSON documents:
- Layers (
tar.gz/tar.zstd): Filesystem diffs created during build steps. Each layer is identified by its SHA-256 hash. - Configuration (
config.json): Metadata including environment variables, working directory, entrypoint, default arguments, architecture (amd64,arm64), and OS. - Manifest (
manifest.json): The primary index document referencing the configuration digest and each layer digest in sequential order. - Index / Multi-Arch Manifest (
index.json): A manifest list pointing to different platform-specific manifests (e.g.linux/amd64vslinux/arm64).
{ "schemaVersion": 2, "mediaType": "application/vnd.oci.image.manifest.v1+json", "config": { "mediaType": "application/vnd.oci.image.config.v1+json", "digest": "sha256:9b1a03f833...b71c8", "size": 1472 }, "layers": [ { "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", "digest": "sha256:a486411936...780ec", "size": 3401568 }, { "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", "digest": "sha256:8df5124018...f9c21", "size": 12845 } ]}2. OCI Runtime Specification
The OCI Runtime Specification defines how an unpacked container image must be executed on a host.
It completely separates storage from execution. The runtime does not download images or understand registries; it only consumes an OCI Bundle:
my-container-bundle/├── config.json # The declarative execution contract└── rootfs/ # The unpacked filesystem tree ├── bin ├── etc ├── usr └── varThe runtime specification details standard lifecycle operations that every runtime must implement:
create: Prepares the container environment (namespaces, cgroups) without starting the main process.start: Starts the user-defined process inside the container.kill: Sends a signal (e.g.,SIGTERMorSIGKILL) to the container.delete: Cleans up container resources and cgroups.state: Queries the container’s status (creating,running,stopped).
The Reference Runtime: runc
Docker extracted its core execution engine (libcontainer) and donated it to OCI as the official reference implementation: runc. Today, higher-level engines like containerd, CRI-O, and Podman all delegate to runc (or compatible alternatives like crun) to spawn containers.
3. OCI Distribution Specification
The OCI Distribution Specification standardizes the HTTP REST API that container registries implement.
Before this specification, pushing and pulling images was tied to Docker’s proprietary registry API v2. With the OCI distribution spec:
- Any build tool (
docker,podman,buildah,kaniko,ko) can push to any compliant registry. - Any registry (Docker Hub, GitHub Packages / GHCR, Quay, Harbor, AWS ECR, Google Artifact Registry) can host images interchangeably.
- Non-container artifacts (Helm charts, WebAssembly modules, Cosign signatures, OPA policies) can be packaged and distributed using the exact same registry API (often called ORAS - OCI Registry As Storage).
4. How Everything Connects: The Full OCI Lifecycle
Let’s walk through an end-to-end example showing how different tools collaborate seamlessly thanks to OCI standards:
sequenceDiagram
autonumber
actor Dev as Engineer
participant Builder as Build Tool (Buildah / Kaniko)
participant Reg as OCI Registry (GHCR / Harbor)
participant Runtime as High-Level Runtime (containerd)
participant LowLevel as OCI Runtime (runc)
participant Kernel as Linux Kernel
Dev->>Builder: Build application container
Note over Builder: Compiles layers & outputs OCI Image Spec
Builder->>Reg: Push manifest & layers (OCI Distribution Spec)
Note over Runtime: Kubernetes schedules pod on node
Runtime->>Reg: Pull manifest & layers (OCI Distribution Spec)
Runtime->>Runtime: Unpack layers into OverlayFS
Runtime->>Runtime: Generate OCI Bundle (rootfs/ + config.json)
Runtime->>LowLevel: Execute runc create (OCI Runtime Spec)
LowLevel->>Kernel: clone() with namespaces, apply cgroups, pivot_root()
LowLevel->>Kernel: execve() application binary (PID 1)
LowLevel-->>Runtime: Process running; runc exitsThe Power of Mix-and-Match
Because each step adheres to an open specification, you can swap any component without breaking the pipeline:
| Phase | OCI Specification | Interchangeable Tools |
|---|---|---|
| Build | OCI Image Spec | docker build, buildah, kaniko, ko, pack (Cloud Native Buildpacks) |
| Registry | OCI Distribution Spec | GitHub Packages, Docker Hub, Harbor, AWS ECR, Azure ACR |
| Host Orchestration | CRI / High-level daemon | containerd, CRI-O, podman |
| Execution | OCI Runtime Spec | runc (standard), crun (fast C), gVisor (secure sandbox), Kata (VM-isolated) |
Key Takeaways
- A container is not a VM: It is a regular Linux process running with isolated views (namespaces), resource quotas (cgroups), an isolated root filesystem (
pivot_root), and restricted privileges (capabilities & seccomp). - OCI provides the common language: The Open Container Initiative guarantees that container tools from different companies can interoperate cleanly without vendor lock-in.
- The 3 OCI Specs:
- Image Spec: Defines layers, manifests, and image configuration.
- Runtime Spec: Defines the bundle (
rootfs/+config.json) and lifecycle operations (create,start,kill,delete). - Distribution Spec: Defines the API for pushing, pulling, and discovering container images across registries.
- Modularity drives innovation: Because of OCI, you can run untrusted code inside sandbox runtimes like
gVisoror micro-VMs likeKata Containersusing the exact same Dockerfile and Kubernetes manifests.
This article is part of the Container Internals series. Next up: containerd and OCI Runtimes: What Runs Your Containers?.