Skip to main content
RunBook Academy

Proxmox VEX · LXC ContainersArchitecture

LXC architecture: namespaces and cgroups

Intermediate⏱ ~22 min

What you'll learn

  • Explain how Linux namespaces isolate what a process can see
  • Explain how cgroups limit what a process can consume
  • Map the user, mount, PID, network, UTS, IPC, and cgroup namespaces
  • Distinguish namespace boundaries from security boundaries
  • Inspect the namespaces and cgroup of a running container from the host

Prerequisites

Verified against Proxmox VE 9.2.4 · Proxmox Backup Server 4.2.5 · Ceph Squid / Tentacle · Debian 13 (Trixie) · Linux kernel 7.0 (PVE 9.2 default) · 2026-08-12

Not yet marked complete on this device.

Why this matters in production

Containers rely on two kernel features: namespaces (visibility) and cgroups (resources). Understanding them lets you reason about what a container can and cannot do.

Namespaces

A namespace wraps a global system resource so a process sees an isolated instance. Linux has seven namespace types relevant to containers:

NamespaceWhat it isolates
PIDProcess IDs (the container sees its own PID 1)
NetworkNetwork devices, IPs, routes
MountMount points
UTSHostname and domain name
IPCInter-process communication (System V IPC, POSIX message queues)
UserUIDs and GIDs (root in container = different UID on host)
Cgroupcgroup hierarchy visibility
flowchart LR
  A[Container processes] --> B[PID namespace]
  A --> C[Network namespace]
  A --> D[Mount namespace]
  A --> E[User namespace]

Seeing a namespace, not just reading about one

A namespace is an object in the kernel with an inode number. Two processes are in the same namespace exactly when the inode behind their /proc/<pid>/ns/ entry matches. That makes the abstraction directly observable.

Read-only / Safethe host and a container are in different namespaces - here is the proof
# readlink /proc/self/ns/pid /proc/self/ns/net /proc/self/ns/user
pct exec 200 -- readlink /proc/self/ns/pid /proc/self/ns/net /proc/self/ns/user
pid:[4026531836]
net:[4026531840]
user:[4026531837]
pid:[4026532614]
net:[4026532617]
user:[4026532610]

Illustrative output

Different numbers mean different namespaces. This is worth doing once by hand, because it converts “the container is isolated” from a claim into an observation, and because it is the fastest way to answer “is this process really inside the container” during an incident.

Note what the same command shows for a privileged container: its user namespace inode matches the host’s, because a privileged container is not in a user namespace at all. That single line of output is the whole privileged/unprivileged distinction, expressed mechanically.

cgroups

cgroups (control groups) limit, account for, and isolate resource usage. PVE 9 uses cgroup v2 exclusively - the unified hierarchy, mounted at /sys/fs/cgroup, with no v1 controllers and no hybrid mode. Any guidance you find that refers to memory.limit_in_bytes, cpu.cfs_quota_us or blkio.weight is describing cgroup v1 and does not apply.

flowchart LR
  A[Container processes] --> B[cpu.max: quota and period]
  A --> C[cpu.weight: relative share]
  A --> D[memory.max: hard limit]
  A --> E[pids.max: process count]

The container config maps onto v2 interface files:

Configcgroup v2 fileEffect
cores: 4cpusetWhich host CPUs its tasks may run on
cpulimit: 2cpu.maxHard bandwidth ceiling
cpuunits: 100cpu.weightRelative share under contention only
memory: 4096memory.maxHard limit; OOM kill at the boundary
swap: 512memory.swap.maxAdditional swap allowance
Read-only / Saferead a running container's actual limits
set -euo pipefail
CTID=200
CG=$(find /sys/fs/cgroup -maxdepth 2 -type d -name "$CTID" \
     -not -path '*monitor*' | head -1)

printf 'cgroup path: %s\n' "$CG"
cat "$CG/memory.max" "$CG/memory.current" "$CG/cpu.max" "$CG/cpu.weight"

Those files are the enforcement. Everything the web interface shows about a container’s limits is a rendering of them, and when the two disagree - which happens after a hand-edited config, or a pct set that did not take - the cgroup is what the kernel obeys. The full treatment is in container resource limits.

What namespaces give you

  • Visibility isolation: a process in a PID namespace cannot see or signal processes outside it (except via shared resources).
  • Resource visibility: a mount namespace gives the container its own filesystem hierarchy.
  • Identity isolation: a user namespace maps UIDs/GIDs so the container’s root is not the host’s root.

What namespaces do NOT give you

  • Kernel isolation: the container uses the host kernel. A kernel vulnerability in the host affects every container.
  • Hardware isolation: there is no VMX/SVM boundary.
  • Trust: a privileged container has many of the same capabilities as the host root.

Unprivileged containers

Proxmox’s unprivileged containers use user namespaces to map the container’s root (UID 0) to a high-numbered UID on the host. This is the default and should be kept.

flowchart LR
  subgraph CONTAINER[Container]
    UID0[UID 0]
  end
  subgraph HOST[Host]
    UID100000[UID 100000]
  end
  UID0 -->|maps to| UID100000

Production considerations

Common mistakes

  • Treating privileged containers as VMs.
  • Disabling cgroup limits “for performance.”
  • Running containers with kernel modules the host does not have.
  • Following cgroup v1 guidance on PVE 9, which is cgroup v2 only.
  • Reading a container’s memory usage from inside it during an incident, rather than from the host cgroup that is doing the enforcing.

Verification

Three commands that answer “is this container isolated the way I think it is”, and each can fail:

Read-only / Safeconfirm the isolation a container actually has
set -euo pipefail
CTID=200

# 1. Is it in a user namespace at all? A privileged container is not.
pct exec "$CTID" -- cat /proc/self/uid_map

# 2. Which namespaces differ from the host's?
for NS in pid net mnt uts ipc user cgroup; do
H=$(readlink "/proc/self/ns/$NS")
C=$(pct exec "$CTID" -- readlink "/proc/self/ns/$NS")
[ "$H" = "$C" ] && printf '%-7s SHARED WITH HOST\n' "$NS" \
                || printf '%-7s isolated\n' "$NS"
done

# 3. Are the limits you configured the limits being enforced?
CG=$(find /sys/fs/cgroup -maxdepth 2 -type d -name "$CTID" \
     -not -path '*monitor*' | head -1)
pct config "$CTID" | grep -E '^(cores|cpulimit|memory|swap):'
cat "$CG/memory.max" "$CG/cpu.max"

Check 1 is the decisive one. 0 100000 65536 is an unprivileged container. 0 0 4294967295 is a privileged container: the whole ID space, identity mapped, which is the mechanical statement of “container root is host root”.

Key takeaways

  • Namespaces give visibility isolation; cgroups give resource isolation.
  • Neither provides kernel isolation; containers share the host kernel.
  • PVE 9 is cgroup v2 only - v1 file names and guidance do not apply.
  • Default to unprivileged, and verify it by reading uid_map rather than trusting the config field.

Knowledge check

Knowledge check · 5 questions

  1. Q1. What does a PID namespace provide?

  2. Q2. A privileged container can be considered host-root-equivalent.

  3. Q3. Which mechanism limits CPU and memory for a container?

  4. Q4. Which of these are NOT namespaced, and are therefore shared by every container on a host? Select all that apply.

  5. Q5. You read /proc/self/uid_map inside a container and it reports "0 0 4294967295". What does this tell you?

Passing score: 75%. Answers are checked in this browser.