If you stay in the container world long enough, you will hear people say things like:
- "A container is just a lightweight VM"
- "Docker created containers"
- "Containers are magic"
All 3 are wrong enough to be dangerous.
A container is still just a Linux process (or process tree).
What makes that process feel like a tiny isolated machine is not magic. It is a combination of Linux primitives. If I have to compress that story into one teaching-friendly mental model, I would call these the 3 common Linux primitives used by containers:
- Namespaces for isolation
- Cgroups for resource control
- A layered root filesystem for the container's file view, often implemented with OverlayFS
This article uses Podman for the demos, because Podman is close enough to Docker to feel familiar, but transparent enough that we can still see the Linux pieces underneath.
1. What exactly is Podman?
If you are coming from Docker, the shortest accurate version is:
- Podman is a daemonless container engine
- It speaks a Docker-like CLI
- It uses an OCI runtime like
crunorruncunderneath - It supports both rootful and rootless containers
The word daemonless matters.
With Docker, the usual mental model is:
docker CLI -> dockerd -> containerd -> OCI runtime -> kernel
With Podman, the normal local mental model is simpler:
podman CLI -> libpod -> OCI runtime -> kernel
That does not mean Podman is "just a client". It is still a full container engine. It just does not require a long-lived central daemon like dockerd for normal local usage.
Strictly speaking, Podman also uses helper components such as conmon, but the simplified flow above is the right mental model for this article.
That is why Podman is a good teaching tool for this topic: it gets out of the way faster.
2. The 3 Common Linux Primitives Behind Containers
Primitive 1: Namespaces
Namespaces give a process its own view of specific system resources.
Common namespace types you will meet in containers:
- PID namespace: the process gets its own process tree
- NET namespace: its own network stack, interfaces, routes, ports
- MNT namespace: its own mount table
- UTS namespace: its own hostname
- IPC namespace: its own shared memory / semaphores
- USER namespace: user and group ID remapping, especially important for rootless containers
There are also cgroup and time namespaces, but the ones above are the types you will encounter most often in container discussions.
This is why process 12345 on the host can look like PID 1 inside the container. It is the same underlying kernel, but the process is looking through a different namespace view.
Primitive 2: Cgroups
Namespaces isolate what a process can see.
Cgroups control how much of the machine a process can consume.
That includes things like:
- CPU time
- Memory
- Number of processes
- I/O accounting and limits
Without cgroups, a "container" could still exist as an isolated process, but it would be terrible for multi-tenant systems because one noisy workload could starve everything else.
Primitive 3: A Layered Root Filesystem
Every container needs a filesystem view:
/bin/sh/etc/usr- application files
- libraries
That is the job of the container root filesystem.
In modern Linux container engines, this filesystem is often built from image layers plus a writable container layer. A very common implementation is OverlayFS.
That gives us the classic model:
lower layers (image) + upper layer (container writes) -> merged view
Important nuance: OverlayFS is common, but not mandatory. The deeper primitive is "a container-specific root filesystem view". OverlayFS is simply one of the most common ways to implement it on Linux.
3. Lab Setup
To follow this lab on Ubuntu 24.04, install:
- Podman
- jq
- util-linux for
nsenterand related tools - iproute2 for
ip addr
sudo apt update
sudo apt install -y podman jq util-linux iproute2
Quick checks:
podman version
podman info --format 'rootless={{.Host.Security.Rootless}} cgroupVersion={{.Host.CgroupVersion}} graphDriver={{.Store.GraphDriverName}} ociRuntime={{.Host.OCIRuntime.Name}}'
On a healthy modern host, you usually want to see:
cgroupVersion=v2- an OCI runtime such as
crunorrunc - a storage driver such as
overlay
For this article, I will use rootful Podman in the low-level inspection steps because it makes namespace and filesystem introspection simpler. Podman absolutely supports rootless mode too, and we will talk about that near the end.
4. Step 1: Run One Real Container
Let us create a tiny container with very visible limits:
sudo podman run -d --name primitive-lab \
--hostname primitive-lab \
--memory 128m \
--cpus 0.5 \
--pids-limit 64 \
docker.io/library/alpine:3.22 \
sleep infinity
Check it:
sudo podman ps
sudo podman inspect primitive-lab --format '{{.State.Status}}'
Grab the host PID of the container's main process:
PID=$(sudo podman inspect -f '{{.State.Pid}}' primitive-lab)
echo "$PID"
This is the key mental flip:
primitive-lab is not a mini-VM. It is just a process (or process tree) on the host. Podman simply created it with a different set of namespaces, cgroups, and mounts.
5. Step 2: Prove the Namespace Story
List the namespaces associated with that process:
sudo lsns -p "$PID"
You will typically see namespace types like:
mntutsipcpidnetcgroup
Now look at the namespace handles directly:
sudo readlink /proc/$PID/ns/mnt
sudo readlink /proc/$PID/ns/uts
sudo readlink /proc/$PID/ns/pid
sudo readlink /proc/$PID/ns/net
Those ns:[...] IDs are the kernel objects behind the isolation.
Enter the container's namespaces from the host:
sudo nsenter -t "$PID" -m -u -i -n -p sh
Now run these commands inside that namespace view:
hostname
ip addr
ps -ef
mount | head
What you should notice:
hostnameisprimitive-lab, not the host hostname- the process list is tiny compared with the host
- the network interfaces and routes are container-specific
- the mount table is not the same as the host mount table
Exit the namespace shell:
exit
That is namespaces in one line:
The process is still on the same kernel, but it sees a different world.
6. Step 3: Prove the Cgroup Story
We started the container with:
--memory 128m--cpus 0.5--pids-limit 64
Now let us see where those limits live.
First, inspect the cgroup path:
cat /proc/$PID/cgroup
On a cgroup v2 host, the interesting line usually looks like:
0::/some/cgroup/path
Capture it:
CGROUP_REL=$(awk -F: '$1=="0"{print $3}' /proc/$PID/cgroup)
CGROUP_DIR="/sys/fs/cgroup${CGROUP_REL}"
echo "$CGROUP_DIR"
Now inspect the actual cgroup files:
sudo cat "$CGROUP_DIR/memory.max"
sudo cat "$CGROUP_DIR/cpu.max"
sudo cat "$CGROUP_DIR/pids.max"
You should see values that correspond to the limits we gave Podman.
For example:
memory.maxshould be around134217728bytes for128mpids.maxshould be64cpu.maxshould show a quota/period pair that approximates0.5 CPU
This is the second mental flip:
Podman did not invent resource limits. It translated your CLI flags into cgroup settings the Linux kernel already understands.
You can also look at live usage:
sudo podman stats --no-stream primitive-lab
Again, the container is not special. It is just a process tree attached to a cgroup subtree.
7. Step 4: Prove the Layered Filesystem Story
Now let us inspect the container filesystem driver:
sudo podman inspect primitive-lab | jq '.[0].GraphDriver'
On many modern Linux hosts, the interesting shape looks like this:
{
"Name": "overlay",
"Data": {
"LowerDir": "...",
"UpperDir": "...",
"WorkDir": "...",
"MergedDir": "..."
}
}
That maps directly to OverlayFS concepts:
- LowerDir: read-only image layers
- UpperDir: writable layer for this container
- WorkDir: OverlayFS bookkeeping
- MergedDir: the final mounted root filesystem the process sees
Extract those paths:
LOWER_DIR=$(sudo podman inspect primitive-lab | jq -r '.[0].GraphDriver.Data.LowerDir')
UPPER_DIR=$(sudo podman inspect primitive-lab | jq -r '.[0].GraphDriver.Data.UpperDir')
MERGED_DIR=$(sudo podman inspect primitive-lab | jq -r '.[0].GraphDriver.Data.MergedDir')
echo "LOWER_DIR=$LOWER_DIR"
echo "UPPER_DIR=$UPPER_DIR"
echo "MERGED_DIR=$MERGED_DIR"
Now write a file from inside the container:
sudo podman exec primitive-lab sh -c 'echo "hello from upperdir" > /root/hello.txt'
Check the merged filesystem view:
sudo ls -l "$MERGED_DIR/root/hello.txt"
sudo cat "$MERGED_DIR/root/hello.txt"
Then inspect the upper layer:
sudo ls -l "$UPPER_DIR/root/hello.txt"
sudo cat "$UPPER_DIR/root/hello.txt"
That file was not baked into the image. It landed in the writable upper layer of this specific container.
So the third mental flip is:
A container image is not one giant mutable disk. It is usually a stack of read-only layers plus one writable layer on top.
That is why containers can start fast and why many containers can share the same base image efficiently.
8. Put the 3 Pieces Together
By now, the "container magic" should look much more boring:
Podman CLI
-> asks an OCI runtime to start a process
-> that process gets new namespaces
-> that process is attached to cgroups
-> that process sees a merged root filesystem
-> done
So when somebody says "a container is lightweight", the kernel-level translation is closer to:
- no second kernel
- no hardware virtualization boundary
- no emulated machine
- just a process with isolation, limits, and a filesystem view
That is the reason containers are usually lighter than VMs.
9. Where Podman Fits In
At this point, we can describe Podman more accurately.
Podman is not "the thing that makes containers possible".
Linux already had the primitives.
Podman is the tool that:
- pulls images
- creates container metadata
- sets up mounts
- invokes an OCI runtime
- gives you a human-friendly CLI for all of that
Roughly, when you type podman run, the flow looks something like this:
podman run
-> prepare container metadata and OCI spec
-> set up the OverlayFS mount (lower + upper + merged)
-> prepare networking (typically via netavark; rootless setups often use slirp4netns or pasta)
-> call OCI runtime (crun/runc) with the spec
-> OCI runtime creates the process with new namespaces
-> attach the process to its cgroup
-> switch root into the merged filesystem
-> exec the entrypoint
This is a simplified view. The real implementation has more moving parts. But the point is: every step maps back to one of the 3 primitives we just proved.
In other words, Podman is the orchestrator of the local container lifecycle, not the source of the underlying kernel features.
That is also why Docker, Podman, containerd, and CRI-O can all feel different at the UX layer while still relying on the same Linux building blocks underneath.
10. Bonus: Rootless Podman Is Where User Namespaces Get Fun
One thing Podman does especially well is rootless containers.
In rootless mode:
- you can run containers as a regular user
- Podman uses user namespaces to map container IDs
- container storage usually lives under your home directory
- networking may use helpers such as slirp4netns or pasta
Quick check:
podman info --format '{{.Host.Security.Rootless}}'
If you want to peek into the rootless user namespace mapping:
podman unshare cat /proc/self/uid_map
podman unshare cat /proc/self/gid_map
But the real "aha" moment is this: root inside the container, non-root outside.
# Run a rootless container
podman run -d --name rootless-test alpine sleep infinity
# Inside the container, the process thinks it is root
podman exec rootless-test whoami # -> root
podman exec rootless-test id # -> uid=0(root)
# But on the host, the PID runs as YOUR user
RPID=$(podman inspect -f '{{.State.Pid}}' rootless-test)
ps -o user,pid,cmd -p "$RPID" # -> your_username, not root
# Clean up
podman rm -f rootless-test
That is user namespaces doing the mapping. The container sees UID 0, but the kernel knows the real UID is your unprivileged user.
That is a great next topic once you are comfortable with the 3 primitives from this article.
11. Summary
If you want one sentence to remember, make it this:
A container is just a Linux process built from namespaces, cgroups, and a container-specific root filesystem.
And if you want the Podman version:
Podman is a daemonless container engine that packages those Linux primitives into a clean, Docker-like workflow.
So yes, "namespaces + cgroups + OverlayFS" is a useful teaching shortcut.
But the most precise version is:
- Namespaces provide isolation
- Cgroups provide resource control
- A layered root filesystem provides the file view
- OverlayFS is a very common implementation of that last part
Once you internalize that, containers stop looking magical and start looking like what they really are:
well-packaged Linux processes (or process trees).
12. Cleanup
sudo podman rm -f primitive-lab
References:
- Podman: What is Podman?
- Podman rootless mode
- Linux cgroup v2 documentation
- Linux OverlayFS documentation
- Linux namespaces overview
- Linux
nsenter