Skip to content
Français
Account: not signed in, sync available My learning
Select theme
Français

By updated

Improve this page

With rootless mode, the Docker daemon and the containers run under an ordinary account, with no root rights on the host. This lesson teaches you to install a rootless engine next to the usual engine, to check that it really belongs to the account, then to qualify what it changes: UID mapping, storage, network, resource limits and persistence. It also says what this mode cannot do, so you can decide whether it meets your need. The measurements come from a throwaway Debian 13 VM with Docker Engine 29.9.2 and RootlessKit 3.3.0; the test that tells modifying from deleting comes from a second pass on the same VM and the same versions. Do not replay the installation on a shared host before you have understood it.

What you will learn

Each check is read on the real state of the system, not on the announced configuration.

  • You explain what rootless mode changes compared with the docker group.
  • You check the prerequisites: a range of subordinate UIDs (and how to give one to an account that has none), uidmap, cgroup v2 and the docker-ce-rootless-extras package.
  • You install a rootless engine with dockerd-rootless-setuptool.sh, without touching the rootful engine.
  • You prove that the processes and the storage belong to the account.
  • You read the UID mapping and you deduce its consequences on the files of a volume.
  • You qualify the network (ports, source address, --network host), the resource limits and the startup with no open session.
  • You uninstall rootless mode without leaving anything behind.

Prerequisites

The lesson Put a proxy in front of the Docker API (C36b) comes before this one. You must have installed Docker Engine from the official repository, as in the lesson Install Docker Engine 29 and prove it works, know how to read an ls -l and use sudo.

The demonstration uses two roles that the text always names. The rootful engine is the usual Docker engine, started by root. The account c37-dev, created for the occasion, plays the developer who wants to run containers without root rights. The administration commands, which need sudo, are preceded by the comment # administrateur ("administrator" in French, kept as is in the listings). The account's commands are preceded by # c37-dev, en session SSH ("in an SSH session"): open a real SSH connection under this account, because a sudo -u does not create the session that rootless mode needs. All the containers start from the same image, pinned to its digest, which you define in every session that runs containers:

Terminal window
ALP="alpine:3.24.3@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a"

What does rootless mode change for the engine?

Rootless mode runs the daemon, its containers and its socket under an ordinary account, inside a user namespace. A user namespace is a kernel mechanism that gives a group of processes its own view of UIDs: the root inside corresponds, seen from the host, to an unprivileged UID (the page Understanding Linux namespaces details the mechanism).

The lesson Remove direct access to the Docker daemon (C36) shows the problem from the other side: an account in the docker group talks to a root daemon, so it is root. Rootless mode is the alternative for a user who really must run containers: the user is not given access to a powerful daemon, the user starts one of their own, whose power is only that of the account. Podman works this way by default, as the page Les concepts de Podman (in French) describes.

The diagram puts the two engines face to face on the same host. Look at the owners, the socket and storage paths, the detached network namespace where only the bridge network containers live, and the UID mapping at the bottom, which the section on UIDs then measures.

Two engines coexist on one host. The rootful engine, started by root, exposes /run/docker.sock and stores its data in /var/lib/docker. The rootless engine, started by the c37-dev account (UID 1002), exposes /run/user/1002/docker.sock and stores its data in ~/.local/share/docker; RootlessKit creates its user namespace and a detached network namespace, where only the bridge network containers live: the daemon stays in the host network namespace. In the user namespace, container UID 0 corresponds to host UID 1002, and UIDs 1 to 65536 correspond to host UIDs 231072 to 296607.One host, two independent enginesRootful engine, started by rootRootless engine, started by c37-dev (UID 1002)UID mapping inside the namespace/run/docker.sockowner root, group dockerdockerd and containerdroot processes/var/lib/dockerroot's images and volumes/run/user/1002/docker.sockowner c37-devRootlessKituser namespacedockerd and containerdc37-dev processes~/.local/share/dockerc37-dev's images and volumesDetached network namespace (--detach-netns)only bridge-network containers live in itthe daemon stays in the host network namespaceContainer UID 0Host UID 1002Container UIDs 1 to 65536Host UIDs 231072 to 296607startscreatesapplies
Source · SVG
Esc to close, + and − to zoom, scroll to move around. 100 %

This engine is entirely separate from the first one: socket, processes, images, volumes and networks are not shared. What runs in it does not see the containers of the rootful engine, and the other way round.

Which prerequisites must the machine and the account meet?

Three things must exist before the installation: a range of subordinate UIDs for the account, the two programs newuidmap and newgidmap from the uidmap package, and cgroups v2. A subordinate UID is a UID that the administrator reserves for an account, in /etc/subuid (and /etc/subgid for groups), for it to use in its user namespace: without a range, the account has only one UID to put there, its own. Rootless Docker requires this range: the troubleshooting section of its documentation quotes the error No subuid ranges found for an account that has none. In the demonstration below, useradd gives its range to the account of the lesson, and a system account gets its own with usermod. Start by reading the state of the machine: kernel, cgroups and packages already installed.

Terminal window
# administrateur
uname -r
stat -fc %T /sys/fs/cgroup
dpkg-query -W -f '${Package} ${Version}\n' docker-ce docker-ce-rootless-extras dbus-user-session
6.12.115+deb13.1-amd64
cgroup2fs
dbus-user-session 1.16.2-2
docker-ce 5:29.9.2-1~debian.13~trixie
docker-ce-rootless-extras 5:29.9.2-1~debian.13~trixie

cgroup2fs indicates cgroups v2. docker-ce-rootless-extras brings dockerd-rootless.sh, dockerd-rootless-setuptool.sh and rootlesskit (RootlessKit, the program that creates the namespace and its network); the installation lesson already pinned it together with docker-ce, at the same version 29.9.2. dbus-user-session gives the account's service manager its session bus. The Docker documentation quotes it in its troubleshooting: without this bus, the containers fail to start.

On Debian 13, useradd gives the range by itself to a normal account (not to a system account created with -r), thanks to the SUB_UID_COUNT and SUB_GID_COUNT settings of /etc/login.defs, as long as /etc/subuid exists:

Terminal window
# administrateur
grep -E '^(SUB_UID|SUB_GID)_(MIN|MAX|COUNT)' /etc/login.defs
SUB_UID_MIN 100000
SUB_UID_MAX 600100000
SUB_UID_COUNT 65536
SUB_GID_MIN 100000
SUB_GID_MAX 600100000
SUB_GID_COUNT 65536

