Development
2026-01-15
11 min read

Processes vs Containers: What Docker Really Isolates

A

Abhay Vachhani

Developer

There's a common misconception that containers are "lightweight virtual machines." This isn't quite right. The reality is simpler and more elegant: a container is just a process on your host machine, wrapped in a few clever Linux kernel features. Understanding these features—namespaces and cgroups—is the key to truly understanding Docker.

The Truth About Containers

When you run docker run nginx, Docker doesn't spin up a mini operating system. It starts a process—just like any other process on your machine. You can verify this yourself:

$ docker run -d nginx
$ ps aux | grep nginx
root     12345  0.0  0.1  nginx: master process

That nginx process is visible on your host system. So what makes it a "container"? Two Linux kernel features: namespaces (for isolation) and cgroups (for resource limits).

Linux Namespaces: The Isolation Layer

Namespaces are the magic that makes containers feel isolated. They create separate views of system resources, so a process thinks it's alone in the world. Modern Linux provides 8 types of namespaces:

1. PID Namespace

Process ID isolation. Inside a container, the main process sees itself as PID 1, even though the host sees it as a different PID. This creates the illusion of a separate process tree.

# Inside container
$ ps aux
PID   USER     COMMAND
1     root     nginx

# On host - get the actual PID
CID=$(docker run -d nginx)
PID=$(docker inspect -f '{{.State.Pid}}' "$CID")

# Proof: inspect namespace links
ls -l /proc/$PID/ns/pid
lrwxrwxrwx 1 root root 0 pid:[4026532198]

The PID 1 gotcha: PID 1 has special responsibilities in Linux—it must reap zombie processes and handle signals differently. If your container hangs on shutdown, it's often because your app doesn't handle SIGTERM properly. Use docker run --init or tini to get a proper init process.

2. Network Namespace

Each container gets its own network stack: network interfaces, IP addresses, routing tables, and port bindings. This is why multiple containers can each bind to port 80 inside their own namespace without conflicts. However, only one container can publish to the host's port 80 at a time (e.g., -p 80:80).

# Create a network namespace manually
$ sudo ip netns add demo
$ sudo ip netns exec demo ip addr
# Shows only loopback, isolated from host

3. Mount Namespace

Filesystem isolation. Containers have their own root filesystem and mount points. When you see /app inside a container, it's completely separate from the host's /app.

4. UTS Namespace

Hostname and domain name isolation. Each container can have its own hostname without affecting the host or other containers.

5. IPC Namespace

Inter-Process Communication isolation. Shared memory segments, semaphores, and message queues are isolated between containers.

6. User Namespace

UID/GID mapping. This allows a process to be root inside the container (UID 0) while being an unprivileged user on the host. This is critical for security but often not enabled by default—available via rootless Docker or userns-remap configuration.

7. Cgroup Namespace

Isolates the view of cgroups, making the container's cgroup appear as the root. This is a newer addition to Linux namespaces.

8. Time Namespace

Time namespaces virtualize CLOCK_MONOTONIC and CLOCK_BOOTTIME by applying per-namespace offsets. This is useful for testing time-dependent logic (timeouts, retries, "uptime", boot-time assumptions) without changing the host clock. Note: it doesn't generally "warp" wall-clock time (CLOCK_REALTIME).

Cgroups: Resource Limits

While namespaces provide isolation, cgroups (control groups) enforce resource limits. Without cgroups, a container could consume all available CPU or memory, starving other processes. Cgroups prevent this.

Resource Controllers

  • CPU: Limit CPU usage with --cpus or --cpu-shares. On cgroup v2, relative CPU sharing is expressed via weight (cpu.weight), while hard caps use cpu.max.
  • Memory: Set hard limits with --memory
  • Block I/O: Control disk read/write with --blkio-weight
  • Network: Managed via traffic control (tc) outside Docker
# Limit container to 512MB RAM and 0.5 CPU cores
CID=$(docker run -d --memory=512m --cpus=0.5 nginx)
PID=$(docker inspect -f '{{.State.Pid}}' "$CID")

# Inspect cgroup settings (works on both v1 and v2)
cat /proc/$PID/cgroup
# Follow the path under /sys/fs/cgroup/...
# On cgroup v2: cat /sys/fs/cgroup/.../memory.max
# On cgroup v1: cat /sys/fs/cgroup/memory/.../memory.limit_in_bytes

Putting It Together: How Docker Uses These

When you run docker run, here's what happens under the hood:

  • Docker creates a new set of namespaces (PID, network, mount, etc.)
  • Docker configures cgroups to enforce resource limits
  • The container runtime (runc) starts your process inside these namespaces
  • Your process runs, completely unaware it's "containerized"

Practical Demonstrations

Creating a "Container" Manually

You can create container-like isolation without Docker using the unshare command:

# Create a new PID and mount namespace
$ sudo unshare --pid --fork --mount-proc /bin/bash
# You're now in an isolated environment
$ ps aux
# Shows only processes in this namespace

Inspecting Container Namespaces

# List all namespaces
$ sudo lsns

# Enter a container's namespace
$ docker inspect [container] | grep Pid
$ sudo nsenter -t [pid] -n ip addr
# Now you're inside the container's network namespace

Viewing Cgroup Configuration

# Get PID, then locate cgroup path (v1 or v2)
CID=$(docker run -d --memory=512m --cpus=0.5 nginx)
PID=$(docker inspect -f '{{.State.Pid}}' "$CID")

# Shows the cgroup membership paths (works everywhere)
cat /proc/$PID/cgroup

# On cgroup v2 you'll typically read:
#   /sys/fs/cgroup//memory.max
# On cgroup v1 you'll read controller-specific files like:
#   /sys/fs/cgroup/memory//memory.limit_in_bytes

# Monitor resource usage in real-time
$ systemd-cgtop

Security Implications

Understanding namespaces and cgroups is crucial for container security:

  • Namespaces provide isolation, not security boundaries. A kernel vulnerability can allow escape.
  • Privileged containers bypass many protections. Avoid --privileged unless absolutely necessary.
  • User namespaces improve security by mapping container root to an unprivileged host user.
  • Cgroups prevent resource exhaustion attacks but don't prevent privilege escalation.

Actionable mitigations: Drop unnecessary capabilities (--cap-drop=ALL --cap-add=NET_BIND_SERVICE), prevent privilege escalation (--security-opt no-new-privileges), use seccomp profiles to restrict syscalls, enable AppArmor or SELinux, and mount filesystems read-only where possible (--read-only). Defense in depth is key.

Common FAQs

Conclusion

Containers aren't magic—they're processes with clever isolation. By understanding namespaces and cgroups, you gain insight into how Docker really works, which helps with debugging, security, and performance optimization. The next time you run docker run, remember: you're just starting a process with some kernel features enabled. That's the beauty of containers.

FAQs

Are containers really just processes?

Yes. They're processes with Linux kernel features (namespaces + cgroups) applied. There's no magic virtualization layer.

Can I use namespaces without Docker?

Absolutely. Tools like unshare, nsenter, and ip netns let you work with namespaces directly.

Why don't containers use user namespaces by default?

Compatibility. Many images assume root privileges. Docker supports user namespaces, but they must be explicitly enabled.

What's the performance overhead of containers?

Minimal. Since containers share the host kernel, there's almost no overhead compared to running processes directly.