Defence in depth turns a container compromise into a contained incident instead of a host takeover.
Root in a container is the host's UID 0 (unless user namespaces are used), restricted by several layers. Knowing them lets you tighten each.
Capabilities split root's power into units. Docker grants a reduced default set (including CHOWN, DAC_OVERRIDE, SETUID, SETGID, NET_BIND_SERVICE, NET_RAW, KILL) and withholds dangerous ones like SYS_ADMIN, SYS_MODULE and NET_ADMIN. Best practice: --cap-drop ALL, then --cap-add only what's needed.
seccomp filters system calls. Docker's default profile blocks dozens of risky syscalls (kernel module loading, reboot, kexec_load, mount without the right capability...) while allowing what normal apps use. Custom profiles go in with --security-opt seccomp=profile.json. Never use seccomp=unconfined casually.
`--security-opt no-new-privileges` stops setuid binaries and file capabilities from granting more privilege than the process started with.
LSMs: AppArmor (docker-default profile) or SELinux labels add mandatory access control on files and mounts.
`--privileged` turns nearly all of this off: all capabilities, all devices, no seccomp. It is effectively host root, so avoid it outside of special cases such as Docker-in-Docker or hardware access.
User namespaces remap container root to an unprivileged host UID. userns-remap in daemon.json does this for all containers. Rootless mode goes further: the daemon itself runs as a normal user (dockerd-rootless-setuptool.sh install), using subordinate UID ranges from /etc/subuid and RootlessKit for networking. A full escape then yields only that user's rights. Trade-offs: binding ports below 1024 needs a sysctl change, and some networking and storage features are limited.
Combine these with a non-root USER and a read-only root filesystem for strong defaults.
Example
docker run -d --name api \
--user 10001:10001 \
--read-only --tmpfs /tmp \
--cap-drop ALL --cap-add NET_BIND_SERVICE \
--security-opt no-new-privileges \
-p 80:80 acme/api:1.4.2
docker inspect api --format '{{.HostConfig.CapDrop}} {{.HostConfig.SecurityOpt}}'
docker run --rm alpine grep Cap /proc/self/status # default capability bitmasks
# Rootless Docker for the current user
dockerd-rootless-setuptool.sh install
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock
docker info --format '{{.SecurityOptions}}' # [name=seccomp,... name=rootless ...]Remember
- Drop all capabilities and add back only what's needed
- The default seccomp profile blocks risky syscalls; don't run unconfined
- --privileged disables almost all isolation
- userns-remap or rootless mode map container root to an unprivileged host user
Watch out for
- Reaching for --privileged to fix a permission error instead of adding the single capability or device actually required.