Switching Docker to Rootless Mode: A Hands-On Guide

In this hands-on guide, I take you step-by-step through switching my local Docker server to rootless mode. From reconfiguring services to tackling real-world challenges, discover how to transform your Docker environment into a more secure and robust system, even on a home server. Let’s dive in!

Switching Docker to Rootless Mode: A Hands-On Guide
Photo by Bernd 📷 Dittrich / Unsplash

Running Docker as the root user can pose significant security risks. By configuring Docker to run in rootless mode, you can enhance your system's security without sacrificing functionality. In this post, I will demonstrate how I switched my local Docker server—complete with a Jenkins service running in a container—to rootless mode. This guide will walk you through the steps, share solutions to challenges I encountered, and provide practical tips to ensure a smooth transition.

1. Overview of My Configuration

Before diving into the process, here is an overview of my setup:

  • Host: A virtual machine (VM) on VMware Fusion running on a Mac mini, which serves as my home server.
  • Operating System: Ubuntu Server.
  • Docker Setup: Installed as a system service with automatic startup.
  • Additional Configurations: Docker data is stored on an external disk, and a Jenkins service is running in a container.

Goals

  1. Transition Docker to rootless mode.
  2. Maintain existing configurations, including the external disk for Docker data storage.
  3. Ensure Jenkins and other services run seamlessly after the transition.
  4. Enhance overall system security by running Docker under a restricted, non-administrative user.

Current response from ps -ef | grep docker | grep -v grep:

ps -ef | grep docker | grep -v grep
root        1301       1  0 Dec17 ?        00:05:30 /usr/bin/dockerd --containerd=/run/containerd/containerd.sock
root       46111    1301  0 Dec20 ?        00:00:00 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 5000 -container-ip 172.18.0.2 -container-port 5000
root       46119    1301  0 Dec20 ?        00:00:00 /usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 5000 -container-ip 172.18.0.2 -container-port 5000
root       46128    1301  0 Dec20 ?        00:00:00 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 8080 -container-ip 172.18.0.2 -container-port 8080
root       46148    1301  0 Dec20 ?        00:00:00 /usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 8080 -container-ip 172.18.0.2 -container-port 8080

As you can see, the dockerd service is running as user root.

2. Preparing for the Transition

Step 1: Stop All Running Containers and Disable the Docker Service

To avoid issues during the transition, stop all running containers and disable the Docker service:

sudo systemctl stop docker
sudo systemctl disable docker

Verify that no Docker processes are running:

ps aux | grep docker

Step 2: Backup Your Docker Data

Ensure you back up all important data before making any changes:

tar -czvf docker_backup.tar.gz /path/to/docker/data

Please always backup before messing with important configurations.

3. Installing Required Packages

To enable rootless mode, install the necessary Docker components:

sudo apt update
sudo apt install docker-ce-rootless-extras

Confirm the installation:

dockerd-rootless-setuptool.sh check
[INFO] Requirements are satisfie

4. Configuring Rootless Mode

Step 1: Create a Non-Root User

If you don’t already have a non-root user, create one. In my setup, I use the user dolpa.

Step 2: Configure Docker for the User

From the user dolpa and set up Docker rootless mode:

dockerd-rootless-setuptool.sh install

During this process, a configuration directory will be created under ~/.config/docker/. This directory contains files specific to the rootless setup.

Step 3: Adjust Environment Variables

To ensure proper functionality, add the following lines to the user's shell configuration file (e.g., .bashrc or .zshrc):

export PATH=/usr/bin:$PATH
export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock

Reload the configuration:

source ~/.bashrc

The socket of Docker Service will not be available under the regular path /var/run/docker.sock in the rootless mode it will be /run/user/<user id>/docker.sock in my case: /run/user/1000/docker.sock.

5. Change docker configuration

The problem is my new service will not pick up any of my custom configurations. My old file /etc/docker/daemon.json will not be accessible to the new service. Move it and change owner:

mv /etc/docker/daemon.json ~/.config/docker/daemon.json
sudo chown dolpa:dolpa ~/.config/docker/daemon.json

In the file, ~/.config/docker/daemon.json change the socket file path from /var/run/docker.sock to unix:///run/user/1000/docker.sock. For example, my final daemon.json looks like this:

{
  "hosts": ["unix:///run/user/1000/docker.sock", "tcp://127.0.0.1:2375"],
  "data-root": "/DATA/docker-data/var/lib/docker/",
  "debug": true,
  "log-level": "info",
  "default-ulimits": {
    "nofile": {
      "Name": "nofile",
      "Hard": 8192,
      "Soft": 4096
    }
  }
}

