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-cli and docker-ce-rootless-extras). The Ubuntu docker.io package does not ship the rootless tools.
  • A non-root user with sudo privileges that will own the rootless daemon. Log in as this user directly over SSH. Shells opened with su or sudo -u do 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=host uses 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 a su or sudo -u shell. Log in as the user over SSH.
  • RootlessKit fails with permission denied or operation not permitted errors on Ubuntu 24.04: the AppArmor profile from Step 2 is missing or not loaded. Check sudo aa-status | grep rootlesskit.
  • Cannot connect to the Docker daemon at unix:///var/run/docker.sock: the CLI is using the default context. Run docker context use rootless or export DOCKER_HOST as 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.