Going rootless with Docker and Containers

Historically, Docker Engine or Docker has always required root privileges to run. This is because certain features like namespaces or mount points which forms the basis of Docker filesystems have always required elevated privileges. You may have started running docker daemon or dockerd in context of another user, but that user needs to be made part of Docker Group, which was assigned root privileges during installation time. Rootless mode means running the Docker daemon and even containers as an unprivileged user to protect the root user from future attacks on the host system.

Since Docker Engine is comprised of whole stack of smaller components – runc, containerd, dockerd, etc., running in rootless mode means running the whole stack in rootless mode. Also since dockerd itself is running as a non-root user, the containers launched will also not have any root privileges associated with them. So this mode protects the host system from potential attacks that exploit vulnerabilities in the application code or misconfiguration arising from dockerd or containerd or runc.

Rootless mode was introduced in Docker version 19.03 as an experimental feature and it had some disadvantages. In Particular, it did not supported cgroups and also did not supported OverlayFS, so you could not define resource limitations for rootless containers. However starting with Docker Engine v20.10., some of these limitations have been removed and it is considered ready for general usage.

What Looks like rootless but is not?

  • docker run --user foo: It allows you to execute process in containers as non-root. Notably you cannot perform privileged activities like package installation etc. runc, containerd, etc still run as root.
  • usermod -aG docker foo: Allows a non-root user to connect to docker socket. It is equivalent to allow user to run as root.
  • sudo docker and chmod +s dockerd: Needs no explanation
  • dockerd --userns-remap: It allows you to run containers as non-root. runc, containerd, etc still run as root.

How does rootless mode works in Docker?

As we understand it, a lot of docker engine features requires root privileges. Rootless mode works around this restriction by taking advantage of something called user namespaces. User namespaces map a range of user IDs so that the root user in the inner namespace maps to an unprivileged range in the parent namespace. A fresh process in user namespace also picks up a full set of process capabilities.

User namespaces has been around since Linux kernel v3.8, so this feature has been present in Docker for a long time with the --userns-remap flag. The rootless mode works in a similar way, except that it creates a user namespace first and start the daemon already in the remapped namespace. The daemon and the containers will both use the same user namespace that is different from the host one:

Image with courtesy of https://www.docker.com/blog/experimenting-with-rootless-docker/

Pre-requisites / Dependencies

Rootless mode has a dependency on the uidmap package that can do the remapping of users. This package provides required binaries for it to work.

Install Docker in Rootless mode

For installing docker engine in rootless mode you do not need root privileges on the host system. You do not need to run any command as sudo or need access to package managers like apt, dnf, yum, etc. as well. Installation steps are covered in detail at https://docs.docker.com/engine/security/rootless/. This is TL;DR version of the same for Ubuntu 20.04 LTS:

  • Install uidmap package with sudo privileges or ask your system admin to do it for you: apt-get install -y uidmap
cloud_user@d7e5dc06581c:~$ sudo apt-get install -y uidmap
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following NEW packages will be installed:
uidmap
0 upgraded, 1 newly installed, 0 to remove and 0 not upgraded.
Need to get 26.3 kB of archives.
After this operation, 171 kB of additional disk space will be used.
Get:1 http://ap-southeast-1.ec2.archive.ubuntu.com/ubuntu focal-updates/universe amd64 uidmap amd64 1:4.8.1-1ubuntu5.20.04 [26.3 kB]
Fetched 26.3 kB in 1s (50.4 kB/s)
Selecting previously unselected package uidmap.
(Reading database ... 193042 files and directories currently installed.)
Preparing to unpack .../uidmap_1%3a4.8.1-1ubuntu5.20.04_amd64.deb ...
Unpacking uidmap (1:4.8.1-1ubuntu5.20.04) ...
Setting up uidmap (1:4.8.1-1ubuntu5.20.04) ...
Processing triggers for man-db (2.9.1-1) ...
  • Grab installation script from Docker and run it: curl -fsSL https://get.docker.com/rootless | sh
