Containers are Linux: namespaces, cgroups and Docker
A container is a Linux process: namespaces limit what it sees, cgroups what it uses, ulimits each process. Build one by hand, then see how Docker puts it together.
By the end of this lesson you'll be able to
0 of 6 completeA container is a process
Everything in this course applies inside containers, because a container is a Linux process. The kernel has no "container" object. It has two features that, used together, make a process look like it has a machine to itself:
| Virtual machine | Container | |
|---|---|---|
| Kernel | its own | the host's, shared |
| Starts in | tens of seconds | milliseconds |
| Isolation | hardware-level | kernel features: strong, but one kernel |
| Seen from the host | one qemu process | the app's own processes, in ps |
Namespaces: what a process can see
Each namespace gives a process its own view of one part of the system:
| Namespace | Gives the process its own | Container effect |
|---|---|---|
pid | process numbers | its app is PID 1, it can't see host processes |
net | interfaces, IPs, ports, routes | its own eth0 and localhost |
mnt | mounted filesystems | sees the image's files as / |
uts | hostname | hostname shows the container ID |
user | user and group IDs | root inside can be a normal user outside |
ipc, cgroup, time | shared memory, cgroup view, clocks |
$ ls /proc/self/ns # every process has one of each cgroup ipc mnt net pid pid_for_children time time_for_children user uts
ls /proc/self/nslslist files
You can make namespaces yourself, without root, using unshare (-r maps you to root inside a new
user namespace):
$ unshare -r -u sh -c 'hostname box1; hostname' # new UTS namespace: own hostname box1 $ hostname # the host is untouched Elonerajeev $ unshare -r -p -f --mount-proc ps -ef # new PID namespace UID PID PPID C STIME TTY TIME CMD root 1 0 0 05:40 ? 00:00:00 ps -ef
Inside the new PID namespace, ps is PID 1 and sees nothing else. That's exactly why docker exec … ps shows only the container's processes. nsenter does the reverse: it runs a command inside an
existing process's namespaces, which is how you debug a container that has no shell.
cgroups: what a process can use
Control groups put processes in a tree and limit each branch: memory, CPU, number of processes,
disk I/O. Modern distributions use cgroups v2, one tree under /sys/fs/cgroup:
$ stat -fc %T /sys/fs/cgroup # cgroup2fs means v2 cgroup2fs $ cat /proc/self/cgroup # which cgroup this shell is in $ systemd-cgls --no-pager | head # the whole tree: systemd organises it $ systemd-cgtop # live use per cgroup, like top
cat /proc/self/cgroupcatprint a file
systemd-cgls --no-pager | head|pipe: send output to the next commandheadshow the first lines
systemd puts every service in its own cgroup, which is why systemctl status shows a service's
memory. You can limit any command or service the same way a container runtime does:
$ systemd-run --user --scope -p MemoryMax=50M sh -c 'cat /sys/fs/cgroup$(cut -d: -f3 /proc/self/cgroup)/memory.max' 52428800
| File in the cgroup | Controls | Docker flag | systemd setting |
|---|---|---|---|
memory.max | memory limit, then the OOM killer | --memory 512m | MemoryMax=512M |
cpu.max | CPU time per period | --cpus 1.5 | CPUQuota=150% |
pids.max | number of processes | --pids-limit 200 | TasksMax=200 |
memory.current | memory used now | docker stats | systemctl status |
When a cgroup hits memory.max, the kernel's OOM killer kills a process in it with SIGKILL: exit code
137 (128 + 9). docker inspect then shows "OOMKilled": true, and journalctl -k has the kill.
ulimits: limits per process
ulimits (resource limits) cap a single process, not a group. The one that bites in production is open files: every file, socket and connection uses one file descriptor.
$ ulimit -n # open-files limit for this shell 1048576 $ ulimit -a # every limit $ grep -E "processes|open files" /proc/$(pgrep -o nginx)/limits # a running process's limits Max processes 15109 15109 processes Max open files 1048576 1048576 files
grep -E "processes|open files" /proc/$(pgrep -o nginx)/limitsgrepfind lines that match a pattern
Each limit has a soft value (what applies) and a hard value (the ceiling a normal user may raise the soft one to). Where to raise them:
| For | Set it in |
|---|---|
| a systemd service | LimitNOFILE=65536 in the unit's [Service], then daemon-reload and restart |
| a container | docker run --ulimit nofile=65536:65536 … |
| user logins | /etc/security/limits.conf (deploy soft nofile 65536) |
"Too many open files" under load means this limit, not the disk. Note that limits.conf doesn't
apply to systemd services: they only read their unit file.
Container runtimes: who does what
docker run passes through several programs, each with one job. Knowing them helps when one breaks,
and it's what Kubernetes uses too:
docker (CLI) → dockerd images, networks, volumes, the API → containerd pulls images, runs and watches containers → runc creates namespaces and cgroups, starts the process, exits → your process PID 1 inside the container
- An image is a stack of read-only filesystem layers plus settings (command, environment). Running it adds a thin writable layer on top, using overlayfs: delete the container and that layer goes too. Data you keep belongs in a volume.
- OCI (the Open Container Initiative) standardises images and runtimes, so an image built with Docker runs on Podman, containerd or Kubernetes unchanged.
- Podman runs the same images without a daemon, and rootless by default;
podmanaccepts the same commands asdocker.
Docker on a Linux server
$ sudo apt install docker.io # or Docker's own repository (Packages lesson) $ sudo usermod -aG docker deploy # let deploy use docker (log in again) $ docker run -d --name web -p 8080:80 --memory 256m --cpus 1 --restart unless-stopped nginx:1.27 $ docker ps # running containers $ docker logs -f web # its stdout and stderr $ docker exec -it web sh # a shell inside its namespaces $ docker stats --no-stream # live CPU and memory per container $ docker inspect web --format '{{.State.Pid}} {{.State.OOMKilled}}'
sudo apt install docker.iosudorun the next command as rootaptDebian/Ubuntu package manager
docker run -d --name web -p 8080:80 --memory 256m --cpus 1 --restart unless-stopped nginx:1.27dockerbuild and run containersrunstart a new container-drun in the background--name webname the container-p 8080:80publish a port, host:container--memory 256mmemory limit--cpus 1CPU limit, in cores--restart unless-stoppedwhen Docker restarts itnginx:1.27the image to start
docker pspslist running containers
docker logs -f weblogsshow a container’s output-fkeep following new outputwebthe container
docker exec -it web shexecrun a command in a running container-itinteractive, with a terminalshthe command to run inside it
docker stats --no-streamstatslive CPU and memory use--no-streamprint once instead of updating
docker inspect web --format '{{.State.Pid}} {{.State.OOMKilled}}'inspectshow every detail as JSONwebthe container or image--format '{{.State.Pid}} {{.State.OOMKilled}}'print only these fields (Go template)
From the host, the container is just a process. Its PID from docker inspect works with every tool
in this course: ps -fp <pid>, cat /proc/<pid>/limits, sudo nsenter -t <pid> -n ss -tlnp (its
network, your tools).
Build a container by hand
Without Docker, start a shell that has its own hostname, its own process tree and a 50 MB memory limit. Prove each one from inside, and prove the host can still see it as an ordinary process.
A shell that is, to the kernel, a containerReveal one safe solution
$ systemd-run --user --scope -q -p MemoryMax=50M \ unshare -r -u -p -f --mount-proc sh -c ' hostname box1 echo "hostname: $(hostname)" echo "memory: $(cat /sys/fs/cgroup$(cut -d: -f3 /proc/self/cgroup)/memory.max) bytes" ps -eo pid,comm ' hostname: box1 memory: 52428800 bytes PID COMMAND 1 sh 6 ps
Three kernel features, no container software: a UTS namespace for the hostname, a PID namespace (the
shell is PID 1 and sees only itself and ps), and a cgroup capping memory at 50 MB. Docker
adds a network namespace, a mount namespace with the image as /, and the tooling to ship it, but
underneath it's this. Run hostname afterwards: the host's name never changed.
Try it yourself
0 of 6 steps doneRun each task in your own account or terminal, then tick it off.
Quick check
What is a container, from the Linux kernel's point of view?
Official reading for this lesson
Or get every quiz answer right and it completes itself.