Skip to content

Linux Containers: Isolation Without a Hypervisor

A team that needs to run "the same application, the same way, on every machine" eventually collides with the oldest problem in operations: "it works on my machine." A container solves this by packaging an application together with its exact dependencies — libraries, runtime, configuration — into a single unit that runs identically on a laptop, a CI runner, and a production server, because in every case it is running on top of the same thing: a Linux kernel, sharing that kernel with the host through a set of isolation boundaries the kernel itself provides. Understanding containers starts with understanding that they are not a virtualization technology in the hypervisor sense — they are a particular, disciplined use of process isolation features that have existed in the Linux kernel for years, wrapped in tooling that makes them convenient. This article builds the mental model; the next two articles, Namespaces and cgroups and Docker and Podman in Practice, show the kernel mechanism and the everyday tooling in turn.

What You Will Learn

By the end of this article you will be able to:

  • explain, precisely, what a Linux container is and is not;
  • contrast containers with virtual machines at the architecture level, not just "containers are lighter";
  • describe an OCI image, an OCI runtime, and how the two relate to docker run under the hood;
  • read docker inspect output and connect it back to the isolation mechanisms behind it;
  • reason about when a container is the right tool and when a VM is the safer choice.

A Container Is a Process, Not a Machine

The single most important fact about a Linux container is one most beginners get backward: a container is an ordinary Linux process, running directly on the host kernel, with a restricted view of the system. There is no container-specific execution mode in the kernel, no separate "container kernel." What makes a process look and behave like an isolated container is a combination of two kernel features applied to that one process:

  • namespaces restrict what the process can see — its own process tree, its own network interfaces, its own filesystem mount points, its own hostname;
  • cgroups restrict what resources the process can consume — how much CPU, memory, and I/O bandwidth it is allowed to use.

Everything else — images, registries, docker run, Kubernetes — is tooling built on top of these two kernel primitives. Namespaces and cgroups goes through both in detail with hands-on commands; for this article, the important point is architectural: a container's isolation is a view, enforced by the kernel, not a separate execution environment.

docker run -d --name web nginx:alpine
ps -eo pid,comm | grep nginx
   4821 nginx
   4855 nginx

Those nginx processes are visible in the host's own process table, under the host's own kernel, with an ordinary PID — because that is exactly what they are. Inside the container, however, nginx sees itself as PID 1, alone in its own process namespace, unaware that thousands of other processes exist on the same machine.

Containers vs. Virtual Machines: Where the Boundary Actually Sits

A virtual machine and a container solve overlapping problems — running an isolated workload — through fundamentally different architectures:

Virtual machine stack:
  Hardware -> Hypervisor -> Guest kernel (full OS) -> Guest processes

Container stack:
  Hardware -> Host kernel -> Namespaced/cgrouped process (the "container")

A VM's isolation boundary is the hypervisor, emulating (or passing through) hardware to a completely separate guest kernel — the guest can be a different operating system entirely, with its own kernel, its own drivers, its own boot process. A container's isolation boundary is a set of kernel data structures inside the single kernel every container on that host shares — there is no guest kernel, no boot process, and no possibility of running a Windows container on a Linux host without some form of VM underneath it.

Virtual machine Container
Isolation boundary Hypervisor + guest kernel Namespaces + cgroups in one shared kernel
Startup time Seconds to tens of seconds (kernel boot) Milliseconds to low seconds (process start)
Kernel One per VM, can differ from the host Shared with the host, always
Overhead per instance Full guest OS memory and CPU footprint Only the process's own footprint
Cross-OS workloads Yes (Linux host can run a Windows guest) No (a Linux container needs a Linux-compatible kernel)
Attack surface if compromised Guest kernel; escaping to the host requires a hypervisor bug Host kernel is shared; a kernel bug can affect isolation directly

The security line in the last row deserves emphasis, and it is a common interview trap: because every container on a host shares one kernel, a kernel vulnerability that breaks namespace or cgroup isolation is a much more direct path to compromising the whole host than an equivalent bug in a hypervisor, which has to break out of hardware-level virtualization first. This is why running genuinely untrusted, multi-tenant workloads on bare containers — without a VM, a gVisor-style sandboxed runtime, or Kata Containers providing an extra layer — is a real production risk, not a theoretical one.

Interview angle: "are containers as secure as VMs?"

