courses

Containerization

Isolation built from kernel features, not a VM

Linux containers are isolated environments built from kernel features — primarily namespaces and cgroups — rather than full virtual machines. This page covers the Linux primitives that make containers possible, container runtimes, and Docker.

⚠️ The security takeaway up front: a container shares the host kernel. The isolation is namespaces (what a process can see) plus cgroups (what it can use) — not a hardware boundary. A kernel exploit or a misconfigured container (--privileged, host network, root user, the Docker socket mounted in) can cross it. Treat container isolation as a strong default, not an impenetrable wall.

ulimits

ulimits (user limits) constrain the resources available to a process and its children. They protect the system from runaway processes or resource exhaustion.

Types of limits

Each resource has a soft limit (current enforced limit, can be raised by the user up to the hard limit) and a hard limit (ceiling; only root can raise it).

ulimit — shell built-in

ulimit -a               # show all current limits
ulimit -aH              # show all hard limits
ulimit -aS              # show all soft limits

# Common limits
ulimit -n               # max open file descriptors (default often 1024)
ulimit -u               # max user processes
ulimit -m               # max memory (KB)
ulimit -s               # max stack size (KB)
ulimit -t               # max CPU time (seconds)
ulimit -f               # max file size (KB)
ulimit -c               # max core dump size (0 = no core dumps)

# Set a limit (for current shell and children)
ulimit -n 65536         # increase open file descriptors to 64K
ulimit -c unlimited     # allow unlimited core dumps

Persistent limits — /etc/security/limits.conf

# /etc/security/limits.conf format:
# domain  type    item    value
*         soft    nofile  65536
*         hard    nofile  131072
alice     soft    nproc   2048
alice     hard    nproc   4096
@devs     soft    memlock unlimited

Files in /etc/security/limits.d/ take precedence over the main file. These take effect for new PAM sessions (login, SSH).

cat /etc/security/limits.conf       # main limits config
ls /etc/security/limits.d/          # per-application overrides

Viewing limits for a running process

cat /proc/<PID>/limits              # all limits for a specific process
# Example:
cat /proc/1/limits                  # limits for PID 1 (systemd)

systemd service limits

Services managed by systemd can have limits set in their unit files:

[Service]
LimitNOFILE=65536
LimitNPROC=4096
LimitMEMLOCK=infinity

Worked example — fix “too many open files” error:

# Check current limit
ulimit -n

# Raise for this session
ulimit -n 65536

# Check what a process is using
lsof -p $(pgrep nginx) | wc -l

# Set permanently in /etc/security/limits.d/nginx.conf:
# nginx soft nofile 65536
# nginx hard nofile 131072

cgroups

Control Groups (cgroups) are a Linux kernel feature that limits, measures, and isolates the resource usage (CPU, memory, disk I/O, network) of groups of processes. They are the foundation of containers.

cgroups v1 vs v2

Most modern distributions use cgroups v2 (unified hierarchy). cgroups v1 used separate hierarchies per controller.

# Check which version is in use
mount | grep cgroup
ls /sys/fs/cgroup/                  # cgroupfs mount point
cat /proc/cgroups                   # v1: list of controllers and their state

cgroups v2 structure

In cgroups v2, the hierarchy is a single tree under /sys/fs/cgroup/:

ls /sys/fs/cgroup/                  # root cgroup
cat /sys/fs/cgroup/cgroup.controllers  # available controllers
cat /sys/fs/cgroup/cgroup.subtree_control  # enabled controllers

# Create a cgroup
mkdir /sys/fs/cgroup/mygroup
echo "+memory +cpu" > /sys/fs/cgroup/cgroup.subtree_control

# Move a process into a cgroup
echo <PID> > /sys/fs/cgroup/mygroup/cgroup.procs

# Set memory limit (200 MB)
echo $((200 * 1024 * 1024)) > /sys/fs/cgroup/mygroup/memory.max

# Set CPU weight (relative scheduling weight, default 100)
echo 50 > /sys/fs/cgroup/mygroup/cpu.weight

# Set CPU quota: 20ms out of every 100ms (20% of one CPU)
echo "20000 100000" > /sys/fs/cgroup/mygroup/cpu.max

Viewing cgroup membership

# See which cgroup a process belongs to
cat /proc/<PID>/cgroup

# systemd assigns each service to a cgroup
systemctl status nginx              # shows cgroup path
systemd-cgtop                       # live resource usage by cgroup

systemd and cgroups

systemd uses cgroups to manage service isolation. Each service runs in its own cgroup slice:

systemd-cgls                        # cgroup tree
systemd-cgtop                       # live resource view
systemctl set-property nginx.service MemoryMax=512M   # set limit
systemctl set-property nginx.service CPUQuota=50%     # CPU quota

Container Runtime

A container runtime creates and manages containers. It sets up namespaces (isolation), cgroups (resource limits), and mounts to provide a contained environment.

Linux namespaces

Namespaces are the isolation mechanism. Each namespace type isolates a different resource:

Namespace Flag Isolates
mnt CLONE_NEWNS Filesystem mount points
pid CLONE_NEWPID Process IDs (PID 1 inside container)
net CLONE_NEWNET Network interfaces, routes, ports
uts CLONE_NEWUTS Hostname and domain name
ipc CLONE_NEWIPC SysV IPC, POSIX message queues
user CLONE_NEWUSER User and group IDs
cgroup CLONE_NEWCGROUP cgroup root
time CLONE_NEWTIME Boot and monotonic clocks
# View namespaces for a process
ls -la /proc/<PID>/ns/

# Run a command in a new network namespace
ip netns add testns
ip netns exec testns bash
# Inside: own lo interface, no external network by default
ip netns del testns

# Inspect namespaces with lsns
lsns                                # list all namespaces
lsns -t net                         # network namespaces only

