Cloud Native 23 min read

Pods vs Containers: Exploring Namespaces, cgroups, and Docker‑Based Pods

This article examines how containers and Kubernetes Pods differ by diving into Linux namespaces, cgroups, and practical experiments that show how Pods share resources, how Docker can emulate Pod behavior, and why Pods are considered a higher‑level construct than simple containers.

Efficient Ops
Efficient Ops
Efficient Ops
Pods vs Containers: Exploring Namespaces, cgroups, and Docker‑Based Pods

1. Exploring Containers

The container can be a lightweight VM alternative, but Docker/OCI standardization popularized the single‑process container model, which improves isolation, scaling, and reusability, yet most VMs run multiple services.

Docker offers workarounds for multi‑service containers, while Kubernetes takes a bolder step by using a group of co‑located containers called a Pod as the smallest deployable unit.

When first learning Kubernetes, you discover each Pod has a unique IP and hostname, and containers within the same Pod can communicate via

localhost

, making a Pod appear like a tiny server.

Later you notice each container has its own isolated filesystem, so you cannot see processes of sibling containers, suggesting a Pod is merely a set of containers sharing a network stack.

However, containers in a Pod can also share memory, proving that network namespace is not the only shared resource.

2. Exploring Container Internals

OCI runtime specifications are not limited to Linux containers, but in this article "container" refers to the traditional Linux implementation using namespaces and cgroups.

Setting up a Playground

<code>$ cat > Vagrantfile <<EOF
# -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.configure("2") do |config|
  config.vm.box = "debian/buster64"
  config.vm.hostname = "docker-host"
  config.vm.define "docker-host"
  config.vagrant.plugins = ['vagrant-vbguest']
  config.vm.provider "virtualbox" do |vb|
    vb.cpus = 2
    vb.memory = "2048"
  end
  config.vm.provision "shell", inline: <<-SHELL
    apt-get update
    apt-get install -y curl vim
  SHELL
  config.vm.provision "docker"
end
EOF
$ vagrant up
$ vagrant ssh
</code>

Start a container:

<code>$ docker run --name foo --rm -d --memory='512MB' --cpus='0.5' nginx</code>

Exploring Container Namespaces

After a container starts, the following isolation primitives are created:

<code># Look up the container in the process tree.
$ ps auxf
root      4707 /usr/bin/containerd-shim-runc-v2 -namespace moby -id cc9466b3e...
root      4727 \_ nginx: master process nginx -g daemon off;
systemd+  4781 \_ nginx: worker process
systemd+  4782 \_ nginx: worker process

# Find the namespaces used by 4727 process.
$ sudo lsns
        NS TYPE   NPROCS   PID USER    COMMAND
4026532157 mnt         3  4727 root    nginx: master process nginx -g daemon off;
4026532158 uts         3  4727 root    nginx: master process nginx -g daemon off;
4026532159 ipc         3  4727 root    nginx: master process nginx -g daemon off;
4026532160 pid         3  4727 root    nginx: master process nginx -g daemon off;
4026532162 net         3  4727 root    nginx: master process nginx -g daemon off;
</code>

The container is isolated by the following namespaces:

mnt (mount): container has an isolated mount table.

uts (Unix Time Sharing): container has its own hostname and domain.

ipc (inter‑process communication): processes can communicate via system‑level IPC within the container.

pid (process ID): processes see only those in the same PID namespace.

net (network): container has its own network stack.

User namespaces are not used by default, so the container’s root user is the host’s root.

Cgroup namespaces provide an isolated view of the cgroup hierarchy; Docker can place a container in a private cgroup namespace but does not do so by default.

Exploring Container cgroups

Even though a container’s processes are isolated, they still share host resources, which can be limited using cgroups.