The honest answer is no, not by default, and explaining why demonstrates real understanding rather than marketing repetition. A VM's isolation is enforced by hardware-assisted virtualization and a separate kernel; a container's isolation is enforced entirely in software, by one shared kernel correctly interpreting namespace and cgroup boundaries for every process on the box. A kernel-level container-escape vulnerability (several have been disclosed over the years) can let a process inside a container reach the host directly. In practice this is mitigated with rootless containers (Docker and Podman in Practice covers Podman's rootless model), read-only filesystems, dropped Linux capabilities, and — for genuinely untrusted workloads — running containers inside a VM boundary anyway (the common cloud pattern: a VM per tenant, containers inside it per application).

Images, Containers, and the OCI Specification

Two words get used almost interchangeably in casual conversation but mean distinct things:

  • An image is a read-only, layered filesystem snapshot plus metadata (the default command to run, environment variables, exposed ports) — it is data, sitting on disk or in a registry, doing nothing on its own.
  • A container is a running (or stopped) instance of an image — a writable layer added on top of the image's read-only layers, plus the namespace and cgroup configuration that isolates the process running inside it.
docker images
REPOSITORY   TAG      IMAGE ID       SIZE
nginx        alpine   d1c2c9c3f7a1   45.2MB
docker ps -a
CONTAINER ID   IMAGE          STATUS                   NAMES
7f3a9b2e1c44   nginx:alpine   Exited (0) 2 minutes ago  web

Multiple containers can be created from the same image simultaneously, each with its own independent writable layer — exactly the same relationship a running process has to the executable file on disk it was started from.

This layering, and the format both Docker and Podman use to build and run it, is standardized by the Open Container Initiative (OCI), a project under the Linux Foundation. The OCI defines two specifications that matter in practice:

  • the image spec — how an image's layers, manifest, and configuration are structured and stored;
  • the runtime spec — how a compliant runtime must take an image's unpacked filesystem and a configuration file, then create the actual namespaced, cgrouped process.

runc, the reference implementation of the OCI runtime spec, is the low-level tool that Docker, Podman, and containerd all eventually call to do the actual unshare/clone-based process creation described in Namespaces and cgroups. The practical benefit of this standardization is portability: an image built with docker build can be run by Podman, by containerd (which is what actually runs containers under Kubernetes), or by any other OCI-compliant runtime, without rebuilding it.

docker build ──> OCI image ──> containerd/CRI-O ──> runc ──> namespaced process
                     ^
                     └── same image, any OCI-compliant runtime

Reading docker inspect

docker inspect prints the full configuration and live state of a container as JSON — and every field in it maps back to one of the isolation mechanisms above. It is the single most useful command for confirming what a container actually is rather than what you assumed it to be:

docker run -d --name web nginx:alpine
docker inspect -f '{{.State.Pid}} {{.State.Status}} {{.HostConfig.NetworkMode}}' web
48213 running default
  • .State.Pid (48213) is the container's main process as an ordinary host PID — the same number ps -eo pid,comm shows on the host, and the entry point into /proc/48213/ns/ where every namespace the process belongs to is listed (walked through in Namespaces and cgroups). A .State.Pid of 0 means the container is not running — there is no process to isolate.
  • .State.Status (running) distinguishes a live container from exited, paused, or restarting; an exited status here with a non-zero .State.ExitCode is the first thing to read when a container refuses to stay up.
  • .HostConfig.NetworkMode (default) confirms the container has its own network namespace wired to the default bridge; a value of host would mean the container shares the host's network namespace and has no network isolation at all — a common and easily-missed misconfiguration.

The connection to make in an interview: docker inspect is not showing you a virtual machine's properties — it is showing you the PID, status, and namespace/cgroup configuration of one ordinary host process, which is exactly what a container is.

Why Containers, and When Not To

Containers solve three problems at once, which is why they spread so quickly through both development and operations:

  1. Reproducibility. An image pins exact library versions, so "works on my machine" becomes "works in this image," verifiable by anyone who runs the same image.
  2. Density. Because there is no guest kernel to boot and no full OS memory footprint per instance, a single host can run far more containers than VMs, for workloads with similar resource needs.
  3. Fast, disposable lifecycle. A container starts in the time it takes a process to start — not a kernel to boot — which is what makes rapid scaling, frequent redeployment, and CI pipelines that build-and-test-in-a-container practical at the speed modern development expects.

Containers are the wrong tool when a workload genuinely needs kernel-level isolation from other tenants it does not trust (multi-tenant SaaS running arbitrary customer code is the classic case — most such platforms still put a VM boundary around groups of containers), when the workload needs a different kernel or OS than the host provides, or when the application is a single long-lived stateful system (a large on-premises database with strict I/O and NUMA tuning requirements) where the operational simplicity of a dedicated VM or bare-metal host outweighs container density.

Common Mistakes

  • Treating "container" and "virtual machine" as differing only in size. As shown above, the difference is architectural — a shared kernel versus a separate one — with direct security and portability consequences, not just a performance number.
  • Assuming container isolation is equivalent to VM isolation for untrusted code. A container is a strong default boundary for your own trusted workloads; it is not, by itself, a sandbox suitable for running arbitrary untrusted code from third parties.
  • Confusing an image with a container. Deleting an image that a stopped container still references, or expecting a container's writable-layer changes to appear in the image it was built from, both come from not distinguishing the two.
  • Assuming every container runtime is Docker. Kubernetes has not used the Docker daemon directly as its default runtime since v1.24 (it uses containerd or CRI-O through the CRI); the images themselves remain OCI-compatible and portable regardless of which runtime executes them.

Exercises

  1. Run docker run -d --name web nginx:alpine, then find the corresponding nginx process IDs with ps -eo pid,ppid,comm on the host. Explain, in your own words, why those PIDs are ordinary host PIDs even though nginx inside the container believes it is PID 1.
  2. List two properties of a virtual machine's isolation model that make it a better fit than a plain container for running code from a customer you do not trust.
  3. Pull the same image with docker pull nginx:alpine and (if Podman is installed) podman pull nginx:alpine, then compare docker images and podman images output. Explain what the fact that both can read the same image confirms about the OCI image format.

Summary

A Linux container is an ordinary process, isolated from the rest of the system by namespaces (what it can see) and constrained by cgroups (what it can consume) — not a separate virtual machine with its own kernel. Images are the read-only, layered, portable artifact defined by the OCI image spec; containers are running instances of those images, created by an OCI-compliant runtime like runc. This distinction — shared kernel, software-enforced isolation — is exactly what makes containers fast and dense, and exactly what makes them a weaker security boundary than a VM for genuinely untrusted workloads. Namespaces and cgroups opens up the two kernel mechanisms behind all of this in hands-on detail.

References