44.6 OCI: The Open Container Initiative Standard
Right, so you’ve got your head around cgroups and namespaces—the raw, kernel-level primitives that let us box processes up. Powerful stuff, but also a bit like being handed a pile of lumber, a box of nails, and a saw. You could build a house with it, but you’d probably rather have a blueprint and some pre-fab walls. That’s where the Open Container Initiative, or OCI, comes in. It’s the blueprint.
In the early, wild-west days of Docker, everyone was building their own container houses with their own weird-shaped windows and doors. This was, as you can imagine, a maintenance nightmare. The OCI is a governance structure formed to create open standards around container runtimes and images. In practice, it gave us two massively important specs: the Runtime Specification (runtime-spec) and the Image Specification (image-spec). These specs are the reason you can build a container with Docker, ship it to a QA team using Podman, and run it in production on a containerd-powered Kubernetes node without losing your mind.
The Image Spec: Your Container’s Boxed-Up Filesystem
Think of the OCI Image Spec as the box your application ships in. It’s not the running container; it’s the tarball-plus-metadata that you pull from a registry. Its job is to define exactly what goes into the filesystem that will become the root of your container. The most important thing it gives us is a predictable format, which is just a JSON file (manifest.json) pointing to a config (config.json) and one or more layer tarballs.
Let’s say you pull an image. You can actually poke around and see this. Using skopeo (a brilliant tool for working with container images without a daemon), you can copy an image and untar it to see the guts.
# Copy the 'hello-world' image from Docker Hub to a tarball directory layout
skopeo copy docker://hello-world:latest dir:./hello-world-oci
# Look at the directory structure it creates
tree ./hello-world-oci
This will show you the manifest.json, a version file, and the blobs directory containing the actual layer tarballs and config file. The config file is a goldmine. It contains everything the runtime needs to know to start the container: the command to run, the environment variables, the working directory, and crucially, which user to run as.
The Runtime Spec: The Blueprint for runc
Now, the OCI Runtime Spec is the actual instruction manual for your container runtime (like runc, crun, or youki) to create a running container. It doesn’t care how the image was built; it just takes a filesystem bundle (which conforms to the image spec) and a config.json file and tells the runtime how to set it up.
This config.json is the heart of the matter. It’s a sprawling JSON file that defines the namespaces to join, the cgroups to create, the capabilities to drop, the syscalls to block (seccomp), the root filesystem to use, and the process to execute. You can generate a default one easily with runc, the reference implementation of the OCI runtime.
# Create a simple root filesystem (using a busybox image for simplicity)
mkdir my-container-rootfs
docker export $(docker create busybox) | tar -C my-container-rootfs -xvf -
# Generate the default config.json for this rootfs
runc spec --rootless
# Peek at the massive generated config.json file
cat config.json | jq . # using jq for readability
This file is verbose for a reason. It’s designed to be explicit. Every mount point, every device node permission, every namespace flag is right there. This explicitness is why you can audit a container’s security posture before it even runs. You can see exactly what privileges it’s requesting.
Why This All Matters: Interoperability and the runc Monoculture
The OCI specs created something desperately needed: a neutral ground. Docker, Google, CoreOS, and others could all agree on the format of the box and the instructions for unpacking it. This allowed the ecosystem to explode.
But let’s be honest about the rough edge: while the spec is standard, the implementation is largely a monoculture. Almost every major container engine—Docker, containerd, Podman, CRI-O—uses runc under the hood to actually spawn the containers. They bundle it up and manage it in different ways, but the core runtime is the same. This is both a blessing and a curse. It creates incredible consistency, but it also means that a vulnerability in runc (see: CVE-2019-5736) is a “break the entire internet” level event. It’s a single point of failure that the OCI spec itself was ironically designed to avoid.
The Pitfall: It’s Just a Spec, Not an Enforcer
Here’s the crucial thing to burn into your brain: the OCI spec defines the interface, not the implementation. It says “thou shalt have a config.json file with a process object containing an args array,” but it doesn’t force you to set a USER in that process object. This is why you’ll see so many containers still running as root inside the container, even if the OCI runtime is perfectly capable of running them as a non-root user. The spec provides the tools for security, but it’s up to you, the person building the image and the config, to use them. Don’t blame the blueprint if the builder decides to ignore the instructions for installing load-bearing walls.
The best practice is to always be explicit. Build your images to run as a non-root user. Define your resource limits in the runtime config. Use the config.json not as a suggestion but as a strict contract for how your container should behave. Because that’s what it is. The OCI specs gave us that contract, and it’s the reason you aren’t manually wiring cgroups and namespaces for every single microservice. You’re welcome.