DevOps.Academy
CoursesCurriculumPricing

    Loading…

    ↑ ↓ move · ↵ open · Ctrl K anywhere

    ◔My learningSign in
    DevOps.Academy

    Free, hands-on DevOps lessons. Made with care in India.

    Learn

    All coursesLinux roadmapCurriculumMy learning

    Academy

    PricingCertificates

    Legal

    PrivacyTermsContact

    devops.rajeev.pro

    Linux & BashLesson 16 of 16
    Your progress · 0%
    On this page
    1. A container is a process
    2. Namespaces: what a process can see
    3. cgroups: what a process can use
    4. ulimits: limits per process
    5. Container runtimes: who does what
    6. Docker on a Linux server
    ↖ Course roadmap
    Lesson 16 · Containers on Linux

    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.

    40 min read · intermediate · hands-on

    By the end of this lesson you'll be able to

    0 of 6 complete

    A 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:

    Namespaces: what it can see. cgroups: what it can use. One shared kernel underneath.© Diagram: DevOps Academy

    Namespaces: what it can see. cgroups: what it can use. One shared kernel underneath.© Diagram: DevOps Academy
    Virtual machineContainer
    Kernelits ownthe host's, shared
    Starts intens of secondsmilliseconds
    Isolationhardware-levelkernel features: strong, but one kernel
    Seen from the hostone qemu processthe 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:

    NamespaceGives the process its ownContainer effect
    pidprocess numbersits app is PID 1, it can't see host processes
    netinterfaces, IPs, ports, routesits own eth0 and localhost
    mntmounted filesystemssees the image's files as /
    utshostnamehostname shows the container ID
    useruser and group IDsroot inside can be a normal user outside
    ipc, cgroup, timeshared 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/ns
    • lslist 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/cgroup
    • catprint a file
    systemd-cgls --no-pager | head
    • |pipe: send output to the next command
    • headshow 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 cgroupControlsDocker flagsystemd setting
    memory.maxmemory limit, then the OOM killer--memory 512mMemoryMax=512M
    cpu.maxCPU time per period--cpus 1.5CPUQuota=150%
    pids.maxnumber of processes--pids-limit 200TasksMax=200
    memory.currentmemory used nowdocker statssystemctl 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)/limits
    • grepfind 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:

    ForSet it in
    a systemd serviceLimitNOFILE=65536 in the unit's [Service], then daemon-reload and restart
    a containerdocker 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; podman accepts the same commands as docker.

    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.io
    • sudorun the next command as root
    • aptDebian/Ubuntu package manager
    docker run -d --name web -p 8080:80 --memory 256m --cpus 1 --restart unless-stopped nginx:1.27
    • dockerbuild and run containers
    • runstart 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 it
    • nginx:1.27the image to start
    docker ps
    • pslist running containers
    docker logs -f web
    • logsshow a container’s output
    • -fkeep following new output
    • webthe container
    docker exec -it web sh
    • execrun a command in a running container
    • -itinteractive, with a terminal
    • shthe command to run inside it
    docker stats --no-stream
    • statslive CPU and memory use
    • --no-streamprint once instead of updating
    docker inspect web --format '{{.State.Pid}} {{.State.OOMKilled}}'
    • inspectshow every detail as JSON
    • webthe 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).

    The docker group is root

    Anyone in the docker group can start a container that mounts the host's / and change any file on it. Treat membership like sudo. For untrusted workloads use rootless Docker or Podman.

    In production

    Always set a memory limit: one leaking container without one can take the whole host's memory. Pin image versions (nginx:1.27, not latest), run as a non-root user inside the image, log to stdout so docker logs and your log shipper see it, and keep data in volumes, never in the container.

    ⌘
    Mini mission

    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.

    Input: Any Linux machine with systemd and cgroups v2 (no root needed)Output: A shell that is, to the kernel, a container
    Reveal 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 done

    Run each task in your own account or terminal, then tick it off.

    Knowledge check

    Quick check

    Question 1 of 5

    What is a container, from the Linux kernel's point of view?

    Go deeper

    Official reading for this lesson

    • Docsnamespaces(7) manualUbuntu manpages ↗
    • DocsControl Group v2: the kernel documentationLinux kernel ↗
    • DocsDocker: resource constraintsDocker ↗
    • ArticleOpen Container Initiative: runtime and image specsOCI ↗
    • Docsgetrlimit(2): resource limitsUbuntu manpages ↗

    Or get every quiz answer right and it completes itself.

    ← PreviousTroubleshooting the network: from "it is down" to the causeUp nextBack to the course roadmap