Securing Docker on a Linux Machine
Docker group access grants root-level control. See how to secure Docker on Linux with rootless mode, sudo rules, Podman, container hardening, and an audit checklist.
Adding a user to the docker group gives that user root on the machine: not “almost root”, but full root, because anyone who can talk to the Docker socket can mount the host filesystem into a container and edit it.
You ran usermod -aG docker $USER so you could stop typing sudo, and Docker’s own post-install guide told you to do it. The same Linux post-installation page for Docker Engine also warns, in a box most people skim past, that membership in this group is equal to root. If you’re still getting comfortable with the basics, the beginner guide to Docker images and containers covers them. This article proves the risk with one command. It then walks through the fixes in order of strength and ends with a checklist for auditing a machine you inherited.
Key Takeaways
- Any member of the
dockergroup, and any process running as that member, can get a root shell on the host withdocker run --rm -it -v /:/host alpine chroot /host sh. - Rootless Docker runs both
dockerdand your containers inside a user namespace, so a daemon compromise or container escape lands on an unprivileged host UID instead of root. - A narrowed sudoers rule for
/usr/bin/dockeris still root-equivalent, but it puts a password in front of Docker and logs every use. - Podman has no daemon and runs rootless by default when you use it as a normal user, and its CLI is mostly compatible with Docker’s, so
alias docker=podmancovers most daily workflows. - On an inherited host, check who is in the
dockergroup, which user ownsdockerd, the socket permissions, and whether the daemon listens on TCP port 2375.
Why Does the Docker Group Grant Root?
The docker group is root-equivalent because the Docker daemon runs as root and the group controls write access to its Unix socket. Being able to write to that socket means being able to tell a root process to do anything. If you want the underlying mechanics, the Linux file permissions guide explains how a file’s group permissions decide who can open it. For Docker, the only detail that matters is that the socket is group-writable by docker.
Here is the proof:
docker run --rm -it -v /:/host alpine chroot /host sh
This bind-mounts the host’s / into the container and then chroots into it. You now have a root shell on the real host filesystem. From there you can edit /etc/shadow or /etc/sudoers, or add SSH keys to /root. You don’t need a password or an exploit. Any script, dependency or compromised editor extension running under your user can do the same thing. Docker’s documentation covers this under the Docker daemon attack surface.
Set Up Rootless Docker
Rootless Docker is a mode that runs dockerd and every container inside a user namespace owned by your account, which makes it the direct fix for the docker group problem. The socket sits at $XDG_RUNTIME_DIR/docker.sock rather than /var/run/docker.sock. RootlessKit creates the namespace. The setuid helpers newuidmap and newgidmap map UID 0 in the namespace to your own UID, and they map a range from /etc/subuid and /etc/subgid for the other container UIDs. Networking runs through a user-mode network stack such as slirp4netns, gvisor-tap-vsock or pasta.
Install the prerequisites, then check your subordinate ID ranges. The rootless mode docs ask for a range of at least 65,536 subordinate IDs for your user in both /etc/subuid and /etc/subgid.
sudo apt-get install -y uidmap docker-ce-rootless-extras
grep ^$(whoami): /etc/subuid /etc/subgid
Disable the root daemon, then run the setup tool as your normal user:
sudo systemctl disable --now docker.service docker.socket
dockerd-rootless-setuptool.sh install
systemctl --user enable --now docker
sudo loginctl enable-linger $USER
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock
The enable-linger line keeps your daemon running after you log out. Once rootless Docker works, remove yourself from the old group with sudo gpasswd -d $USER docker. The official rootless mode docs cover other distributions. If the client can’t reach the daemon afterwards, troubleshooting “Docker daemon not running” errors walks through socket and systemd checks.
Rootless mode needs unprivileged user namespaces. Ubuntu 24.04 and later restrict them through AppArmor by default. The apparmor package on Ubuntu already ships the profile rootlesskit needs, so an apt install of docker-ce-rootless-extras works with no extra steps. If you install with the get.docker.com/rootless script instead, you have to add that profile yourself. Don’t disable the restriction system-wide with kernel.apparmor_restrict_unprivileged_userns=0. The rootless troubleshooting page covers the other failure cases.
What Does Rootless Mode Cost?
These are the main trade-offs compared with a rootful daemon:
| Limitation | Workaround |
|---|---|
| Ports below 1024 cannot be bound by default | sysctl net.ipv4.ip_unprivileged_port_start=80, or a reverse proxy |
--privileged grants no real host privileges | Only capabilities inside the user namespace apply |
| User-mode networking is slower than the bridge driver | The experimental lxc-user-nic driver, or benchmark before production |
| Kernels before 5.11 need fuse-overlayfs | Kernel 5.11+ supports native overlay2 |
The troubleshooting page lists a few more, such as no AppArmor, no checkpoints and no overlay networks.
Use sudo With a Narrowed Rule
Requiring sudo for Docker instead of docker group membership is the middle ground when rootless mode breaks your workload. It is still root-equivalent, because sudo docker can run the same chroot command. What you gain is a password prompt and an audit log entry for every call. Code that runs silently as your user no longer has the socket available.
sudo gpasswd -d alice docker
sudo visudo -f /etc/sudoers.d/docker
alice ALL=(root) /usr/bin/docker
This rule allows only the Docker binary, not arbitrary commands. Review usage with journalctl _COMM=sudo. The post-install docs also describe a newgrp method that puts a group password in front of Docker while the CLI still runs as your own user.
Switch to Podman
Podman has no daemon, and when you run it as a normal user it is rootless by default. Containers run as child processes of your user, inside a user namespace, using the same /etc/subuid mappings. Its CLI mirrors Docker’s closely enough that most commands work unchanged.
sudo apt-get install -y podman
alias docker=podman
podman run --rm -p 8080:80 docker.io/library/nginx
The differences are at the edges: fully qualified image names, Compose support, and tools that expect a Docker socket. Test your scripts before you switch a team over.
Harden the Container Itself
Whichever daemon you run, limit what the process inside the container can do. Four settings cover most of the risk: a non-root USER, no capabilities beyond the ones the app needs, a read-only root filesystem, and no privilege escalation through setuid binaries.
FROM node:22-alpine
WORKDIR /app
COPY --chown=node:node . .
USER node
CMD ["node", "server.js"]
docker run -d \
--cap-drop=ALL \
--read-only --tmpfs /tmp \
--security-opt no-new-privileges \
-p 8080:8080 myapp
The Dockerfile USER instruction sets the runtime UID. --cap-drop=ALL removes every capability. If an app truly needs one back, add it with --cap-add. This app listens on port 8080, so it needs none. --read-only combined with --tmpfs means only /tmp is writable. no-new-privileges stops setuid binaries from raising privileges. All of these are documented in the docker run reference.
Audit an Inherited Machine
On a VPS someone else set up, answer four questions: who is in the docker group, which user runs dockerd, who can write to the socket, and whether the daemon listens on the network.
getent group docker
ps -eo user,cmd | grep [d]ockerd
ls -l /var/run/docker.sock
sudo ss -ltnp | grep -E ':2375|:2376'
systemctl cat docker | grep -- '-H'
grep -s hosts /etc/docker/daemon.json
Every name getent prints has root access. If ps shows root, the daemon is rootful. The socket should be srw-rw---- root docker, and any world-writable bit is an immediate fix. A listener on port 2375 means the Docker API is open without TLS. Current Docker Engine releases refuse to start with that setup, but an older engine on an inherited host may still run it. Anyone who can reach that port has root. A TLS listener on port 2376 is safer, but anyone who holds the client keys still has root. Follow the guide to protecting the daemon socket to switch to SSH or mutual TLS. Also check for containers that mount the socket, since each one is a root handle:
docker ps -q | xargs -r docker inspect \
--format '{{.Name}} {{range .Mounts}}{{.Source}} {{end}}' | grep docker.sock
Access to the Docker socket is root access, full stop. Start by running getent group docker on each machine you use. Then either move those users to rootless Docker or Podman, or put the socket behind a logged sudo rule.
FAQs
What is the difference between rootless Docker and userns-remap?
With userns-remap, dockerd still runs as root and only the containers run under remapped subordinate UIDs, such as the dockremap user created by setting userns-remap to 'default' in /etc/docker/daemon.json. It does not close the docker group hole: a group member can pass --userns=host to turn remapping off for one container and run the chroot escape. Rootless mode runs the daemon itself as your user.
Can rootless and rootful Docker run on the same machine?
Yes. The two daemons use separate sockets: /var/run/docker.sock for rootful and $XDG_RUNTIME_DIR/docker.sock for rootless. Running dockerd-rootless-setuptool.sh install creates a CLI context named 'rootless' and switches to it. Use docker context use default to target the root daemon and docker context use rootless to switch back. While the rootful daemon keeps running, anyone who can write to its socket still has root.
Do my existing images and containers carry over to rootless Docker?
No. The rootless daemon keeps its own data root, ~/.local/share/docker by default, separate from the /var/lib/docker directory the root daemon uses. Images, containers and volumes created under rootful Docker are not visible to it. Pull the images again, or export them with docker save from the rootful daemon and import them with docker load into the rootless one. Update any backup jobs that only cover /var/lib/docker.
Why do files written by rootless containers show odd owners on the host?
Rootless Docker maps container UIDs to host UIDs. Root inside the container maps to your own UID, so files it writes to a bind mount show up as yours. Every other container UID maps into your /etc/subuid range. With a range that starts at 100000, container UID 1000 becomes host UID 100999, which your user cannot edit directly. Running chown to 0:0 inside a container gives the files back to your user.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k