What Are Containers?
What a container is and how it differs from a virtual machine: images and containers, Linux namespaces and cgroups, the OCI standards, and the tools around them such as Docker, Podman and containerd.
About this topic
The Cloud topic introduces containers and Kubernetes in two overview pages: Containers (images, a Dockerfile line by line, registries, containers versus virtual machines) and Kubernetes Basics (why orchestration, pods, deployments, services). This topic goes deeper, for the people who run, support and troubleshoot containers day to day. It does not repeat those pages; where a term is already explained there, it links to it.
The topic has two halves:
- Containers on one host (pages 1–10): this page, Images, Layers and Registries, Running Containers, Writing Dockerfiles, Building Good Images, then storage, networking, Docker Compose, Container Security and Debugging Containers.
- Kubernetes (pages 11–19): from How a Kubernetes Cluster Works to kubectl and Troubleshooting and Helm, Kustomize and GitOps.
The examples follow Kestrel Energy, a fictional Dutch energy
supplier, with four workloads: the customer portal,
a meter-api that serves meter readings, a
PostgreSQL database and a nightly billing-export
job. Their images live in a private registry, for example
registry.example.com/kestrel/portal:1.4.2.
About the command output
Output labeled Recorded output · Docker 28.2, arm64 was captured for real on a test machine, a Raspberry Pi 5 (arm64 processor, Debian 12) running Docker Engine 28.2.1. Output labeled illustrative was written from the official documentation, because it could not be recorded on that machine. Two things about the recordings are worth knowing:
-
Docker 29 looks different in places. As of
October 2026 the current Docker Engine is 29.x (29.8.2). Version
29 shows
docker image lsas a new, collapsed tree view instead of the classic table; uses the containerd image store by default on fresh installations (upgraded systems keep the classic overlay2 storage); removed Docker Content Trust (DOCKER_CONTENT_TRUST) from the command-line tool; and deprecated cgroup v1. Commands, error messages and exit codes on these pages are the same on both versions unless a page says otherwise. - No memory limits on that machine. Its kernel was started with the memory controller of cgroups switched off, so Docker could not enforce memory limits there and a Kubernetes test cluster could not start. Where a page needs that behavior, its output is illustrative and says so.
Your first container
The classic first command is docker run hello-world.
The image was not on the machine yet, so Docker downloaded it from
Docker Hub, created a container from it, started it and showed its
output:
$ docker run --name hello hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
58dee6a49ef1: Pulling fs layer
58dee6a49ef1: Verifying Checksum
58dee6a49ef1: Download complete
58dee6a49ef1: Pull complete
Digest: sha256:5e23090353324d887c48ad5e5c56d294eab81588df9605b07d1afe895f9cc8f8
Status: Downloaded newer image for hello-world:latest
Hello from Docker!
This message shows that your installation appears to be working correctly.
To generate this message, Docker took the following steps:
1. The Docker client contacted the Docker daemon.
2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
(arm64v8)
3. The Docker daemon created a new container from that image which runs the
executable that produces the output you are currently reading.
4. The Docker daemon streamed that output to the Docker client, which sent it
to your terminal.
…
The four numbered steps are a summary of this whole page: a client
talks to a daemon, the daemon gets an image from a registry and
turns it into a running process. Note (arm64v8): the
image exists for several processor architectures, and Docker picked
the one that matches the Pi (see
multi-platform images).
hello-world prints its text and exits, so the container is no longer
running. docker ps lists only running containers;
docker ps -a also shows stopped ones:
$ docker ps --filter name=hello
$ docker ps -a --filter name=hello
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES b86c2055bdd6 hello-world "/hello" Less than a second ago Exited (0) Less than a second ago hello2 187db6bfc581 hello-world "/hello" 1 second ago Exited (0) Less than a second ago hello
The first list is empty. The second shows two stopped containers
(the command was run twice), both with exit code 0. A stopped
container still exists, with its settings and any files it wrote,
until you remove it with docker rm. That life cycle is
the subject of
Running Containers.
A container is a process
On the
Cloud page, a
container
is described as an isolated process that shares the host's
kernel. You can see that literally. This nginx web server runs in a
container called web. Asked from inside, its process
list contains only nginx itself, and the main process has
process ID
(PID) 1:
$ docker exec web ps
PID USER TIME COMMAND
1 root 0:00 nginx: master process nginx -g daemon off;
30 nginx 0:00 nginx: worker process
31 nginx 0:00 nginx: worker process
32 nginx 0:00 nginx: worker process
33 nginx 0:00 nginx: worker process
46 root 0:00 ps
Asked from the host, the same process has a very different PID:
$ docker inspect --format '{{.State.Status}} pid={{.State.Pid}} started={{.State.StartedAt}}' web
running pid=2406295 started=2026-10-06T12:18:36.263421317Z
PID 1 inside, PID 2406295 outside: one process, two views. On the
host, ps or top shows the nginx processes
among all the others, and a host administrator can see, signal and
limit them like any other process. There is no virtual hardware and
no second operating system, which is why a container starts in a
fraction of a second: starting it means starting a process.
Two kernel features create the separate view and the limits. They are described in the next two sections.
Linux namespaces: what a process can see
A Linux namespace wraps one kind of global system resource so that the processes inside it get their own, separate copy of it. A process in a new PID namespace sees its own process numbers starting at 1; a process in a new network namespace has its own network interfaces, IP addresses, routing table and ports. Linux has eight kinds:
| Namespace | Isolates | Effect in a container |
|---|---|---|
| PID | Process IDs | The container sees only its own processes; its main process is PID 1. |
| Network (net) | Interfaces, addresses, routes, ports | Own IP address; port 8080 in one container does not clash with port 8080 in another. |
| Mount (mnt) | The tree of mounted file systems |
The image's files appear as /; host directories
are visible only when mounted in.
|
| UTS (UNIX Time-Sharing) | Host name and domain name | The container has its own host name, by default the short container ID. |
| IPC (inter-process communication) | Shared memory, message queues | Containers cannot reach each other's shared memory. |
| User | User and group IDs | Can map root in the container to an unprivileged user on the host. Not used by Docker by default. |
| Cgroup | The view of the cgroup tree | The container sees its own cgroup as the root. |
| Time | Some system clocks | Rarely used; containers normally share the host's clocks. |
Every process has a link per namespace in
/proc/<pid>/ns/, such as
pid:[4026531836]. Two processes with the same number
share that namespace. This script compares sample values for a shell
on the host and the meter-api container's main process:
# Namespace links as `ls -l /proc/<pid>/ns` shows them (sample values).
host_shell = {
"cgroup": "cgroup:[4026531835]", "ipc": "ipc:[4026531839]",
"mnt": "mnt:[4026531841]", "net": "net:[4026531840]",
"pid": "pid:[4026531836]", "time": "time:[4026531834]",
"user": "user:[4026531837]", "uts": "uts:[4026531838]",
}
meter_api = {
"cgroup": "cgroup:[4026532412]", "ipc": "ipc:[4026532410]",
"mnt": "mnt:[4026532408]", "net": "net:[4026532413]",
"pid": "pid:[4026532411]", "time": "time:[4026531834]",
"user": "user:[4026531837]", "uts": "uts:[4026532409]",
}
for kind in sorted(host_shell):
same = host_shell[kind] == meter_api[kind]
print(f"{kind:7} {'shared with the host' if same else 'own namespace'}")
cgroup own namespace ipc own namespace mnt own namespace net own namespace pid own namespace time shared with the host user shared with the host uts own namespace
That is the picture for a default Docker container: six namespaces of its own, but the same user namespace as the host. So user ID 0 (root) inside the container is user ID 0 on the host as far as the kernel is concerned. That is why running as a non-root user matters, and why Container Security covers rootless mode and user namespace remapping.
Cgroups: what a process may use
Namespaces hide things; they do not limit anything. A process in its
own namespaces can still use every CPU core and all the memory of
the host. Cgroups (control groups)
are the kernel feature that groups processes and accounts for and
limits the resources the group uses: CPU time, memory, block I/O and
the number of processes. Each container gets its own cgroup, and
options such as --memory 256m and
--cpus 0.5 become settings in it (see
resource limits). When a container exceeds its memory limit, the kernel's
out-of-memory killer ends a process in that cgroup, not a random
process elsewhere on the host.
There are two generations. Cgroup v1 had a separate hierarchy per resource type; cgroup v2 has one unified hierarchy and is what current distributions use. The ecosystem is leaving v1 behind (as of October 2026):
- Kubernetes deprecated cgroup v1 in version 1.35; by default the kubelet no longer starts on a cgroup v1 node.
- Docker Engine 29.0 deprecated cgroup v1; Docker says support continues until at least May 2029.
- Podman 6.0 (June 2026) removed cgroup v1 support, according to its release announcement.
Docker reports which version a host uses:
$ docker info --format '{{.ServerVersion}} {{.OperatingSystem}} {{.Architecture}} {{.Driver}} cgroup{{.CgroupVersion}} CPUs={{.NCPU}} Mem={{.MemTotal}}'
28.2.1 Debian GNU/Linux 12 (bookworm) aarch64 overlay2 cgroup2 CPUs=4 Mem=17007542272
Cgroup v2, overlay2 storage, 4 CPU cores and 17,007,542,272 bytes
(15.8 GiB) of memory. Inside a process, the file
/proc/self/cgroup tells the same story: on a pure
cgroup v2 system it contains a single line that starts with
0::. A small check, with sample file contents:
def cgroup_version(text):
"""Reads the contents of /proc/<pid>/cgroup and names the cgroup version."""
lines = [line for line in text.splitlines() if line.strip()]
unified = [line for line in lines if line.startswith("0::")]
if unified and len(lines) == 1:
return "cgroup v2 (unified hierarchy)"
if unified:
return "hybrid: v1 controllers plus a v2 hierarchy"
return "cgroup v1 (one hierarchy per controller)"
samples = {
"host shell, Debian 12": "0::/user.slice/user-1000.slice/session-3.scope\n",
"inside a container": "0::/\n",
"older host": (
"12:pids:/docker/3f9c2a7b\n"
"11:memory:/docker/3f9c2a7b\n"
"4:cpu,cpuacct:/docker/3f9c2a7b\n"
"1:name=systemd:/docker/3f9c2a7b\n"
),
}
for where, text in samples.items():
print(f"{where:22} -> {cgroup_version(text)}")
host shell, Debian 12 -> cgroup v2 (unified hierarchy) inside a container -> cgroup v2 (unified hierarchy) older host -> cgroup v1 (one hierarchy per controller)
Inside the container the path is just /: the cgroup
namespace makes the container's own cgroup look like the root.
Containers and virtual machines
The Cloud page draws the two stacks side by side: every virtual machine runs its own operating system on virtual hardware provided by a hypervisor; containers share the host's kernel. For the Kestrel meter-api, the practical difference looks like this (typical orders of magnitude, not measurements):
| meter-api as … | Virtual machine | Container |
|---|---|---|
| What you ship | A disk image with Debian, Python and the app | An image with Python and the app, no kernel |
| Size | A few gigabytes | About 150 MB |
| Start time | Tens of seconds (an operating system boots) | Under a second (a process starts) |
| Patching | Patch the guest OS in place | Rebuild the image, replace the container |
| Isolation boundary | Hypervisor and virtual hardware | The shared kernel |
| Runs on | Any OS the hypervisor supports | A host with a compatible kernel and processor architecture |
The last two rows matter most for operations. A Linux container needs a Linux kernel, which is why Docker Desktop on Windows and macOS runs a small Linux virtual machine in the background. And because every container on a host talks to the same kernel, a flaw in that kernel can affect all of them at once. The United States National Institute of Standards and Technology (NIST) describes containers in its Application Container Security Guide (SP 800-190, 2017) as "a form of operating system virtualization combined with application software packaging". Both halves of that sentence matter.
In practice the two are combined: most containers in the cloud run on virtual machines. The virtual machine gives a strong boundary between customers or environments; containers give fast, uniform packaging inside it.
The OCI standards
Docker made containers popular in 2013. To keep the format from belonging to one company, Docker, CoreOS and others founded the Open Container Initiative (OCI) under the Linux Foundation in June 2015. It maintains three specifications:
| Specification | Defines | Version (as of October 2026) |
|---|---|---|
| Image spec | What an image consists of: an index, manifests, a configuration and layers | v1.1.1 |
| Runtime spec | How to run a container from an unpacked file system and a configuration file | v1.3.0 |
| Distribution spec | The HTTP API that registries offer for push and pull | v1.1.1 |
The reference implementation of the runtime spec is runc, which Docker donated to the OCI. Because the standards exist, the tools are interchangeable at the edges: an image built with Docker runs under Podman, a Kubernetes node or a cloud container service, and any OCI registry can store it.
Engines, runtimes and the Docker stack
"Docker" is a brand, a company and a stack of programs. Two general terms help to take the stack apart:
- A container runtime is the software that actually creates and runs containers on a host. The low-level runtime (runc, crun) sets up the namespaces and cgroups and starts the process, following the OCI runtime spec. A high-level runtime such as containerd or CRI-O manages the whole life of containers around that: pulling and unpacking images, storage, starting and stopping, and calling the low-level runtime.
- A container engine is the user-facing layer on top: the tool people and scripts talk to, with a command-line interface or an API, that builds images, manages networks and volumes and asks a runtime to run the containers. Docker Engine and Podman are container engines.
Docker Engine is Docker's open
source container engine for Linux: the background service (daemon) dockerd with its API, plus the docker
command-line client. The client does nothing by itself; it sends
every command to the daemon, by default over the Unix socket
/var/run/docker.sock. docker version shows
both sides and the components underneath:
$ docker version
Client: Docker Engine - Community Version: 28.2.1 API version: 1.50 Go version: go1.24.3 Git commit: 879ac3f Built: Wed May 28 19:25:28 2025 OS/Arch: linux/arm64 Context: default Server: Docker Engine - Community Engine: Version: 28.2.1 API version: 1.50 (minimum version 1.24) Go version: go1.24.3 Git commit: 0e2cc22 Built: Wed May 28 19:25:28 2025 OS/Arch: linux/arm64 Experimental: false containerd: Version: 1.7.27 GitCommit: 05044ec0a9a75232cad458027ca83437aae3f4da runc: Version: 1.2.5 GitCommit: v1.2.5-0-g59923ef docker-init: Version: 0.19.0 GitCommit: de40ad0
Reading from the bottom: docker-init is a tiny init
process that docker run --init places in a container as
PID 1 (see
why PID 1 matters); runc starts the containers;
containerd manages them for the daemon; and the
Engine is dockerd. If the server half
is missing and you see "Cannot connect to the Docker daemon", the
client works but the daemon is not running or you have no access to
its socket.
Podman, Docker Desktop and licensing
Podman is an open source container
engine that needs no background daemon: each
podman command starts the containers itself, as the
user who runs it. It reads Dockerfiles, runs the same OCI images and
accepts almost the same commands, so much that its documentation
says most users can simply alias docker=podman. Because
there is no daemon running as root, rootless containers are its
normal mode, and it integrates well with
systemd. It is the default container tool on Red Hat Enterprise Linux and
Fedora. Podman 6.0, released in June 2026, dropped several legacy
features (among them cgroup v1 and the old CNI networking) in favor
of their modern replacements. The project's GitHub home moved to the
podman-container-tools organization.
Licensing confuses people, so precisely (as of October 2026):
- Docker Engine on Linux is open source and free, for any use and any company size.
- Docker Desktop (the application for Windows, macOS and Linux desktops, which includes a Linux virtual machine) needs a paid subscription for professional use in organizations with 250 or more employees or more than USD 10 million in annual revenue. It is free for smaller businesses, personal use, education and non-commercial open source projects.
- Podman and containerd are open source and free. Podman Desktop is a free graphical alternative to Docker Desktop.
| Where you meet it | Usually |
|---|---|
| A developer laptop | Docker Desktop, Podman Desktop or Rancher Desktop |
| A Linux server running a few containers | Docker Engine (Debian, Ubuntu) or Podman (Red Hat family) |
| A Kubernetes node | containerd or CRI-O, without Docker Engine |
| A cloud container service | Hidden from you; see managed container services |