Crisp answer: Namespaces give processes an isolated view of the system (they can't see outside their namespace). cgroups limit what resources they can consume. Together they're what makes containers work — there's no guest kernel, just isolation and resource limits on regular Linux processes.
Namespaces in depth:
A namespace wraps a global resource and gives each namespace its own isolated instance. Processes inside see only their namespace's resources.
PID namespace:
# On the host, a container's init process has some high PID:
ps aux | grep myapp
# 29847 /myapp/server
# Inside the container, the same process sees itself as PID 1:
docker exec mycontainer ps aux
# PID CMD
# 1 /myapp/server
# Why this matters: a process can only send signals to processes in its
# namespace. PID 1 in a container namespace gets SIGTERM when the container
# stops — it must handle it to shut down gracefully.
Network namespace:
# Each container gets its own network namespace:
# - Its own loopback (127.0.0.1)
# - Its own eth0 with a container IP
# - Its own routing table
# - Its own iptables rules
# The veth (virtual ethernet) pair connects the container ns to the host:
ip link show # On host: shows veth pairs
ip netns list # List network namespaces (may not show Docker namespaces)
# Kubernetes creates a pause container to hold the network namespace for the pod.
# All containers in a pod share the same network namespace — that's why they
# can communicate on localhost.
Mount namespace:
# Each container has its own filesystem view.
# Uses overlay (overlayfs) filesystem:
# Lower: read-only image layers
# Upper: writable container layer (lost when container is removed)
# Merged: what the container sees
docker inspect --format '{{.GraphDriver.Data.LowerDir}}' mycontainer
# /var/lib/docker/overlay2/abc.../diff:
# /var/lib/docker/overlay2/def.../diff
# Kubernetes uses similar overlay layers via containerd
User namespace (rootless containers):
# User namespaces map UIDs inside the namespace to different UIDs outside.
# This allows a process to be "root" (UID 0) inside a container,
# but map to an unprivileged user on the host.
# Inside container: root (UID 0)
# On host: UID 65534 (nobody)
# This is how Podman rootless and Docker rootless mode work.
cat /proc/<pid>/uid_map # See the UID mapping
Inspecting namespaces:
# See what namespaces a process is in:
ls -la /proc/<pid>/ns/
# lrwxrwxrwx cgroup -> cgroup:[4026531835]
# lrwxrwxrwx ipc -> ipc:[4026531839]
# lrwxrwxrwx mnt -> mnt:[4026532174] ← unique = has its own mount namespace
# lrwxrwxrwx net -> net:[4026532176] ← unique = has its own network namespace
# lrwxrwxrwx pid -> pid:[4026532175] ← unique = has its own PID namespace
# lrwxrwxrwx user -> user:[4026531837]
# lrwxrwxrwx uts -> uts:[4026532173]
# Two processes with the same namespace inode share that namespace.
# Two processes with different inodes are isolated.
# Enter a process's namespace (debugging tool):
nsenter -t <pid> --net # Enter its network namespace
nsenter -t <pid> --all # Enter all namespaces (like exec into a container)
cgroups in depth:
cgroups (control groups) organise processes into a hierarchy and enforce resource limits. cgroups v2 (unified hierarchy) is the default on modern kernels.
# cgroups v2 filesystem:
ls /sys/fs/cgroup/
# cpu.stat io.stat memory.stat ...
# system.slice/ user.slice/ docker/ kubepods/
# Kubernetes cgroup structure:
ls /sys/fs/cgroup/kubepods/
# burstable/ guaranteed/ besteffort/
# These correspond to Kubernetes QoS classes
# See a pod's cgroup:
cat /proc/<container-pid>/cgroup
# 0::/kubepods/burstable/podXXXX/containerYYYY
# Memory limit for a container:
cat /sys/fs/cgroup/kubepods/burstable/podXXXX/containerYYYY/memory.max
# 536870912 ← 512MB in bytes (or "max" = unlimited)
# Current memory usage:
cat /sys/fs/cgroup/kubepods/burstable/podXXXX/containerYYYY/memory.current
# CPU limit (100000 = 100ms of 100ms = 1 full CPU):
cat /sys/fs/cgroup/kubepods/burstable/podXXXX/containerYYYY/cpu.max
# 200000 100000 ← 200ms per 100ms = 2 CPUs
OOM killer and cgroups:
When a cgroup exceeds its memory limit, the kernel's OOM killer fires and kills processes in that cgroup. In Kubernetes:
# Check for OOM kills:
dmesg | grep -i "oom\|killed process"
# [1234.567] oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=...,
# task=myapp,pid=29847,uid=0
# [1234.568] Memory cgroup out of memory: Killed process 29847 (myapp)
# Or in journalctl:
journalctl -k | grep -i oom
# Kubernetes event:
kubectl describe pod <pod>
# Last State: Terminated Reason: OOMKilled
The complete container creation sequence (using namespaces + cgroups):
- Container runtime creates a new cgroup for the container
- Sets memory, CPU, pids limits on the cgroup
- Calls
clone()with namespace flags:CLONE_NEWPID— new PID namespace (process sees itself as PID 1)CLONE_NEWNET— new network namespaceCLONE_NEWNS— new mount namespaceCLONE_NEWUTS— new hostname namespaceCLONE_NEWIPC— new IPC namespace
- Adds the new process to the cgroup
- Sets up the overlay filesystem and
pivot_rootinto it - Applies seccomp profile and drops capabilities
execve()the container entrypoint
What to say in the interview:
"Namespaces and cgroups are the two kernel primitives that make containers work. Namespaces give each container an isolated view: its own PID space so it thinks it's PID 1, its own network stack with its own IP, its own filesystem. cgroups enforce the limits: memory.max triggers OOM kills, cpu.max enforces CPU quotas. When you see OOMKilled in Kubernetes, that's the kernel's OOM killer firing because the container exceeded its cgroup memory limit. I can inspect this directly in /sys/fs/cgroup/kubepods — the container's cgroup is there with current usage and limits. The container runtime sets all this up with clone() flags and cgroup writes; the kernel enforces everything. There's no hypervisor, no guest kernel — just very well-configured isolation on the host kernel."