<code>PID=$(docker inspect --format '{{.State.Pid}}' foo)
# Check cgroupfs node for the container main process (4727).
$ cat /proc/${PID}/cgroup
11:freezer:/docker/cc9466b3eb67ca374c925794776aad2fd45a34343ab66097a44594b35183dba0
10:blkio:/docker/cc9466b3eb67ca374c925794776aad2fd45a34343ab66097a44594b35183dba0
9:rdma:/
8:pids:/docker/cc9466b3eb67ca374c925794776aad2fd45a34343ab66097a44594b35183dba0
7:devices:/docker/cc9466b3eb67ca374c925794776aad2fd45a34343ab66097a44594b35183dba0
6:cpuset:/docker/cc9466b3eb67ca374c925794776aad2fd45a34343ab66097a44594b35183dba0
5:cpu,cpuacct:/docker/cc9466b3eb67ca374c925794776aad2fd45a34343ab66097a44594b35183dba0
4:memory:/docker/cc9466b3eb67ca374c925794776aad2fd45a34343ab66097a44594b35183dba0
3:net_cls,net_prio:/docker/cc9466b3eb67ca374c925794776aad2fd45a34343ab66097a44594b35183dba0
2:perf_event:/docker/cc9466b3eb67ca374c925794776aad2fd45a34343ab66097a44594b35183dba0
1:name=systemd:/docker/cc9466b3eb67ca374c925794776aad2fd45a34343ab66097a44594b35183dba0
0::/system.slice/containerd.service
</code>

Docker also allows checking memory limits:

<code>ID=$(docker inspect --format '{{.Id}}' foo)
$ cat /sys/fs/cgroup/memory/docker/${ID}/memory.limit_in_bytes
536870912  # 512 MB as requested
</code>

2. Exploring Pods

Kubernetes Pods are the smallest deployable unit; their implementation can vary across CRI runtimes. For example, when Kata containers are used, a Pod may be a real VM.

We use a minikube cluster with the ContainerD runtime to keep the comparison fair.

Setting up a Playground

<code># Install arkade
$ curl -sLS https://get.arkade.dev | sh
$ arkade get kubectl minikube
$ minikube start --driver virtualbox --container-runtime containerd
</code>

Create a test Pod:

<code>$ kubectl --context=minikube apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
  name: foo
spec:
  containers:
    - name: app
      image: docker.io/kennethreitz/httpbin
      ports:
        - containerPort: 80
      resources:
        limits:
          memory: "256Mi"
    - name: sidecar
      image: curlimages/curl
      command: ["/bin/sleep", "3650d"]
      resources:
        limits:
          memory: "128Mi"
EOF
</code>

Exploring Pod Containers

Inspect the Pod on the node:

<code>$ minikube ssh
$ ps auxf
root      4947 \_ containerd-shim -namespace k8s.io -workdir /mnt/sda1/var/lib/containerd/...
root      4966 \_ /pause
root      4981 \_ containerd-shim -namespace k8s.io -workdir /mnt/sda1/var/lib/containerd/...
root      5001 \_ /usr/bin/python3 /usr/local/bin/gunicorn -b 0.0.0.0:80 httpbin:app -k gevent
root      5016   \_ /usr/bin/python3 /usr/local/bin/gunicorn -b 0.0.0.0:80 httpbin:app -k gevent
root      5018 \_ containerd-shim -namespace k8s.io -workdir /mnt/sda1/var/lib/containerd/...
100       5035 \_ /bin/sleep 3650d
</code>

Three container processes are created: the pause container (the sandbox) and the two user containers.

Using

ctr

we see three containers, while

crictl ps

lists only the two user containers; the pause container is the sandbox.

<code>$ sudo ctr --namespace=k8s.io containers ls
CONTAINER      IMAGE                                   RUNTIME
097d4fe8a7002  docker.io/curlimages/curl@sha256:...   io.containerd.runtime.v1.linux
dfb1cd29ab750  docker.io/kennethreitz/httpbin:latest   io.containerd.runtime.v1.linux
f0e87a9330466  k8s.gcr.io/pause:3.1                    io.containerd.runtime.v1.linux
</code>

The pause container provides the shared network, IPC, and UTS namespaces for the Pod.

Exploring Pod Namespaces

<code>$ sudo lsns
        NS TYPE   NPROCS   PID USER    COMMAND
4026532614 net         4  4966 root    /pause
4026532715 mnt         1  4966 root    /pause
4026532716 uts         4  4966 root    /pause
4026532717 ipc         4  4966 root    /pause
4026532718 pid         1  4966 root    /pause
4026532719 mnt         2  5001 root    /usr/bin/python3 ...
4026532720 pid         2  5001 root    /usr/bin/python3 ...
4026532721 mnt         1  5035 100    /bin/sleep 3650d
4026532722 pid         1  5035 100    /bin/sleep 3650d
</code>

