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
- Transition Docker to rootless mode.
- Maintain existing configurations, including the external disk for Docker data storage.
- Ensure Jenkins and other services run seamlessly after the transition.
- 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 8080As 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.jsonIn 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 includedebug,info,warn,error, andfatal, wheredebugis 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.
- Configures default system resource limits (
"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.
- Defines the ulimit for the number of open files (
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_dataHere, 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 bashInside 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_dataIf it is not the correct user ID, you can change it:
chown -R 1000:1000 /tmp/jankins_dataFrom here, I can start the Jenkins service back by using docker-compose:
cd /DATA/projects/jenkins_server
docker-compose -f docker-compose.yml up -dAnd 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!