Each new account therefore gets 65,536 subordinate UIDs, taken between SUB_UID_MIN and SUB_UID_MAX. The official documentation asks for a range of at least 65,536: it is a recommendation, but a shorter range makes the download of an image fail when its files belong to higher UIDs, with an lchown … invalid argument error. Create the working account, without putting it in the docker group, then read its UID and its ranges:

Terminal window
# administrateur
sudo useradd -m -s /bin/bash c37-dev
id c37-dev
grep '^c37-dev:' /etc/subuid /etc/subgid
uid=1002(c37-dev) gid=1002(c37-dev) groups=1002(c37-dev)
/etc/subuid:c37-dev:231072:65536
/etc/subgid:c37-dev:231072:65536

The account has UID 1002 and the UIDs 231072 to 296607 (65,536 values) are reserved for it. The starting point of the range, 231072, is specific to this VM: it depends on the accounts already created, it is not a default value. These figures come back in the UID mapping, further down.

A system account, on the other hand, has no range. To give it one, first read the ranges already taken in /etc/subuid: two accounts whose ranges overlap would share UIDs. Here, the three existing lines stop at 296607, and the range 300000-365535 overlaps none of them. The value 300000 is specific to this VM: compute yours from your own file.

Terminal window
# administrateur
sudo useradd -r -m -d /var/lib/c37-svc -s /usr/sbin/nologin c37-svc
grep -c '^c37-svc:' /etc/subuid || true
cat /etc/subuid
sudo usermod --add-subuids 300000-365535 --add-subgids 300000-365535 c37-svc; echo "usermod_rc=$?"
grep '^c37-svc:' /etc/subuid /etc/subgid
sudo userdel -r c37-svc; echo "userdel_svc_rc=$?"
grep -c '^c37-svc:' /etc/subuid || true
0
student:100000:65536
ansible:165536:65536
c37-dev:231072:65536
usermod_rc=0
/etc/subuid:c37-svc:300000:65536
/etc/subgid:c37-svc:300000:65536
userdel: c37-svc mail spool (/var/mail/c37-svc) not found
userdel_svc_rc=0
0

The first 0 confirms that the system account has no line in /etc/subuid. usermod --add-subuids … --add-subgids … gives it one, in both files, and userdel -r removes it with the account (the last 0). The warning about the mailbox is harmless. The account of the lesson, c37-dev, does not need this step: useradd already gave it its range.

To open the SSH session of the account, the administrator copies their own authorized key to it here; adapt this step to the way you reach the machine:

Terminal window
# administrateur
sudo install -d -m 0700 -o c37-dev -g c37-dev /home/c37-dev/.ssh
sudo install -m 0600 -o c37-dev -g c37-dev "$HOME/.ssh/authorized_keys" /home/c37-dev/.ssh/authorized_keys; echo "cle_rc=$?"
cle_rc=0

In the account's session, three readings give the starting state: who it is, the runtime directory of its session, and what the rootful engine answers it. The installation tool then checks its prerequisites with the check subcommand:

Terminal window
# c37-dev, en session SSH
id
echo "XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR"
docker version --format '{{.Server.Version}}' 2>&1 | tail -1
dockerd-rootless-setuptool.sh check 2>&1 | sed 's/\x1b\[[0-9;]*m//g'
uid=1002(c37-dev) gid=1002(c37-dev) groups=1002(c37-dev)
XDG_RUNTIME_DIR=/run/user/1002
permission denied while trying to connect to the docker API at unix:///var/run/docker.sock
[ERROR] Missing system requirements. Run the following commands to
[ERROR] install the requirements and run this tool again.
########## BEGIN ##########
sudo sh -eux <<EOF
# Install newuidmap & newgidmap binaries
apt-get install -y uidmap
EOF
########## END ##########

XDG_RUNTIME_DIR=/run/user/1002 is the directory that a real session creates, and the socket of the rootless engine will be placed in it. The account, which is outside the docker group, is refused the socket of the rootful engine: this is intended. As for check, it refuses because uidmap is missing, and it copies the command to run, which only an administrator can type: the account cannot install uidmap itself, whose two programs are setuid root so that they can write the UID mapping. The sed removes the color codes from the output.

How do you install a rootless engine next to the rootful engine?

The installation takes two steps: the administrator prepares the machine, then the account runs dockerd-rootless-setuptool.sh install. The administrator installs uidmap, then sets the account's lingering, before the installation. Lingering is a systemd-logind setting that keeps an account's service manager alive even when nobody is logged in. Without it, this manager stops with the last session of the account and takes the rootless daemon with it: setting it first gives a stable daemon throughout the lesson, with the same PIDs. The section on persistence measures what happens without it.

Terminal window
# administrateur
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq uidmap >/dev/null 2>&1; echo "apt_rc=$?"
dpkg-query -W -f '${Package} ${Version}\n' uidmap
ls -l /usr/bin/newuidmap /usr/bin/newgidmap
sudo loginctl enable-linger c37-dev; echo "linger_rc=$?"
loginctl show-user c37-dev -p Linger
apt_rc=0
uidmap 1:4.17.4-2
-rwsr-xr-x 1 root root 63584 Apr 19 2025 /usr/bin/newgidmap
-rwsr-xr-x 1 root root 59488 Apr 19 2025 /usr/bin/newuidmap
linger_rc=0
Linger=yes

The output of apt-get is redirected to /dev/null to stay short, and DEBIAN_FRONTEND=noninteractive avoids any question. The s in the mode -rwsr-xr-x marks the setuid bit: these two programs run with the rights of root, which lets them write into a user namespace UIDs that the account does not own. Linger=yes confirms the lingering.

The account then reruns check, then install. The tool does not ask for --force here: it stops when the socket of the rootful engine is writable by the account, and the account, outside the docker group, has no access to it. The --force option also exists to override a RootlessKit failure: do not add it by reflex, it hides errors. The return codes are read with PIPESTATUS[0], which gives the one of the tool and not the one of sed. The excerpts below keep only the useful lines of the installation:

