Compared to a decade ago, when we had to sift through obscure websites and multiple forum posts just to deploy a FOSS application, the self-hosting ecosystem has become a lot more approachable these days. Part of that can be attributed to Docker’s rise in popularity, its first-party tools, and a massive collection of images that work right out-of-the-box. But despite its beginner-friendly nature, Docker has a couple of annoying quirks that can get in the way of your projects.
The worst among these is its tendency to run containerized environments with root privileges, and it’s especially terrible that most guides and learning resources tend to gloss over Docker’s rootless operation mode. So, here’s a piece on how you can secure your self-hosting workstation by disabling root access for your Docker-powered containers.
5 Docker mistakes beginners make in their first month
We're all guilty of making them
Leaving Docker containers with root-level access is a terrible idea
Especially with containers sharing the kernel with the host machine
Although containers consume significantly fewer resources than their virtual machine counterparts, they lose on the isolation front. After all, containerized environments share the underlying kernel resources with the host machine, and running everything as the root user can make it easy for malware to escape the container and affect the host machine. Typical containerization platforms (like LXC and Podman) run services as an unprivileged user by default, and you’d have to change a bunch of settings deliberately if you want to run your FOSS images with escalated permissions.
Unfortunately, Docker deploys containers with root privileges by default. And leaving sudo out of your Docker commands on a barebones setup will just result in a "permission denied" error. Once you run a Docker image in its default mode, the processes running inside the container with the User ID of 0 have the same UID on the host. Essentially, the containerized services get mapped to the root user on the host, and this can expose your system to a bunch of problems.
If malware breached your container, it’d have an easier time escaping the clutches of the virtual environment and targeting your host machine. While Docker has some protections to deter malicious scripts from wreaking havoc on your system, running the containers with the same root UID as the underlying server is a bad idea from a security standpoint.
There are a couple of caveats to Docker’s rootless mode
As much as I’d like to claim that you should always run Docker containers without root access, you should be aware of some issues with enabling rootless mode. For starters, you’ll have some trouble mapping containers to the privileged ports (port numbers lower than 1024) unless you manually tweak the rootlesskit binary. Likewise, cgroups require some extra steps (and caveats), and the same holds true for the ping command. Sadly, AppArmor, checkpoints, and overlay networks don’t work on a rootless setup, and you can’t expose SCTP ports, either.
Fortunately, enabling rootless mode is pretty simple on Docker
The official Docker script makes this process a cakewalk
When I first realized my Docker setup was running containers as root (albeit inside a virtual machine), I assumed the rootless mode would be a real pain to configure. Luckily, Docker has released a convenient script that forces the container runtime’s daemon to deploy environments inside a user namespace without root-level access.
First, you’ll have to install the uidmap package to map the UIDs inside your containers to an unprivileged UID on the host. On a Debian machine such as mine, it was as simple as running sudo apt install uidmap. After that, you can pull the script via the curl command and use the pipe operator (|) followed by sh to execute it: curl -fsSL https://get.docker.com/rootless | sh. The overall process should take a couple of seconds, and you’ll want to run export PATH=/usr/bin:$PATH afterward. If you encounter issues in certain containers, you might want to configure the DOCKER_HOST variable by executing export DOCKER_HOST=unix:///run/user/1000/docker.sock in the terminal.
Or, you can just switch to Podman
I love Docker as much as the next self-hosting enthusiast, but after dipping my feet into the Podman ecosystem, I’ve started using the latter a lot in my DevOps projects. And there are plenty of reasons for that besides the fact that Podman runs containers without root access. Unlike Docker, Podman doesn’t rely on a daemon process to manage containerized environments, making it faster in resource-constrained systems (especially cheap SBCs with 512MB memory). The pod aspect of the container runtime makes inter-container networking and storage management a lot easier, and I daresay the Podman Desktop app leaves its Docker equivalent in the dust with its extensive functionality. The only caveats are the learning curve and the slight difficulty in adapting certain images to the Podman ecosystem.