Explanation:

  • "hosts": ["unix:///run/user/1000/docker.sock", "tcp://127.0.0.1:2375"]
    • This defines the endpoints where the Docker daemon listens for API requests.
    • "unix:///run/user/1000/docker.sock": Specifies a Unix domain socket for the Docker daemon to communicate with clients. The path here is user-specific (for user ID 1000) and typically used in rootless Docker setups.
    • "tcp://127.0.0.1:2375": Specifies that the Docker daemon listens for HTTP connections on the localhost (127.0.0.1) on port 2375. This enables local API access over TCP.
  • "data-root": "/DATA/docker-data/var/lib/docker/"
    • Defines the directory where Docker stores persistent data such as images, containers, volumes, and networks.
    • In this case, Docker data is redirected to a custom location (/DATA/docker-data/var/lib/docker/) instead of the default /var/lib/docker. This is useful for systems with limited root partition space or for better disk organization.
  • "debug": true
    • Enables debug mode for the Docker daemon.
    • When set to true, additional debug-level log messages are generated, which can help diagnose issues during configuration or runtime.
  • "log-level": "info"
    • Sets the logging level for the Docker daemon.
    • In this case, it is set to "info", meaning informational messages will be logged. Available levels include debug, info, warn, error, and fatal, where debug is the most verbose.
  • "default-ulimits": {}
    • Configures default system resource limits (ulimits) for Docker containers.
    • This ensures containers have predefined limits for resources such as the maximum number of open files, processes, etc.
  • "nofile": {} (nested under "default-ulimits")
    • Defines the ulimit for the number of open files (nofile) for containers.
    • "Name": "nofile": Indicates this limit is for open files.
    • "Hard": 8192: Sets the hard limit to 8192, meaning the maximum number of open files a container can have.
    • "Soft": 4096: Sets the soft limit to 4096, which is the default limit a container can use without explicitly requesting more.

The last thing I need to change is the Docker Data Directory permissions. Because Docker has already created some files, networks, volumes, and other staff that it needs during its work, all these files are owned by the user root because the service was running with root permissions. And better I fixed it before starting the service:

sudo chown dolpa:dolpa -R /DATA/docker-data/

Here, I'm changing the Owner and the Group of all the files and directories under the Docker Data Directory /DATA/docker-data.

6. Testing the Configuration

Step 1: Verify the Docker Service

Start the rootless Docker service:

systemctl --user start docker
systemctl --user enable docker

Check the status:

systemctl --user status docker

Step 2: Run a Test Container

Run a simple container to ensure Docker is working as expected:

docker run hello-world

7. Running Jenkins Server

Another issue that I needed to fix was file permissions inside the Docker Volume for Jenkins Server. As you remember, I have Jenkins Server running in Container on this server. To easier control over the server, I use docker-compose, with the following content:

# Jenkins Server - docker-compose.yaml

version: '3.8'
services:
  jenkins-server:
    image: jenkins/jenkins:2.430-jdk21
    environment:
      - JAVA_OPTS="-Dhudson.model.DownloadService.noSignatureCheck=true"
    ports:
      - "8080:8080"
      - "5000:5000"
    volumes:
      - jenkins_data:/var/jenkins_home/
      - /run/user/1000/docker.sock:/var/run/docker.sock
      - /usr/bin/docker:/usr/bin/docker
      - /usr/bin/docker-proxy:/usr/bin/docker-proxy
volumes:
  jenkins_data:
    external: true
    name: jenkins_data

Here, I have to make sure that files inside the jenkins_data volume belong to the same user that will run the server. Image jenkins/jenkins:2.430-jdk21 doesn't have any shell inside, so you can't run it in interactive mode and check the user and the permissions. You, in your case, will need to find the Dockerfile of the service. In my case, jenkins user ID is 1000. From here, I can mount my jenkins_data volume to any container and change the permissions as needed:

docker run -ti --rm -v jenkins_data:/tmp/jenkins_data dolpa/ubuntu:latest bash

Inside the Container:

ls -la /tmp/
total 12
drwxrwxrwt  1 root root 4096 Dec 22 20:50 .
drwxr-xr-x  1 root root 4096 Dec 22 20:50 ..
drwxr-xr-x 20 1000 1000 4096 Dec 22 18:44 jenkins_data

If it is not the correct user ID, you can change it:

chown -R 1000:1000 /tmp/jankins_data

From here, I can start the Jenkins service back by using docker-compose:

cd /DATA/projects/jenkins_server
docker-compose -f docker-compose.yml up -d

And the service should start with no issues 😄.

8. Handling Common Issues

Issue 1: Permissions on External Disk

If Docker cannot access the external disk, adjust the permissions:

sudo chown -R dolpa:dolpa /DATA/docker-data

Issue 2: Networking Problems

Rootless Docker uses slirp4netns for networking, which may cause issues with certain configurations. Install vpnkit or configure port forwarding manually as needed.

Issue 3: Jenkins Container

If Jenkins does not start properly, ensure its volumes and network settings are compatible with rootless Docker. Update the container's settings as needed.

Conclusion

Transitioning Docker to rootless mode significantly enhances security, even for home servers. By following this guide, I successfully reconfigured my Docker setup to operate under a non-root user while maintaining functionality and performance. If you encounter any issues or have additional questions, feel free to reach out or explore the resources linked below.

Next Steps: Share your experience or ask questions in the comments below. For more detailed guides, check out my other posts!

Read next

Understanding Docker Bench for Security Scores

Docker Bench for Security provides a comprehensive audit of your Docker environment, comparing it against CIS benchmarks to highlight potential security risks. At the end of the scan, a score is generated, reflecting how well your setup adheres to best practices.

Troubleshooting Docker Networking Problems

Docker networking is a crucial aspect of containerization, enabling communication between containers and between containers and external services. However, like any complex system, Docker networking can encounter issues that may disrupt application functionality and deployment.