Terminal window
# c37-dev, en session SSH
dockerd-rootless-setuptool.sh check 2>&1 | sed 's/\x1b\[[0-9;]*m//g'; echo "check_rc=${PIPESTATUS[0]}"
dockerd-rootless-setuptool.sh install 2>&1 | sed 's/\x1b\[[0-9;]*m//g'; echo "install_rc=${PIPESTATUS[0]}"
[INFO] Requirements are satisfied
check_rc=0
[INFO] Creating /home/c37-dev/.config/systemd/user/docker.service
[INFO] starting systemd service docker.service
[...]
Active: active (running) since Wed 2026-11-04 20:21:58 UTC; 3s ago
[...]
├─5090 rootlesskit --state-dir=/run/user/1002/dockerd-rootless --net=gvisor-tap-vsock --mtu=65520 --slirp4netns-sandbox=auto --slirp4netns-seccomp=auto --disable-host-loopback --port-driver=builtin --copy-up=/etc --copy-up=/run --propagation=rslave --detach-netns /usr/bin/dockerd-rootless.sh
[...]
rootlesskit:
Version: 3.3.0
ApiVersion: 1.1.3
NetworkDriver: gvisor-tap-vsock
PortDriver: builtin
StateDir: /run/user/1002/dockerd-rootless
[...]
Nov 04 20:21:58 c45-debian.lab dockerd-rootless.sh[5132]: time="2026-11-04T20:21:58.830671352Z" level=info msg="API listen on /run/user/1002/docker.sock"
[...]
[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 startup, run: `sudo loginctl enable-linger c37-dev`
[...]
[INFO] Creating CLI context "rootless"
Successfully created context "rootless"
[INFO] Using CLI context "rootless"
Current context is now "rootless"
[...]
[INFO] Some applications may require the following environment variable too:
export DOCKER_HOST=unix:///run/user/1002/docker.sock
[...]
install_rc=0

Five facts can be read here. The tool created a systemd user unit (~/.config/systemd/user/docker.service), that is, a service managed by the account's systemd and not by the system's. This unit starts rootlesskit (PID 5090), which in turn starts the daemon. The network driver is gvisor-tap-vsock, with the builtin port driver. The API listens on /run/user/1002/docker.sock, a path under /run/user/<uid>. Finally, the tool created a Docker context named rootless and activated it, which saves exporting DOCKER_HOST by hand for the docker command line (the tool points out that some applications need this variable). The last [INFO] line recalls the lingering: it is already set.

The state after installation is read with one command per object:

Terminal window
# c37-dev, en session SSH
ls ~/.config/systemd/user/
systemctl --user is-active docker.service
systemctl --user is-enabled docker.service
docker context ls
default.target.wants
docker.service
active
enabled
NAME DESCRIPTION DOCKER ENDPOINT ERROR
default Current DOCKER_HOST based configuration unix:///var/run/docker.sock
rootless * Rootless mode unix:///run/user/1002/docker.sock

The two engines each have their own context: default still targets the rootful socket, rootless (marked by the star) targets the account's socket. The rootful engine was neither stopped nor modified.

How do you check that the daemon and its processes belong to the account?

The proof of rootless mode is threefold: the daemon declares itself rootless, its processes belong to the account, and its storage is separate. The daemon declares its mode in SecurityOptions, with the name=rootless entry, and it gives its data directory and its storage driver:

Terminal window
# c37-dev, en session SSH
docker info --format 'Context={{.ClientInfo.Context}} SecurityOptions={{json .SecurityOptions}} DockerRootDir={{.DockerRootDir}} Driver={{.Driver}} CgroupDriver={{.CgroupDriver}} CgroupVersion={{.CgroupVersion}}'
ps -o user=,pid=,comm= -C rootlesskit,dockerd,containerd
Context=rootless SecurityOptions=["name=seccomp,profile=builtin","name=rootless","name=cgroupns"] DockerRootDir=/home/c37-dev/.local/share/docker Driver=overlayfs CgroupDriver=systemd CgroupVersion=2
root 706 containerd
root 1669 dockerd
c37-dev 5090 rootlesskit
c37-dev 5132 dockerd
c37-dev 5154 containerd

Look at three elements. name=rootless in SecurityOptions is the daemon's announcement. DockerRootDir points into the account's home directory: ~/.local/share/docker, not /var/lib/docker. And ps lists two sets of processes: containerd and dockerd of root (the rootful engine, still there), and rootlesskit, dockerd and containerd of the c37-dev account. The storage driver is overlayfs, the containerd snapshotter, which the daemon chooses without configuration.

The rootlesskit shown by ps has PID 5090, the same as in the installation output, and so do the daemon (5132) and containerd (5154): the lingering set before the installation keeps the account's manager, hence the daemon, alive between sessions.

The image store is separate too. The account starts from zero, downloads the image, then measures what it takes up on its side:

Terminal window
# c37-dev, en session SSH
docker image ls -q | wc -l
docker pull -q "$ALP"
docker image ls --format '{{.Repository}} {{.ID}}'
du -sh ~/.local/share/docker
0
docker.io/library/alpine:3.24.3@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a
alpine be2656061b53
13M /home/c37-dev/.local/share/docker

The account's engine contained no image at the start (0). After the download, 13 MB occupy ~/.local/share/docker. The administrator now looks at the store of the rootful engine:

Terminal window
# administrateur
sudo docker image ls --format '{{.Repository}} {{.ID}}'
echo "images_rootful=$(sudo docker image ls -q | wc -l)"
alpine be2656061b53
images_rootful=1

If your system engine already has this image, as here, its store lists it with the same identifier, because it is the same digest. The account, for its part, started from 0 images and had to download it: each engine keeps its own copy. Practical corollary: each rootless account downloads its images again, and its data count in its home directory.

How do container UIDs map to the host?

In a rootless container, root is not the host's root: it is the account itself. The mapping table /proc/self/uid_map writes it in three columns: the first UID in the container, the first matching UID on the host, and the length of the range. Run two containers, one as root by default, the other with --user 1000:1000, and look at them from the host:

Terminal window
# c37-dev, en session SSH
docker run -d --name c37-m-uid --network none "$ALP" sleep 300 >/dev/null
docker exec c37-m-uid sh -c 'id -u; cat /proc/self/uid_map'
ps -o user=,uid=,pid=,args= -p "$(docker inspect -f '{{.State.Pid}}' c37-m-uid)"
docker run -d --name c37-m-uid1000 --network none --user 1000:1000 "$ALP" sleep 300 >/dev/null
docker exec c37-m-uid1000 id -u
ps -o uid=,pid=,args= -p "$(docker inspect -f '{{.State.Pid}}' c37-m-uid1000)"
docker rm -f c37-m-uid c37-m-uid1000 >/dev/null
0
0 1002 1
1 231072 65536
c37-dev 1002 5512 sleep 300
1000
232071 5611 sleep 300

The output reads in the order of the commands. 0 is the UID of the first container, followed by its table. The first line of the table, 0 1002 1, says that UID 0 of the container is UID 1002 of the host: the container's root is the c37-dev account, and ps confirms it (c37-dev 1002). The second, 1 231072 65536, places UIDs 1 to 65536 of the container on the subordinate UIDs 231072 to 296607. The second container, 1000 inside, therefore has, seen from the host, UID 232071, that is 231072 + (1000 - 1). This is the diagram of the previous section, measured.

The concrete consequence touches the files of a volume. A file created by root in the container belongs to the account on the host. A file created for UID 1000 belongs to UID 232071, which no real account bears:

Terminal window
# c37-dev, en session SSH
docker volume create c37-m-donnees >/dev/null
docker run --rm --network none -v c37-m-donnees:/d "$ALP" sh -c 'echo racine > /d/de-root; touch /d/de-1000; chown 1000:1000 /d/de-1000; ls -ln /d'
ls -ln ~/.local/share/docker/volumes/c37-m-donnees/_data/
echo "racine-modifiee" > ~/.local/share/docker/volumes/c37-m-donnees/_data/de-root; echo "ecriture_hote_rc=$?"
touch ~/.local/share/docker/volumes/c37-m-donnees/_data/de-1000; echo "touch_1000_rc=$?"
total 4
-rw-r--r-- 1 1000 1000 0 Nov 4 20:22 de-1000
-rw-r--r-- 1 0 0 7 Nov 4 20:22 de-root
total 4
-rw-r--r-- 1 232071 232071 0 Nov 4 20:22 de-1000
-rw-r--r-- 1 1002 1002 7 Nov 4 20:22 de-root
ecriture_hote_rc=0
touch: cannot touch '/home/c37-dev/.local/share/docker/volumes/c37-m-donnees/_data/de-1000': Permission denied
touch_1000_rc=1

The first two listings show the same volume seen from two sides: 0 and 1000 in the container, 1002 and 232071 on the host. The account easily modifies the file of the container's root (ecriture_hote_rc=0), but the touch command on the file of UID 1000 is refused (touch_1000_rc=1), because the host sees another owner. A bind mount of a directory of your repository calls for the same vigilance: a container that writes as a UID other than root drops files that the account will no longer be able to modify from the host.

Modifying and deleting do not follow the same rule. Deleting an entry depends on the rights on the directory that contains it, not on the file itself. The following test creates, from a container, a file and a directory of UID 1000 in a new volume, then modifies and deletes them from the host:

Terminal window
# c37-dev, en session SSH
docker volume create c37-m-droits >/dev/null
docker run --rm --network none -v c37-m-droits:/d "$ALP" sh -c 'touch /d/fichier-1000; mkdir /d/rep-1000; touch /d/rep-1000/dedans; chown -R 1000:1000 /d/fichier-1000 /d/rep-1000'
V=~/.local/share/docker/volumes/c37-m-droits/_data
ls -ln "$V" "$V/rep-1000"
echo "=== modifier"
touch "$V/fichier-1000"; echo "touch_rc=$?"
echo "=== supprimer un fichier de l'UID 1000 dans un répertoire du compte"
rm -f "$V/fichier-1000"; echo "rm_fichier_rc=$?"
echo "=== vider un répertoire de l'UID 1000"
rm -rf "$V/rep-1000"; echo "rm_rep_rc=$?"
ls -ln "$V"
echo "=== nettoyage"
docker volume rm c37-m-droits
/home/c37-dev/.local/share/docker/volumes/c37-m-droits/_data:
total 4
-rw-r--r-- 1 232071 232071 0 Nov 4 20:47 fichier-1000
drwxr-xr-x 2 232071 232071 4096 Nov 4 20:47 rep-1000
/home/c37-dev/.local/share/docker/volumes/c37-m-droits/_data/rep-1000:
total 0
-rw-r--r-- 1 232071 232071 0 Nov 4 20:47 dedans
=== modifier
touch_rc=1
=== supprimer un fichier de l'UID 1000 dans un répertoire du compte
touch: cannot touch '/home/c37-dev/.local/share/docker/volumes/c37-m-droits/_data/fichier-1000': Permission denied
rm_fichier_rc=0
=== vider un répertoire de l'UID 1000
rm: cannot remove '/home/c37-dev/.local/share/docker/volumes/c37-m-droits/_data/rep-1000/dedans': Permission denied
rm_rep_rc=1
total 4
drwxr-xr-x 2 232071 232071 4096 Nov 4 20:47 rep-1000
=== nettoyage
c37-m-droits

In this capture, the touch message appears below the === supprimer … header ("delete" in French): output and errors travelled through two distinct channels. In your terminal, the error will appear right after the command that produced it. Follow the return codes. The account does not modify the file of UID 1000 (touch_rc=1), yet it deletes it (rm_fichier_rc=0): the parent directory _data belongs to it, and this is the right that counts to remove an entry. It cannot empty rep-1000 (rm_rep_rc=1, Permission denied): this directory belongs to UID 232071, the account has no write right on it, so it cannot remove dedans ("inside") from it, and rep-1000 stays visible in the last ls. To erase this kind of data, go through the daemon while it runs (docker volume rm), or through rootlesskit after the uninstallation, as uninstall suggests.

What does rootless mode change for networking?

Rootless mode changes three things for the network: the service no longer receives the source address of the clients by default, a port below 1024 cannot be published, and --network host exposes the service on the real network of the machine. A high port, for its part, is published normally. The service of the example is a small nc server that answers one line and prints the address of its clients; HOTE ("host" in French) receives the first global IPv4 address of the machine:

Terminal window
# c37-dev, en session SSH
HOTE=$(ip -4 -o addr show scope global | awk '{print $4}' | cut -d/ -f1 | head -1)
SRV='while true; do printf "HTTP/1.0 200 OK\r\n\r\npage-c37\n" | nc -l -n -v -p 8080 2>&1 | grep -i connect; done'
echo "HOTE=$HOTE"
docker run -d --name c37-m-web -p 127.0.0.1:8088:8080 "$ALP" sh -c "$SRV" >/dev/null
sleep 1
docker port c37-m-web
curl -s http://127.0.0.1:8088/; echo "curl_boucle_rc=$?"
curl -s -m 3 "http://$HOTE:8088/"; echo "curl_ip_hote_rc=$?"
sleep 1
docker logs c37-m-web 2>&1 | tail -1
docker rm -f c37-m-web >/dev/null
HOTE=10.10.130.11
8080/tcp -> 127.0.0.1:8088
page-c37
curl_boucle_rc=0
curl_ip_hote_rc=7
connect to [::ffff:172.17.0.2]:8080 from [::ffff:172.17.0.1]:57558 ([::ffff:172.17.0.1]:57558)

The publication on 127.0.0.1:8088 answers (curl_boucle_rc=0), and the address of the VM, which is not in the publication, does not answer (curl_ip_hote_rc=7). The log line shows a limit: the service (172.17.0.2) receives the connection from 172.17.0.1, an address internal to the container network, and not from 127.0.0.1, the real address of the client. With the default configuration (the builtin port driver), the real source address is not passed on, which hinders a service that logs or filters by address.

The Docker documentation states that RootlessKit 3 passes on the source address over TCP, provided the userland-proxy is disabled. The workaround played in this lesson is the slirp4netns port driver, presented further down.

The rootless bridge takes 172.17.0.0/16, like the rootful engine, without any address conflict. The likely explanation: the account's bridge lives in the RootlessKit network namespace, distinct from the host one where the bridge of the rootful engine is.

Now port 80. An ordinary account cannot listen on a port below 1024, and the same goes for RootlessKit. Publishing port 80 therefore fails:

Terminal window
# c37-dev, en session SSH
docker run -d --name c37-m-web80 -p 80:8080 "$ALP" sleep 30; echo "run80_rc=$?"
docker rm -f c37-m-web80 >/dev/null
40226111cd6c425dfc6d9e514c6ae9439cf6b4f361124a55978580c9d9858d7f
docker: Error response from daemon: failed to set up container networking: driver failed programming external connectivity on endpoint c37-m-web80 (398ba7ab05c922a3581a9647ba99db1e738e3d8473793fe17fedfa83f466b200): error while calling RootlessKit PortManager.AddPort(): cannot expose privileged port 80, you can add 'net.ipv4.ip_unprivileged_port_start=80' to /etc/sysctl.conf (currently 1024), or set CAP_NET_BIND_SERVICE on rootlesskit binary, or choose a larger port number (>= 1024): listen tcp4 0.0.0.0:80: bind: permission denied
Run 'docker run --help' for more information
run80_rc=126

The container was created (its identifier is displayed) but did not start, hence the docker rm -f. The return code is 126, and the message quotes its cause, cannot expose privileged port 80, then three ways out that it offers: lowering net.ipv4.ip_unprivileged_port_start in /etc/sysctl.conf (current value 1024), giving the CAP_NET_BIND_SERVICE capability to the rootlesskit binary, or choosing a port greater than or equal to 1024. The first is a kernel setting that applies to the whole machine. The second is set on the file: it holds for every user who runs this binary, not only for your account. The documentation of this mode words them like this: sudo setcap cap_net_bind_service=ep $(which rootlesskit) then a restart of the daemon, or net.ipv4.ip_unprivileged_port_start in /etc/sysctl.d then sudo sysctl --system. These two settings modify the whole machine or a shared binary: they are outside the scope of the demonstration, which stays on an isolated account. The simplest is still to publish a high port, behind a reverse proxy or a redirection set up by the administrator if the service must be reached on port 80.

On this VM, outbound traffic works, ping included:

Terminal window
# c37-dev, en session SSH
docker run --rm "$ALP" sh -c 'wget -q -T 5 -O /dev/null http://deb.debian.org/debian/ && echo http_sortant=ok || echo http_sortant=echec; ping -c1 -W2 1.1.1.1 >/dev/null 2>&1 && echo ping=ok || echo ping=echec'
http_sortant=ok
ping=ok

The ping answered on this Debian 13. The Docker documentation specifies that its operation depends on the kernel parameter net.ipv4.ping_group_range, whose value varies by distribution: on another one, check it.

The --network host option calls for particular attention. The RootlessKit command line seen at installation carries --detach-netns: the daemon stays in the network namespace of the host, and only the containers on the bridge network live in the detached network namespace of RootlessKit (their IP address is therefore not reachable from the host). A container started with --network host thus sees the real interfaces of the host, and a service listening there is reachable from the host:

Terminal window
# c37-dev, en session SSH
docker run --rm --network host "$ALP" sh -c 'ip -4 -o addr show | awk "{print \$2, \$4}"'
docker run -d --name c37-m-hote --network host "$ALP" sh -c "$SRV" >/dev/null
sleep 1
curl -s -m 3 http://127.0.0.1:8080/; echo "curl_hote_8080_rc=$?"
ss -ltn 'sport = :8080' | tail -n +2
docker rm -f c37-m-hote >/dev/null
ss -ltn 'sport = :8080' | tail -n +2 | wc -l
lo 127.0.0.1/8
enp1s0 10.10.130.11/24
docker0 172.17.0.1/16
page-c37
curl_hote_8080_rc=0
LISTEN 0 1 *:8080 *:*
0

The container sees lo, enp1s0 (the interface that carries the address 10.10.130.11 that HOTE read) and docker0, the bridge of the rootful engine: these are the interfaces of the host. The service answers from the host (curl_hote_8080_rc=0), and port 8080 listens on *, every address of the machine. The last 0 shows that the port is freed once the container is removed. This is a security limit: rootless mode does not partition the network of a container started with --network host, which exposes its service to the network of the machine. Keep it for an identified need, and prefer publishing specific ports.

The network driver seen at installation, gvisor-tap-vsock, is not imposed: according to the dockerd-rootless.sh script, it picks slirp4netns if it is installed, otherwise pasta, otherwise vpnkit, and falls back on gvisor-tap-vsock. This driver is embedded in RootlessKit, so there is no extra binary to install, but RootlessKit describes it as experimental: do not present it as a proven driver for production. The workaround below changes driver precisely.

The Docker documentation offers, in its troubleshooting section "docker run -p does not propagate source IP addresses", a workaround to keep the source address: replace the port driver with slirp4netns. It needs the administrator, who installs the Debian package (the engine does not embed slirp4netns), then the account:

Terminal window
# administrateur
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq slirp4netns >/dev/null 2>&1; echo "apt_rc=$?"
dpkg-query -W -f '${Package} ${Version}\n' slirp4netns
apt_rc=0
slirp4netns 1.2.1-1.1

The account restarts its daemon, then compares the drivers before and after with the pilotes function ("drivers" in French). The package alone is enough to change the network, but not the port driver:

Terminal window
# c37-dev, en session SSH
pilotes() { docker version --format '{{range .Server.Components}}{{if eq .Name "rootlesskit"}}{{.Details.NetworkDriver}} {{.Details.PortDriver}}{{end}}{{end}}'; }
pilotes
systemctl --user restart docker.service; echo "restart_rc=$?"
pilotes
gvisor-tap-vsock builtin
restart_rc=0
slirp4netns builtin

The first call shows the default network, gvisor-tap-vsock builtin. After the restart, the network is slirp4netns, but the port driver stays builtin. To change the latter too, the account writes an override of the user unit, reloads systemd and restarts the daemon, then calls the service again with the same nc program. ALP, HOTE and SRV are the ones defined above: if you opened a new session, define them again.

Terminal window
# c37-dev, en session SSH
mkdir -p ~/.config/systemd/user/docker.service.d
printf '[Service]\nEnvironment="DOCKERD_ROOTLESS_ROOTLESSKIT_NET=slirp4netns"\nEnvironment="DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=slirp4netns"\n' > ~/.config/systemd/user/docker.service.d/override.conf
cat ~/.config/systemd/user/docker.service.d/override.conf
systemctl --user daemon-reload
systemctl --user restart docker.service; echo "restart_rc=$?"
pilotes
docker run -d --name c37-m-web -p 8088:8080 "$ALP" sh -c "$SRV" >/dev/null
sleep 1
curl -s http://127.0.0.1:8088/; echo "curl_boucle_rc=$?"
sleep 1
docker logs c37-m-web 2>&1 | tail -1
curl -s -m 3 "http://$HOTE:8088/"; echo "curl_ip_hote_rc=$?"
sleep 1
docker logs c37-m-web 2>&1 | tail -1
docker rm -f c37-m-web >/dev/null
[Service]
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_NET=slirp4netns"
Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=slirp4netns"
restart_rc=0
slirp4netns slirp4netns
page-c37
curl_boucle_rc=0
connect to [::ffff:172.17.0.2]:8080 from [::ffff:10.0.2.2]:58892 ([::ffff:10.0.2.2]:58892)
page-c37
curl_ip_hote_rc=0
connect to [::ffff:172.17.0.2]:8080 from [::ffff:10.10.130.11]:35134 ([::ffff:10.10.130.11]:35134)

After the override, pilotes answers slirp4netns slirp4netns. The two logs read as follows. A call on 127.0.0.1 is seen from 10.0.2.2, the gateway of slirp4netns. A call on the address of the machine is seen from 10.10.130.11: the address of the client is preserved. Two reservations: the client was the host itself, for lack of another machine, and the documentation classes this port driver as slow ("Slow"). So choose it when the service needs the source address, not by default.

The way back takes three steps: the account removes the override, the administrator purges the package, then the account restarts its daemon.

Terminal window
# c37-dev, en session SSH
find ~/.config/systemd/user/docker.service.d -name override.conf -delete
systemctl --user daemon-reload
Terminal window
# administrateur
sudo DEBIAN_FRONTEND=noninteractive apt-get purge -y -qq slirp4netns >/dev/null 2>&1; echo "purge_rc=$?"
dpkg-query -W slirp4netns 2>&1
purge_rc=0
dpkg-query: no packages found matching slirp4netns

The package is removed: dpkg-query no longer finds it. What remains is to restart the account's daemon, which returns to the default network:

Terminal window
# c37-dev, en session SSH
systemctl --user restart docker.service; echo "restart_rc=$?"
docker version --format '{{range .Server.Components}}{{if eq .Name "rootlesskit"}}{{.Details.NetworkDriver}} {{.Details.PortDriver}}{{end}}{{end}}'
restart_rc=0
gvisor-tap-vsock builtin

The daemon is back to gvisor-tap-vsock builtin: the rest of the lesson runs on the original setting.

Which resource limits can the account enforce?

The account can only limit the resources whose cgroup controller systemd delegates to it. On Debian 13 (systemd 257), the account's manager receives cpu, memory and pids by default, not io nor cpuset. The official documentation of rootless mode says that only memory and pids are typically delegated: on this Debian, cpu is too, so the list depends on the machine. To delegate more (io, cpuset), the documentation describes an override of user@.service with Delegate=. To check it here, run containers with these three limits and read the cgroup again from the inside, then read the list of delegated controllers:

Terminal window
# c37-dev, en session SSH
docker run --rm --memory 64m --pids-limit 20 "$ALP" sh -c 'cat /sys/fs/cgroup/memory.max /sys/fs/cgroup/pids.max'; echo "mem_pids_rc=$?"
docker run --rm --cpus 0.5 "$ALP" cat /sys/fs/cgroup/cpu.max; echo "cpus_rc=$?"
cat /sys/fs/cgroup/user.slice/user-$(id -u).slice/user@$(id -u).service/cgroup.controllers
67108864
20
mem_pids_rc=0
50000 100000
cpus_rc=0
cpu memory pids

--memory 64m sets memory.max to 67,108,864 bytes, --pids-limit 20 sets pids.max to 20, and --cpus 0.5 sets cpu.max to 50000 100000, that is half a core. The last line explains why: only cpu, memory and pids are delegated to the account. The absence of io also shows at the start of the daemon, which warns No io.max (rbps) support (and the same line for wbps, riops and wiops), in the daemon log at installation. As no io controller is delegated, the I/O limits such as --device-read-bps cannot apply; this case is outside the scope of the demonstration. If your need depends on it, test it on your machine or ask the administrator to delegate this controller.

Does AppArmor protect rootless containers?

No: in rootless mode, no AppArmor profile is applied, and the option that asks for one is ignored without error. The Docker documentation, besides, lists AppArmor among the features not supported in this mode. The daemon says so in its security options, and the container confirms it in /proc/1/attr/current:

Terminal window
# c37-dev, en session SSH
docker info --format '{{json .SecurityOptions}}'
docker run -d --name c37-m-aa --network none "$ALP" sleep 60 >/dev/null
docker inspect -f 'AppArmorProfile={{json .AppArmorProfile}}' c37-m-aa
docker exec c37-m-aa cat /proc/1/attr/current
docker rm -f c37-m-aa >/dev/null
docker run --rm --network none --security-opt apparmor=docker-default "$ALP" cat /proc/1/attr/current; echo "aa_explicite_rc=$?"
["name=seccomp,profile=builtin","name=rootless","name=cgroupns"]
AppArmorProfile=""
runc (unconfined)
runc (unconfined)
aa_explicite_rc=0

SecurityOptions does not contain apparmor, AppArmorProfile is empty and process 1 of the container is runc (unconfined). Explicitly asking for --security-opt apparmor=docker-default succeeds (code 0, measured without a pipe) and changes nothing: a security option is silently ignored, which is more misleading than an error. The seccomp filter stays active (name=seccomp,profile=builtin). The lesson seccomp and AppArmor profiles for Docker containers details these two layers.

What can a rootless container that escapes reach?

A rootless container that escapes through the daemon or the runtime obtains the powers of the account, not those of root. The UID table showed it: the container's root is UID 1002, the account's, and the other UIDs of the container fall into the account's subordinate range. An escape would therefore reach what this account can read or write: its SSH keys, its code, its tokens. The rootless daemon is not a daemon without power, it is a daemon that has only those of its account.

Hence the choice of the demonstration: a dedicated account, which contains only its rootless engine and its data, and which has neither the keys nor the code of another project. Running rootless mode under a developer's personal account gives the daemon access to everything that account owns. Add to this two limits measured above: no AppArmor profile, and an open host network as soon as a container is started with --network host.

How do you keep the rootless daemon running with no open session?

Without lingering, the daemon stops when the last session of the account closes: the account's manager stops, rootlesskit disappears and the commands run afterwards fail for lack of a socket. Lingering, set by the administrator with sudo loginctl enable-linger, prevents this: it is what lets a user unit start at boot and survive the logout. The measurement below shows rootlesskit started again (PID 8886) as soon as enable-linger is restored.

The checks that follow are made without any open session of the account: close your SSH session of c37-dev before this test. They are played from the administrator. The D() function runs a Docker command under the account, with the socket of the rootless daemon. A container is declared there with --restart unless-stopped, then the lingering is removed and restored. The first check lists the sessions of the machine:

Terminal window
# administrateur
D() { sudo -u c37-dev -H env DOCKER_HOST=unix:///run/user/1002/docker.sock docker "$@"; }
ALP="alpine:3.24.3@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a"
loginctl list-sessions --no-legend
D run -d --name c37-m-veille --restart unless-stopped --network none "$ALP" sleep infinity >/dev/null; echo "run_rc=$?"
D ps --format '{{.Names}} {{.Status}}'
sudo loginctl disable-linger c37-dev; echo "disable_rc=$?"
for i in $(seq 1 90); do pgrep -u c37-dev rootlesskit >/dev/null || break; sleep 1; done; echo "attente=${i}s"
loginctl show-user c37-dev -p Linger -p State 2>&1
pgrep -u c37-dev -a rootlesskit || echo "aucun rootlesskit"
D ps 2>&1 | tail -1
sudo loginctl enable-linger c37-dev; echo "enable_rc=$?"
sleep 20
loginctl show-user c37-dev -p Linger -p State 2>&1
pgrep -u c37-dev -a rootlesskit | cut -c1-60
D ps --format '{{.Names}} {{.Status}}'
44 1002 c37-dev - 8217 manager - no -
46 1001 ansible - 8613 manager - no -
47 1001 ansible - 8680 user - no -
run_rc=0
c37-m-veille Up Less than a second
disable_rc=0
attente=13s
State=closing
Linger=no
aucun rootlesskit
failed to connect to the docker API at unix:///run/user/1002/docker.sock; check if the path is correct and if the daemon is running: dial unix /run/user/1002/docker.sock: connect: no such file or directory
enable_rc=0
State=lingering
Linger=yes
8886 rootlesskit --state-dir=/run/user/1002/dockerd-rootless
c37-m-veille Up 19 seconds

The first line of loginctl list-sessions belongs to c37-dev (UID 1002) and its class is manager: it is not a connection, since you closed yours, but the session that systemd-logind opens for the account's service manager, which the lingering maintains. The two ansible lines, the administrator's account, carry a manager class and a user class, its own connection.

The for loop waits for rootlesskit to end instead of a fixed duration: attente=13s ("wait" in French) says that the stop took 13 seconds (the loop gives up at 90 s). Without lingering, the account is in the closing state, no rootlesskit runs any more and the socket has disappeared: the API answers no such file or directory, and the container is no longer served. With lingering, the state goes to lingering, rootlesskit is back (PID 8886) and the container c37-m-veille, Up 19 seconds, restarted thanks to its restart policy. For a rootless service to survive the logout of its owner, enable lingering, as the last [INFO] line of the installation advises. A related point: the documentation specifies that running rootless Docker as a system service (/etc/systemd/system/docker.service), even with User=, is not supported. Keep the user unit created by the tool.

The D() function only gives access to the daemon, through DOCKER_HOST. For the administrator to drive the account's service manager with systemctl --user, a sudo -u is not enough: XDG_RUNTIME_DIR must also point to the account's runtime directory, and DBUS_SESSION_BUS_ADDRESS to its bus when the default does not fit. The demonstration does not need it: it goes through DOCKER_HOST.

What remains is the reboot of the machine. After a reboot, still without the account logging in, the administrator reads:

Terminal window
# administrateur
D() { sudo -u c37-dev -H env DOCKER_HOST=unix:///run/user/1002/docker.sock docker "$@"; }
uptime -s
loginctl show-user c37-dev -p Linger -p State 2>&1
pgrep -u c37-dev -a rootlesskit | cut -c1-60
D ps --format '{{.Names}} {{.Status}}'
ip -br link show docker0
2026-11-04 20:25:19
State=lingering
Linger=yes
754 rootlesskit --state-dir=/run/user/1002/dockerd-rootless
c37-m-veille Up 14 seconds
docker0 DOWN ee:06:6b:a4:eb:6c <NO-CARRIER,BROADCAST,MULTICAST,UP>

The machine rebooted at 20:25:19, and the rootless daemon and its container came back (the container shows Up 14 seconds) without the account logging in: lingering maintains the account's service manager. The docker0 bridge of the rootful engine is also present on the host, in the DOWN state.

How do you uninstall rootless mode cleanly?

The uninstallation takes three steps: the account stops and removes its engine, it deletes its data, then the administrator removes the lingering, the account and the uidmap package. Reopen an SSH session under the account, which deletes its objects and runs uninstall:

Terminal window
# c37-dev, en session SSH
docker rm -f c37-m-veille >/dev/null; docker volume rm c37-m-donnees
dockerd-rootless-setuptool.sh uninstall 2>&1 | sed 's/\x1b\[[0-9;]*m//g'; echo "uninstall_rc=${PIPESTATUS[0]}"
docker context ls
c37-m-donnees
+ systemctl --user stop docker.service
+ systemctl --user disable docker.service
Removed '/home/c37-dev/.config/systemd/user/default.target.wants/docker.service'.
[INFO] Uninstalled docker.service
[INFO] Deleted CLI context "rootless"
Current context is now "default"
[INFO] Configured CLI to use the "default" context.
[INFO]
[INFO] Make sure to unset or update the environment PATH, DOCKER_HOST, and DOCKER_CONTEXT environment variables if you have added them to `~/.bashrc`.
[INFO] This uninstallation tool does NOT remove Docker binaries and data.
[INFO] To remove data, run: `/usr/bin/rootlesskit rm -rf /home/c37-dev/.local/share/docker`
uninstall_rc=0
NAME DESCRIPTION DOCKER ENDPOINT ERROR
default * Current DOCKER_HOST based configuration unix:///var/run/docker.sock

The tool stops and disables the unit, deletes the rootless context and reactivates default. It also says what it does not do: it removes neither the binaries nor the data, and it suggests rootlesskit rm -rf for the data. You must go through RootlessKit, because this data partly belongs to subordinate UIDs: the account cannot modify these files from the host, and it cannot empty a directory of UID 1000, as the rm refused above shows. rootlesskit creates a new namespace with the same mapping as the daemon, where the account is root: it does not enter an existing namespace. The deletion therefore runs there with the necessary rights:

Terminal window
# c37-dev, en session SSH
du -sh ~/.local/share/docker
rootlesskit find ~/.local/share/docker -delete; echo "suppression_rc=$?"
ls -d ~/.local/share/docker 2>&1
13M /home/c37-dev/.local/share/docker
suppression_rc=0
ls: cannot access '/home/c37-dev/.local/share/docker': No such file or directory

The 13 MB folder has disappeared. Here, the command played is rootlesskit find … -delete, which does the same job as the rootlesskit rm -rf proposed by the tool. What remains is the administrator's side. First close your SSH session of c37-dev: the administrator then removes the lingering, checks that no process remains, deletes the account and its UID ranges. The first command counts the account's sessions, to show that only the lingering one remains:

Terminal window
# administrateur
loginctl list-sessions --no-legend | awk '{print $3}' | grep -c c37-dev || true
sudo loginctl disable-linger c37-dev; echo "disable_rc=$?"
sleep 15
pgrep -u c37-dev -a . || echo "aucun processus de c37-dev"
sudo userdel -r c37-dev; echo "userdel_rc=$?"
grep -c '^c37-dev:' /etc/subuid /etc/subgid
ls -d /home/c37-dev 2>&1
1
disable_rc=0
aucun processus de c37-dev
userdel: c37-dev mail spool (/var/mail/c37-dev) not found
userdel_rc=0
/etc/subuid:0
/etc/subgid:0
ls: cannot access '/home/c37-dev': No such file or directory

The 1 at the start is the manager session of the lingering, not a connection. After disable-linger, no process remains for the account, and userdel -r removed the account, its range in /etc/subuid and /etc/subgid (:0 everywhere) and its home directory. The warning about the mailbox is harmless. What remains is the uidmap package, installed for the lesson:

Terminal window
# administrateur
sudo DEBIAN_FRONTEND=noninteractive apt-get purge -y -qq uidmap >/dev/null 2>&1; echo "purge_uidmap_rc=$?"
dpkg-query -W uidmap 2>&1
find /tmp -maxdepth 1 -user 1002 2>/dev/null | wc -l
purge_uidmap_rc=0
dpkg-query: no packages found matching uidmap
0

uidmap is purged, and no file of UID 1002 is left in /tmp (the last 0): the machine is back to its starting state, leaving nothing behind, apart from the Docker packages installed before the lesson.

Put it into practice

The lab prepares a rootless engine for a dedicated account, through a user unit of its own rather than through the installation tool seen in this lesson, and leaves the system's rootful engine intact. It asks you to qualify this engine. The tests measure the real state: the user unit is active, the daemon process belongs to the unprivileged account, the daemon announces rootless mode, a real HTTP service answers on the loopback, a peer on the network is allowed and a peer with no network is refused, and a piece of data written in a volume survives the recreation of the container.

Key points

Rootless mode replaces access to a root daemon with a daemon that has only the powers of its ordinary account, and those powers are the account's: its keys, its code.

  • You tell the docker group (an access to the root daemon) from rootless mode (a daemon and containers that belong to the account).
  • You check the prerequisites: 65,536 subordinate UIDs in /etc/subuid and /etc/subgid (usermod --add-subuids … --add-subgids … for an account that has none), the uidmap package, cgroups v2, and docker-ce-rootless-extras at the engine's version.
  • You set lingering before the installation, then you install with dockerd-rootless-setuptool.sh install under the account, in an SSH session; check says what is missing, and the tool creates a user systemd unit, a rootless context and the socket /run/user/<uid>/docker.sock.
  • You prove the mode by name=rootless in SecurityOptions, the account's ps processes and DockerRootDir in its home directory; the image store is separate from the rootful engine's.
  • You read /proc/self/uid_map: the container's root is the account, the other UIDs fall into the subordinate range, and the files of a volume bear these UIDs on the host: the account does not modify them, can delete a file whose directory it owns, but cannot empty a directory of UID 1000, hence rootlesskit to erase the data.
  • You know that ports below 1024 cannot be published, that the clients' source address is not passed on by default (the slirp4netns port driver keeps it, more slowly), and that --network host exposes the service on the real network of the machine.
  • You know that cpu, memory and pids are delegated by default on Debian 13, not io, and that AppArmor is not applied in rootless mode: seccomp stays active.
  • You enable lingering so that the daemon survives logout and reboot, and you delete the data with rootlesskit find … -delete.

Knowledge check

Check that the essentials are mastered: what rootless mode changes, how to read the UID mapping and which constraints it imposes on the network, the resources and persistence.

Knowledge check

Check what you have learned with this interactive quiz

6 questions
6 min
70% to pass

How it works

  • The timer starts when you click Start
  • Multiple choice, true/false and short answer questions
  • You can move between questions
  • Detailed results are shown at the end

Starts the quiz and the timer

Is this project useful to you?

The guides, courses, quizzes and labs stay free. If you use them regularly, you can contribute to their upkeep and development.

Become a supporting member (new tab) One-time support (new tab) Learn more

Follow the project on LinkedIn