Isolating and hardening containers: limiting CPU, memory, PIDs, and block I/O via cgroups v2, mounting a read-only rootfs, choosing the user namespace mode, deciding network isolation, and auditing container activity with podman events.

In episode 15 you controlled Podman from a distance via the REST API and podman-remote. Now attention turns back inside the machine: how one misbehaving container can consume all RAM, flood the CPU, or drain host resources. Episode 16 covers isolation & hardening — limiting resources with cgroups, making the filesystem resilient, deciding networking, and leaving an audit trail.
Control groups (cgroups) are the kernel mechanism for limiting and prioritizing resources of a group of processes. Modern Podman uses cgroups v2, a unified hierarchy that brings the CPU, memory, and I/O controllers into a single tree. When you run a container with specific limits, Podman places it in its own cgroup — so the limits are firm and can't be breached by other processes.
The most commonly used limit flags:
| Limit | Flag | Example |
|---|---|---|
| CPU (number of cores) | --cpus | --cpus=0.5 |
| CPU (relative share) | --cpu-shares | --cpu-shares=512 |
| Memory | --memory | --memory=512m |
| Swap memory | --memory-swap | --memory-swap=1g |
| Process count | --pids-limit | --pids-limit=128 |
| Block I/O weight | --blkio-weight | --blkio-weight=500 |
Combined in a single command:
podman run --cpus=0.5 --memory=512m --pids-limit=128 \
--blkio-weight=500 --name api myapp:1.2--cpus=0.5 limits to half a core; --pids-limit=128 stops processes before a fork bomb takes over the host; --blkio-weight gives a relative weight for disk access compared to other containers.
To verify the limits are really active, podman inspect shows the cgroup settings and applied resource limits:
podman inspect api --format '{{.HostConfig.CpuShares}} {{.HostConfig.Memory}}'
cat /sys/fs/cgroup/podman.slice/api.scope/cpu.max--format lets you extract specific fields from the inspect output. On the kernel side, cgroups v2 exposes the CPU limit as a quota and period pair in cpu.max — the value the controller reads at runtime.
Tip
Always limit memory and PIDs, at minimum for untrusted workloads. Unbounded memory means a busy OOM killer; unbounded PIDs means one container can flood the host with processes. Start from reasonable numbers, then calibrate with podman stats.
A healthy container shouldn't need to write to its own filesystem — data should go to volumes or tmpfs. With a read-only rootfs, one whole class of attacks (writing binaries, changing config, planting backdoors) stops dead:
podman run --read-only --tmpfs /tmp --tmpfs /run myapp:1.2--read-only mounts the rootfs as read-only; --tmpfs provides a volatile writable area for directories that genuinely need writes. Important data still flows to volumes or secrets — not into the container layer.
The isolation from episode 13 is deepened here. For public workloads, the recommended modes:
--userns=auto — automatic UID mapping, the container has no knowledge of the host identity.--userns=keep-id — when interaction with a host bind mount is required.--userns=host — avoid unless truly necessary, for example for certain drivers.Warning
The combination of --read-only with a bind mount written by the container often confuses: writes through the bind mount still work because that mount isn't part of the rootfs. If you get a read-only error, check whether the application writes to a directory like /var/lib — point it to a tmpfs or volume.
Networking is another door that needs tidying:
| Mode | Behavior | When to use |
|---|---|---|
| Bridge (default) | Container has its own IP on the netavark network | Generally |
--network=none | No network interface at all | Sensitive workloads, batch jobs |
--network=host | Shares the host network namespace | Latency optimization, needs caution |
--network=container:x | Shares the namespace with another container | Sidecar pattern |
--network=none is the safest choice for jobs that don't need outbound access. If you must connect to other services, restrict published ports and don't expose everything carelessly.
Episode 13 laid the foundation; here both become a mandatory part of the hardening checklist:
--cap-drop=ALL and only add what's needed.--security-opt no-new-privileges.--read-only to drastically narrow the attack surface.Hardening without an audit trail is hard to justify. Podman provides event and statistics streams:
podman events --since 1h
podman stats --no-stream
podman top <container> user,pid,commpodman events records events like start, stop, kill, and mount; podman stats shows resource usage live; podman top reveals the processes inside a container. Integrate all three into your logging system to keep a traceable record.
In episode 16 you isolated and hardened containers: limiting CPU, memory, PIDs, and block I/O with cgroups v2, mounting a read-only rootfs with tmpfs, choosing the right user namespace mode, deciding network isolation, closing seccomp and capabilities back down, and leaving an audit trail via events and stats.
The key points to take home:
--network=none and --userns=auto for untrusted workloads.podman events into your logging system.In the next episode, Episode 17, you'll bring containers to the production service level: advanced systemd & service lifecycle — on-demand socket activation, podman auto-update, and Quadlet as the modern way to define containers.