httpbin and sleep containers reuse the pause container’s net, uts, and ipc namespaces.

<code># Inspect httpbin container
$ sudo crictl inspect dfb1cd29ab750
"namespaces": [
  {"type": "pid"},
  {"type": "ipc", "path": "/proc/4966/ns/ipc"},
  {"type": "uts", "path": "/proc/4966/ns/uts"},
  {"type": "mount"},
  {"type": "network", "path": "/proc/4966/ns/net"}
]
</code>

Exploring Pod cgroups

Pod cgroups can be visualized with

systemd-cgls

:

<code>$ sudo systemd-cgls
Control group /:
-.slice
├─kubepods
│ ├─burstable
│ │ ├─pod4a8d5c3e-3821-4727-9d20-965febbccfbb
│ │ │ ├─f0e87a93304666766ab139d52f10ff2b8d4a1e6060fc18f74f28e2cb000da8b2
│ │ │ │ └─4966 /pause
│ │ │ ├─dfb1cd29ab750064ae89613cb28963353c3360c2df913995af582aebcc4e85d8
│ │ │ │ ├─5001 /usr/bin/python3 /usr/local/bin/gunicorn -b 0.0.0.0:80 httpbin:app -k gevent
│ │ │ │ └─5016 /usr/bin/python3 /usr/local/bin/gunicorn -b 0.0.0.0:80 httpbin:app -k gevent
│ │ │ └─097d4fe8a7002d69d6c78899dcf6731d313ce8067ae3f736f252f387582e55ad
│ │ │   └─5035 /bin/sleep 3650d
</code>

3. Implementing Pods with Docker

By creating a shared cgroup parent and reusing the sandbox container’s network and IPC namespaces, Docker can emulate a Pod.

Setup

<code>$ sudo apt-get install cgroup-tools
$ sudo cgcreate -g cpu,memory:/pod-foo
$ docker run -d --rm \
  --name foo_sandbox \
  --cgroup-parent /pod-foo \
  --ipc 'shareable' \
  alpine sleep infinity
</code>

Launch containers that share the sandbox

<code># app (httpbin)
$ docker run -d --rm \
  --name app \
  --cgroup-parent /pod-foo \
  --network container:foo_sandbox \
  --ipc container:foo_sandbox \
  kennethreitz/httpbin

# sidecar (sleep)
$ docker run -d --rm \
  --name sidecar \
  --cgroup-parent /pod-foo \
  --network container:foo_sandbox \
  --ipc container:foo_sandbox \
  curlimages/curl sleep 365d
</code>

Docker cannot currently share the UTS namespace, so the containers have separate hostnames.

Inspecting the resulting cgroup hierarchy shows a structure similar to the Kubernetes Pod:

<code>$ sudo systemd-cgls memory
Controller memory; Control group /:
├─pod-foo
│ ├─488d76cade5422b57ab59116f422d8483d435a8449ceda0c9a1888ea774acac7
│ │ ├─27865 /usr/bin/python3 /usr/local/bin/gunicorn -b 0.0.0.0:80 httpbin:app -k gevent
│ │ └─27880 /usr/bin/python3 /usr/local/bin/gunicorn -b 0.0.0.0:80 httpbin:app -k gevent
│ └─9166a87f9a96a954b10ec012104366da9f1f6680387ef423ee197c61d37f39d7
│   └─27977 sleep 365d
</code>

4. Summary

Containers and Pods share the same underlying Linux namespaces and cgroups, but a Pod is a higher‑level construct that groups containers on the same node, synchronizes their lifecycles, and deliberately reduces isolation to simplify inter‑container communication. This makes Pods feel more like traditional VMs, enabling patterns such as sidecars or reverse proxies.

DockerKubernetescgroupsContainersNamespacesPods
Efficient Ops
Written by

Efficient Ops

This public account is maintained by Xiaotianguo and friends, regularly publishing widely-read original technical articles. We focus on operations transformation and accompany you throughout your operations career, growing together happily.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.