cloud_user@d7e5dc06581c:~$ curl -fsSL https://get.docker.com/rootless | sh
# Installing stable version 20.10.5
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 65.9M 100 65.9M 0 0 142M 0 --:--:-- --:--:-- --:--:-- 142M
...
...
[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 cloud_user`

[INFO] Make sure the following environment variables are set (or add them to ~/.bashrc):

export PATH=/home/cloud_user/bin:$PATH
export DOCKER_HOST=unix:///run/user/1001/docker.sock
  • Export the environment variables as mentioned or add them to ~/.bashrc. In our case, we’ll edit ~/.bashrc and then reload profile using source.
  • Start dockerd with systemd: systemctl --user start docker

Verify Installation and Run Containers

We can now use docker version to verify docker version installed and run containers using docker run:

cloud_user@d7e5dc06581c:~$ docker version
Client: Docker Engine - Community
Version: 20.10.5
API version: 1.41
Go version: go1.13.15
Git commit: 55c4c88
Built: Tue Mar 2 20:14:11 2021
OS/Arch: linux/amd64
Context: default
Experimental: true

Server: Docker Engine - Community
Engine:
Version: 20.10.5
API version: 1.41 (minimum version 1.12)
Go version: go1.13.15
Git commit: 363e9a8
Built: Tue Mar 2 20:18:31 2021
OS/Arch: linux/amd64
Experimental: false
containerd:
Version: v1.4.3
GitCommit: 269548fa27e0089a8b8278fc4fc781d7f65a939b
runc:
Version: 1.0.0-rc93
GitCommit: 12644e614e25b05da6fd08a38ffa0cfe1903fdec
docker-init:
Version: 0.19.0
GitCommit: de40ad0
cloud_user@d7e5dc06581c:~$ docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b8dfde127a29: Pull complete
Digest: sha256:308866a43596e83578c7dfa15e27a73011bdd402185a84c5cd7f32a88b501a24
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
1. The Docker client contacted the Docker daemon.
2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
(amd64)
3. The Docker daemon created a new container from that image which runs the
executable that produces the output you are currently reading.
4. The Docker daemon streamed that output to the Docker client, which sent it
to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
$ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
https://hub.docker.com/

For more examples and ideas, visit:
https://docs.docker.com/get-started/

Limitations of Rootless Mode

Exposing Network Ports

Be aware that port numbers below 1024 are called privileged ports and not available for rootless users. So you will need to use unprivileged ports like 8080, etc. So if you want to run HTTP server, you need to run docker run -p 8080:80. However, if you really need to expose privileged ports, you can do that by adjusting sysctl /proc/sys/net/ipv4/ip_unprivileged_port_start or by setting CAP_NET_BIND SERVICE capability on binary rootlesskit.

Limiting Resources such as CPU, Memory

Limiting resources with cgroup-related docker run flags such as --cpus--memory--pids-limit is supported only when running with cgroup v2 and systemd. See changing cgroup version to enable cgroup v2 for commands related to your distro.

If docker info shows none as Cgroup Driver, the conditions are not satisfied. When these conditions are not satisfied, rootless mode ignores the cgroup-related docker run flags.

Other not supported features

AppArmor, Checkpoint, Overlay network, Exposing SCTP Ports

Supported Storage Drivers

Only the following storage drivers are supported as of writing of this post:

  • overlay2 (only if running with kernel 5.11 or later, or Ubuntu-flavored kernel, or Debian-flavored kernel)
  • fuse-overlayfs (only if running with kernel 4.18 or later, and fuse-overlayfs is installed)
  • vfs

Rootless Docker vs Podman

Podman from RedHat Inc, is another popular container engine to run and manage containers. It hails running in rootless mode as one of its features over docker engine. With this Docker Inc, has bridged the gap and now they have almost the same features with almost the same performance. They also use lot of shared code between them.

Rootless Docker doesn’t support specifying docker run --net=host, but on the other hand, Rootless Podman doesn't support creating custom networks with docker network create`. But if you really need to use docker run --net=host, Podman might be a better choice for you.

Other than this, podman-compose is still a work in progress and cannot be considered a replacement of docker-compose.

3 thoughts on “Going rootless with Docker and Containers

  1. Have you ever tried as “rootless user” to do a docker login to a repo different to dockerhub? I’m not able to login it always fails.

    Like

  2. Thanks, I installed this for two different users on my system (ubuntu). For one it automatically selected the overlay2 for the other the vfs drivers. Not sure why. But now: how can I tell it to use overlay2 for the second rootless user? where is the daemon.json file (or similar)?

    Like

Leave a comment