Applies to: Docker Engine 29 (tested with 29.1.3 on Linux arm64) and Docker Compose v2.40
docker run --privileged switches off almost all of Docker’s isolation. The container gets every Linux capability and every host device, so a process inside it can take over the host. You rarely need it. Most containers that fail with Operation not permitted need one specific permission, and you can grant only that with --cap-add, --device or --sysctl.
What --privileged changes
A normal container runs as root inside, but that root is limited. Docker keeps 14 Linux capabilities (such as CHOWN, KILL, NET_BIND_SERVICE and SETUID) and drops the rest, including NET_ADMIN (network configuration) and SYS_ADMIN (mounting file systems and much more). A seccomp filter blocks risky system calls, and host devices are not visible.
According to Docker’s docker run reference, --privileged:
- enables all Linux kernel capabilities;
- disables the default seccomp profile and the default AppArmor profile;
- disables the SELinux process label;
- grants access to all host devices;
- makes
/sysand the cgroup mounts read-write.
Docker warns that such a container “is not a securely sandboxed process” and “can get a root shell on the host and take control over the system”. If a privileged web app or a downloaded image is compromised, the whole server is compromised with it.
The errors that make people reach for it
In a default container, these three commands fail:
$ docker run --rm alpine:3 ip link add dummy0 type dummy ip: RTNETLINK answers: Operation not permitted $ docker run --rm ubuntu:26.04 mount -t tmpfs none /mnt mount: /mnt: permission denied. $ docker run --rm alpine:3 sysctl -w net.ipv4.ip_forward=1 sysctl: error setting key 'net.ipv4.ip_forward': Read-only file system
Each one has a narrower fix than --privileged.
Grant only what the container needs
Network changes (a VPN client, WireGuard, iptables inside the container) need NET_ADMIN:
$ docker run --rm --cap-add NET_ADMIN alpine:3 sh -c 'ip link add dummy0 type dummy && ip link show dummy0' 12: dummy0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN qlen 1000
A VPN that creates a tun interface also needs that one device: add --device /dev/net/tun.
Kernel settings for the container’s own network, such as IP forwarding, are set when it starts:
$ docker run --rm --sysctl net.ipv4.ip_forward=1 alpine:3 sysctl net.ipv4.ip_forward net.ipv4.ip_forward = 1
Hardware such as a USB serial adapter: pass only that device, for example --device /dev/ttyUSB0. For NVIDIA GPUs use --gpus all with the NVIDIA Container Toolkit, see use your NVIDIA GPU in Docker.
Mounting file systems needs SYS_ADMIN, and according to Docker, FUSE file systems need --cap-add SYS_ADMIN --device /dev/fuse. Be careful: the Linux capabilities manual calls SYS_ADMIN “overloaded”, because it allows many unrelated administrative actions. On our test host, which has no AppArmor, --cap-add SYS_ADMIN was enough for the tmpfs mount above. On Ubuntu and Debian servers, Docker’s docker-default AppArmor profile contains a deny mount rule, so the mount can still fail there. Usually the better answer is to mount the disk or share on the host and pass it in as a bind mount (-v /mnt/data:/data).
Check and reduce a container’s capabilities
From inside a container, read the capability masks and decode them with capsh (package libcap on Alpine, libcap2-bin on Debian and Ubuntu):
$ grep CapEff /proc/self/status CapEff: 00000000a80425fb $ capsh --decode=00000000a80425fb 0x00000000a80425fb=cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap
That is the default set. You can go further and drop everything, then add back only what the app uses:
$ docker run --rm --cap-drop ALL alpine:3 chown nobody /tmp chown: /tmp: Operation not permitted $ docker run --rm --cap-drop ALL --cap-add CHOWN alpine:3 chown nobody /tmp $
Add --security-opt no-new-privileges as well, so programs inside cannot gain extra privileges through setuid binaries (NoNewPrivs: 1 in /proc/self/status confirms it).
The same in Docker Compose
Instead of privileged: true, list the exact permissions:
services:
vpn:
image: alpine:3
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
sysctls:
net.ipv4.ip_forward: 1
security_opt:
- no-new-privileges:true
docker compose config checks the file and prints how Compose reads it. New to Compose? See Docker Compose basics.
Find privileged containers on a server
docker inspect -f '{{.HostConfig.Privileged}}' my-container
docker ps -q | xargs docker inspect --format '{{.Name}} privileged={{.HostConfig.Privileged}} cap_add={{.HostConfig.CapAdd}}'
/vpn privileged=false cap_add=[CAP_NET_ADMIN] /web privileged=false cap_add=[]
Check Compose projects too, changing /opt to wherever you keep them:
$ grep -rn "privileged" --include="*.y*ml" /opt /opt/app/ci.yml:4: privileged: true
When --privileged is really needed
Docker names running Docker inside Docker as the main use. The official docker:dind image is built for privileged mode: its dind script says it “should be executed inside a docker container in privileged mode”. Low-level hardware and kernel tools can need it too. If you must use it, run only images you trust, keep the container off the public internet, and ideally give it a server or VPS of its own. Mounting /var/run/docker.sock into a container is not a safe alternative: Docker’s security documentation says only trusted users should control the Docker daemon, because it can mount the host’s root file system into a container.
On a KVM VPS you have your own kernel and full root access, so you decide these settings yourself. To install Docker first, see install Docker on Debian and Ubuntu.
Official documentation: docker run --privileged and runtime privilege and Linux capabilities.