In a standard installation the Docker daemon runs as root, and anyone who can talk to its socket effectively has root on the host. Rootless mode runs the daemon and every container inside a user namespace owned by a normal user, so a compromised container or daemon only gets that user's privileges. In this tutorial you will enable Docker rootless mode on Ubuntu 24.04, run it as a systemd user service that starts at boot, and configure resource limits, low ports and client IP handling.
Prerequisites
To follow this guide you need:
- A server running Ubuntu 24.04 LTS, for example a CubePath VPS.
- Docker Engine installed from Docker's official apt repository (packages
docker-ce,docker-ce-clianddocker-ce-rootless-extras). The Ubuntudocker.iopackage does not ship the rootless tools. - A non-root user with
sudoprivileges that will own the rootless daemon. Log in as this user directly over SSH. Shells opened withsuorsudo -udo not get a systemd user session, and the setup will fail.
Check that the rootless setup tool is present:
which dockerd-rootless-setuptool.sh
/usr/bin/dockerd-rootless-setuptool.sh
If nothing is printed, install it with sudo apt install docker-ce-rootless-extras.
Step 1 - Installing the required packages
Rootless mode needs newuidmap and newgidmap (package uidmap) to map UIDs into the user namespace, a D-Bus user session for systemd, and slirp4netns for user-mode networking:
sudo apt update
sudo apt install -y uidmap dbus-user-session slirp4netns
Your user also needs a range of subordinate UIDs and GIDs. Ubuntu creates one automatically for users added with adduser:
grep "^$USER:" /etc/subuid /etc/subgid
/etc/subuid:your_user:100000:65536
/etc/subgid:your_user:100000:65536
If the command prints nothing, add a range of at least 65,536 IDs that does not overlap with other entries, replacing your_user with your user name:
sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 your_user
If you installed dbus-user-session just now, log out and back in so your session picks it up.
Step 2 - Allowing user namespaces in AppArmor
Ubuntu 24.04 restricts unprivileged user namespaces through AppArmor, which blocks RootlessKit, the component that creates the rootless daemon's namespaces. Add an AppArmor profile that allows it:
sudo nano /etc/apparmor.d/usr.bin.rootlesskit
abi <abi/4.0>,
include <tunables/global>
/usr/bin/rootlesskit flags=(unconfined) {
userns,
# Site-specific additions and overrides. See local/README for details.
include if exists <local/usr.bin.rootlesskit>
}
Save the file and reload AppArmor:
sudo systemctl restart apparmor.service
Confirm the profile is loaded:
sudo aa-status | grep rootlesskit
/usr/bin/rootlesskit
Step 3 - Disabling the system-wide daemon
You can keep the rootful daemon running next to the rootless one, but on a server where the goal is to remove root access through Docker, disable it:
sudo systemctl disable --now docker.service docker.socket
sudo rm -f /var/run/docker.sock
Existing images and containers of the rootful daemon stay in /var/lib/docker and are not visible to the rootless daemon. Remove your user from the docker group as well, since that group only grants access to the rootful socket:
sudo gpasswd -d $USER docker
Step 4 - Installing the rootless daemon
Run the setup tool as your normal user, without sudo:
dockerd-rootless-setuptool.sh install
The tool checks the prerequisites, creates a systemd user unit at ~/.config/systemd/user/docker.service, starts it and creates a Docker context called rootless:
[INFO] Creating /home/your_user/.config/systemd/user/docker.service
[INFO] starting systemd service docker.service
...
[INFO] Installed docker.service successfully.
[INFO] To control docker.service, run: `systemctl --user (start|stop|restart) docker.service`
[INFO] To run docker.service on system (re)boot, run: `sudo loginctl enable-linger your_user`
[INFO] Creating CLI context "rootless"
[INFO] Using CLI context "rootless"
If the tool stops with an error about missing packages or AppArmor, fix the reported item and run it again.
The CLI now uses the rootless context. Some tools read DOCKER_HOST instead of contexts, so export it too by adding this line to ~/.bashrc:
echo 'export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock' >> ~/.bashrc
source ~/.bashrc
Step 5 - Starting Docker at boot
User services normally stop when the user logs out. Enable lingering so your user manager, and the rootless daemon with it, starts at boot without a login:
sudo loginctl enable-linger $USER
systemctl --user enable docker.service
Check the service:
systemctl --user status docker.service
● docker.service - Docker Application Container Engine (Rootless)
Loaded: loaded (/home/your_user/.config/systemd/user/docker.service; enabled; preset: enabled)
Active: active (running) since ...
Daemon logs are available with journalctl --user -u docker.service.
Step 6 - Verifying that Docker runs rootless
Ask the daemon for its security options:
docker info --format '{{.SecurityOptions}}'
[name=seccomp,profile=builtin name=rootless name=cgroupns]
name=rootless confirms the daemon runs in rootless mode. Run a container and compare the process owner on the host:
docker run -d --name web -p 8080:80 nginx:alpine
curl -I http://localhost:8080
ps -o user,pid,cmd -C nginx
HTTP/1.1 200 OK
USER PID CMD
your_user 12345 nginx: master process nginx -g daemon off;
100100 12380 nginx: worker process
The Nginx master process, which is root inside the container, belongs to your user on the host, and the workers run under a subordinate UID. The rootless daemon stores its data in ~/.local/share/docker and reads its configuration from ~/.config/docker/daemon.json instead of /etc/docker/daemon.json.
Step 7 - Enabling CPU and I/O limits
Ubuntu 24.04 uses cgroup v2, but systemd only delegates the memory and pids controllers to user sessions by default. Without delegation, flags such as --cpus and --device-read-bps have no effect in rootless mode. Check which controllers your user has:
cat /sys/fs/cgroup/user.slice/user-$(id -u).slice/user@$(id -u).service/cgroup.controllers
memory pids
Create a drop-in that delegates cpu, cpuset and io as well:
sudo mkdir -p /etc/systemd/system/[email protected]
sudo nano /etc/systemd/system/[email protected]/delegate.conf
[Service]
Delegate=cpu cpuset io memory pids
Reload systemd, then log out and back in (or reboot) so your user manager restarts with the new setting:
sudo systemctl daemon-reload
After logging back in, the command above prints all five controllers, and a limit like docker run --rm --cpus 0.5 alpine true runs without a warning.
Step 8 - Publishing ports below 1024
An unprivileged process cannot bind ports below 1024, so -p 80:80 fails in rootless mode. Lower the threshold system-wide:
echo "net.ipv4.ip_unprivileged_port_start=80" | sudo tee /etc/sysctl.d/99-rootless-docker.conf
sudo sysctl --system
Test it:
docker rm -f web
docker run -d --name web -p 80:80 nginx:alpine
curl -I http://localhost
HTTP/1.1 200 OK
This setting lets any local user bind ports 80 and above, so use it only on servers where you control all accounts. If you use UFW, allow the port as usual with sudo ufw allow 80/tcp.
Step 9 - Preserving the client source IP
By default the rootless port forwarder makes every connection appear to come from inside the container network, so web server logs and IP allow lists see the wrong address. Switching the port driver to slirp4netns keeps the real client IP at the cost of some throughput. Create an override for the user service:
mkdir -p ~/.config/systemd/user/docker.service.d
nano ~/.config/systemd/user/docker.service.d/override.conf
[Service]
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=slirp4netns"
Reload and restart the daemon:
systemctl --user daemon-reload
systemctl --user restart docker.service
Recreate your containers, request a page from another machine, and check docker logs web: the log lines now show the remote address.
Limitations to keep in mind
- Overlay networks and Swarm are not supported.
--net=hostuses the rootless network namespace, not the host's real interfaces.- Privileged operations such as loading kernel modules or mounting block devices do not work, even with
--privileged. - Bind mounts of files owned by root, or by other users, may be unreadable inside the container. Keep application data under your home directory or in named volumes.
- Performance of user-mode networking is lower than rootful bridge networking; for high-throughput services measure before switching.
Troubleshooting
Failed to connect to bus: No medium found: you are in asuorsudo -ushell. Log in as the user over SSH.- RootlessKit fails with
permission deniedoroperation not permittederrors on Ubuntu 24.04: the AppArmor profile from Step 2 is missing or not loaded. Checksudo aa-status | grep rootlesskit. Cannot connect to the Docker daemon at unix:///var/run/docker.sock: the CLI is using the default context. Rundocker context use rootlessor exportDOCKER_HOSTas in Step 4.- Containers stop when you log out: lingering is not enabled. Run
sudo loginctl enable-linger $USER.
Conclusion
You configured Docker rootless mode on Ubuntu 24.04: the daemon runs as a normal user under systemd, starts at boot, enforces CPU and I/O limits, and can publish low ports while preserving client IPs. A container escape or a leaked socket now leads to an unprivileged account instead of root. As next steps, configure log rotation in ~/.config/docker/daemon.json, move your Compose projects to the rootless user, and back up ~/.local/share/docker/volumes as part of your regular backups.
