Containers: What Is Isolated and What Is Not
Namespaces hide things and cgroups limit things — and several important resources are covered by neither. The gaps that cause the confusing failures.
A container is not a small machine. It is a process on the host with a restricted view and a resource budget, and the mental model of "lightweight VM" produces the wrong expectations in exactly the places that matter.
For a separate people-operations perspective, further details covers desktop activity signals.
Two mechanisms do the work, and they do different things.
Namespaces control what a process can see. Separate process tables, network stacks, filesystem mounts, hostnames, user ID mappings.
Control groups limit what a process can use. CPU, memory, I/O, process count.
Anything covered by neither is shared with the host and every other container, and that list is longer than people expect.
What is shared
The kernel. One kernel for the host and every container. A kernel panic or a kernel-level vulnerability affects everything. This is the fundamental difference from virtualisation and the reason container isolation is a weaker security boundary.
Kernel parameters, mostly. Some sysctl settings are namespaced and many are not. A container cannot generally tune the host's networking stack, and where it can, it affects everyone.
The clock. There is no time namespace in common use. A container cannot have its own time, and it cannot fix a host clock that is wrong. See clocks in distributed systems.
Page cache. Shared, and attributed to whichever cgroup first touched the page. A container reading large files fills a cache others are also using.
Entropy, the storage layer's queue, and the CPU cache hierarchy — all shared, all sources of interference between containers that are nominally isolated.
The gaps that produce confusing behaviour
The process may not see its own limits
The classic and still common failure. Older runtimes read /proc/cpuinfo and /proc/meminfo, which report the host's resources, not the cgroup limit.
A JVM on a 64-core host inside a container limited to one CPU would size its thread pools and garbage collector for 64 cores. The result is a process configured for resources it cannot use, thrashing on context switches, and being killed for memory it was told it had.
Modern runtimes are container-aware — recent JVM, .NET, Go — but this is version-dependent and worth verifying rather than assuming. Check what your runtime reports as available processors and memory from inside the container.
Anything reading nproc or parsing /proc/meminfo directly is still wrong, and that includes plenty of shell scripts and older libraries.
CPU limits work in two different ways
CPU shares are a relative weight. Under contention, a container with twice the shares gets twice the CPU. When the host is idle, it can use everything. Soft.
CPU quota is a hard ceiling enforced per period — typically 100ms. A container allowed 50% gets 50ms of CPU per 100ms period, and when it exhausts the quota it is stopped until the next period begins.
That throttling is the source of a very confusing symptom: a service showing low average CPU usage and terrible tail latency. It used its whole quota in the first 30ms of the period and sat frozen for 70ms. Average utilisation looks fine; p99 is dreadful.
Check the throttling counters, not just CPU usage. In cgroup v2, cpu.stat reports throttled periods and total throttled time. Any significant throttling with low average usage means the quota is the problem, and raising it or lengthening the period fixes latency without more hardware.
Memory limits kill rather than degrade
Covered in detail in what actually happens when you run out of memory, and the container-specific parts: hitting the cgroup limit kills the process regardless of host memory, page cache counts toward the limit, and exit code 137 with no application logs is the signature.
Filesystem writes are not free
The default union filesystem means writing to a file that exists in a lower layer copies the whole file up first. Writing one byte to a large file copies the large file.
Write to a volume, not to the container filesystem, for anything with real write volume. This also survives a restart, which container-filesystem writes do not.
Security: a boundary, not a strong one
Containers are a real barrier and a weaker one than most people assume.
Root inside the container is root on the host unless user namespaces are enabled, and they frequently are not by default. A container escape then gives full host access.
Run as a non-root user. The single highest-value setting, and it is one line in a Dockerfile.
Drop capabilities. The default set is more than most workloads need. Drop all and add back what is required.
Never run privileged unless you can articulate exactly why. --privileged removes most of the isolation.
Mounting the container runtime's socket is equivalent to giving root on the host. This is common in CI configurations and it is a serious exposure.
Read-only root filesystem, with writable volumes only where needed.
Seccomp and the mandatory access control layer restrict what system calls are available. The defaults are reasonable and worth keeping rather than disabling to make something work.
For genuine multi-tenancy, containers alone are not sufficient. Sandboxed runtimes or lightweight virtual machines exist precisely because the kernel is shared, and if you are running untrusted code, that is the category you need.
Networking behaves differently than expected
Each container gets a network namespace with its own interfaces and routing.
localhost inside a container is the container, not the host. A common source of confusion when a process expects to reach a service on the same machine.
DNS resolution goes through the runtime's resolver, and its behaviour — search domains, ndots settings, caching — differs from the host's. In orchestrated environments, an aggressive search-domain configuration turns one lookup into several failed ones first, which shows up as latency nobody can explain. See DNS is usually the problem.
Port mapping is address translation, and the source address the destination sees may be the host's rather than the container's, which breaks address-based logic.
A practical checklist
- [ ] Runtime reports the cgroup limits, not the host's, from inside the container
- [ ] CPU throttling monitored, not just CPU usage
- [ ] Memory limit set with headroom above observed peak, including page cache
- [ ] Non-root user
- [ ] Capabilities dropped to what is needed
- [ ] Not privileged, no runtime socket mounted
- [ ] Read-only root filesystem, volumes for writes
- [ ] Heavy writes go to volumes, not the union filesystem
- [ ] Base image updated, and rebuilt regularly rather than pinned indefinitely
The summary
Namespaces hide, cgroups limit, and the kernel is shared. Everything unexpected follows from the third.
CPU throttling with low average usage is the tail-latency mystery that most teams eventually spend a week on.
Check what your runtime believes about available resources, from inside the container.
Non-root and dropped capabilities are the two cheapest security improvements available, and both are one line.