OCI and container runtimes

The Open Container Initiative (OCI) defines standards for container images and runtimes.

Runtime Description
runc Reference OCI runtime; used by Docker and containerd
containerd High-level runtime; manages images, snapshots, and networking
crun Lightweight OCI runtime written in C
kata-runtime Lightweight VMs instead of pure Linux namespaces
podman Daemonless container engine; Docker-compatible CLI
# runc directly
runc list                           # list running containers
runc state <container-id>           # inspect container state

# containerd
ctr images list                     # list images
ctr containers list                 # list containers
nerdctl run -it ubuntu bash         # Docker-compatible CLI for containerd

Docker

Docker is the most widely used container platform. It packages the container runtime (containerd + runc), image management, networking, and volumes into an integrated system.

Docker architecture

Images

docker pull ubuntu:22.04            # download an image
docker pull nginx:latest            # latest nginx
docker images                       # list local images
docker image ls                     # same
docker rmi nginx                    # remove an image
docker image prune                  # remove unused images
docker inspect ubuntu:22.04         # detailed image metadata
docker history nginx                # show image layers
docker search nginx                 # search Docker Hub

Running containers

# Basic run
docker run ubuntu echo "hello"      # run and exit
docker run -it ubuntu bash          # interactive terminal
docker run -d nginx                 # detached (background)
docker run --name webserver nginx   # named container
docker run --rm ubuntu echo "temp"  # auto-remove when done

# Resource limits
docker run -m 512m nginx            # limit to 512 MB memory
docker run --cpus="1.5" nginx       # limit to 1.5 CPUs

# Port mapping
docker run -p 8080:80 nginx         # host:8080 → container:80
docker run -p 127.0.0.1:8080:80 nginx  # bind to localhost only

# Volume mounts
docker run -v /host/path:/container/path nginx   # bind mount
docker run -v myvolume:/data nginx               # named volume

# Environment variables
docker run -e MYSQL_ROOT_PASSWORD=secret mysql
docker run --env-file .env myapp

# Network
docker run --network mynet nginx    # connect to named network
docker run --network host nginx     # use host network (no isolation)

Managing running containers

docker ps                           # list running containers
docker ps -a                        # all containers (including stopped)
docker stop webserver               # graceful stop (SIGTERM)
docker kill webserver               # immediate stop (SIGKILL)
docker rm webserver                 # remove stopped container
docker rm -f webserver              # force remove running container
docker restart webserver            # restart

docker exec -it webserver bash      # shell in running container
docker exec webserver nginx -t      # run command in container
docker logs webserver               # view container logs
docker logs -f webserver            # follow logs
docker logs --tail 50 webserver     # last 50 lines
docker stats                        # live resource usage for all containers
docker top webserver                # processes in a container
docker inspect webserver            # detailed container metadata

Building images with Dockerfile

# Example Dockerfile
FROM ubuntu:22.04

# Install dependencies
RUN apt-get update && apt-get install -y \
    python3 \
    python3-pip \
    && rm -rf /var/lib/apt/lists/*

# Set working directory
WORKDIR /app

# Copy application files
COPY requirements.txt .
RUN pip3 install -r requirements.txt
COPY . .

# Create non-root user
RUN useradd -m appuser
USER appuser

# Expose port
EXPOSE 8000

# Default command
CMD ["python3", "app.py"]
docker build -t myapp:1.0 .             # build image from Dockerfile
docker build -t myapp:1.0 -f Dockerfile.prod .  # use specific Dockerfile
docker build --no-cache -t myapp .      # force rebuild all layers
docker tag myapp:1.0 registry/myapp:1.0  # tag for registry
docker push registry/myapp:1.0          # push to registry

Docker networks and volumes

# Networks
docker network ls                       # list networks
docker network create mynet             # create bridge network
docker network inspect mynet            # inspect network
docker network connect mynet webserver  # connect container to network

# Volumes
docker volume ls                        # list volumes
docker volume create mydata             # create named volume
docker volume inspect mydata            # inspect volume
docker volume rm mydata                 # remove volume
docker volume prune                     # remove all unused volumes

Docker Compose

Docker Compose defines multi-container applications in a compose.yaml file:

services:
  web:
    image: nginx:latest
    ports:
      - "8080:80"
    volumes:
      - ./html:/usr/share/nginx/html
    depends_on:
      - app

  app:
    build: .
    environment:
      - DATABASE_URL=postgresql://db/mydb
    depends_on:
      - db

  db:
    image: postgres:15
    volumes:
      - pgdata:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB=mydb
      - POSTGRES_PASSWORD=secret

volumes:
  pgdata:
docker compose up -d                # start all services in background
docker compose up --build           # rebuild images before starting
docker compose down                 # stop and remove containers
docker compose down -v              # also remove volumes
docker compose ps                   # list service containers
docker compose logs -f              # follow all service logs
docker compose logs -f web          # follow web service logs
docker compose exec app bash        # shell in app container
docker compose restart web          # restart a service

Cleanup

docker system prune                 # remove stopped containers, unused images
docker system prune -a              # also remove all unused images
docker system prune -af             # force, no prompt
docker system df                    # disk usage by Docker

Key takeaways

References


Related course pages: DevSecOps Fundamentals · Vulnerability Management · SIEM, SOC, and Threat Detection

🛠️ Maintenance note: the tagged image versions in the examples (ubuntu:22.04, postgres:15, etc.) age out — bump them to current LTS/stable each term (Ubuntu 24.04 is current). Docker Compose v2 (docker compose, compose.yaml) has fully replaced the legacy docker-compose/version: key. NIST SP 800-190 predates cgroups v2’s ubiquity, so pair it with current Docker/Kubernetes hardening guidance.