Proxmox VEX · LXC ContainersArchitecture
LXC architecture: namespaces and cgroups
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
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:
| Namespace | What it isolates |
|---|---|
| PID | Process IDs (the container sees its own PID 1) |
| Network | Network devices, IPs, routes |
| Mount | Mount points |
| UTS | Hostname and domain name |
| IPC | Inter-process communication (System V IPC, POSIX message queues) |
| User | UIDs and GIDs (root in container = different UID on host) |
| Cgroup | cgroup 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.
# 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/userpid:[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:
| Config | cgroup v2 file | Effect |
|---|---|---|
cores: 4 | cpuset | Which host CPUs its tasks may run on |
cpulimit: 2 | cpu.max | Hard bandwidth ceiling |
cpuunits: 100 | cpu.weight | Relative share under contention only |
memory: 4096 | memory.max | Hard limit; OOM kill at the boundary |
swap: 512 | memory.swap.max | Additional swap allowance |
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:
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_maprather than trusting the config field.
Knowledge check
Knowledge check · 5 questions
Q1. What does a PID namespace provide?
Q2. A privileged container can be considered host-root-equivalent.
Q3. Which mechanism limits CPU and memory for a container?
Q4. Which of these are NOT namespaced, and are therefore shared by every container on a host? Select all that apply.
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.