Run Docker in Rootless Mode on Ubuntu

Move Docker onto a non-root user service on Ubuntu so you can reduce daemon privilege without pretending it is the right answer for every workload.

Rootless DockerUbuntuDaemon hardening
Illustrated guide cover for Run Docker in Rootless Mode on Ubuntu
Rootless Docker • Ubuntu • Daemon hardening
What you learn

How to verify prerequisites, install rootless Docker, manage it with systemctl --user, confirm the client is talking to the right socket, and roll back if needed.

Best for

Operators who already understand normal Docker basics and want a safer daemon model for some self-hosted services.

Risk to watch

The failure mode is treating rootless mode like a drop-in replacement for every existing production stack without checking networking, port, and cgroup limits first.

Before you begin

  • An Ubuntu host with a non-root user for Docker.
  • Sudo access for prerequisite packages and optional rollback work.
  • Comfort with user-scoped systemd services.
  • Willingness to test limitations before moving a real app.

Docker's rootless docs define the main benefit clearly: the daemon and containers run as a non-root user, reducing the blast radius of daemon and runtime vulnerabilities. That is worthwhile, but only if you stay honest about the tradeoffs.

Warning: Do not schedule a blind production cutover first. Start with one host and one low-risk workload so you can see which features behave differently in your environment.

Step 1: Verify the rootless prerequisites

The current Docker docs call out two hard prerequisites:

  • newuidmap and newgidmap must exist. On Ubuntu they come from the uidmap package.
  • The chosen user needs subordinate UID and GID ranges in /etc/subuid and /etc/subgid.

Check that now:

sudo apt update
sudo apt install -y uidmap
id -u
whoami
grep "^$(whoami):" /etc/subuid
grep "^$(whoami):" /etc/subgid

The Docker docs show a minimum subordinate range of 65,536 IDs. If your user does not have that, fix it before continuing.

Step 2: Install the rootless user service

If Docker was installed from packages, the rootless setup helper should already exist. Run it as the non-root user:

dockerd-rootless-setuptool.sh install

Docker's setup output normally creates ~/.config/systemd/user/docker.service, may create a rootless CLI context, and tells you to use systemctl --user to manage the daemon.

If the helper is missing, install the extras package first:

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

If you still have a system-wide rootful Docker daemon running, Docker's docs say to consider disabling it so you do not mistake a rootful engine for a rootless one:

sudo systemctl disable --now docker.service docker.socket

Do that only when you have a rollback path and you understand which workloads currently depend on the rootful daemon.

Step 3: Start and verify the user-scoped daemon

Start and enable the user service:

systemctl --user start docker
systemctl --user enable docker
sudo loginctl enable-linger $(whoami)

The docs note that lingering is what lets the user service survive beyond a login session. Then confirm the client is targeting the rootless daemon:

docker context ls
docker info

Look for the rootless security option in the server section. Some applications may also need the client socket exported explicitly:

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

Step 4: Check the important caveats before moving workloads

This is where most rootless writeups get sloppy. The Docker docs call out several limitations that matter in real operations:

  • Privileged ports below 1024 need extra handling.
  • Some resource limits depend on cgroup v2 and systemd.
  • Networking behavior is different enough that you should test published ports and app reachability directly.

If you need a low port like 80 or 443, Docker's tips page documents two approaches: give rootlesskit the CAP_NET_BIND_SERVICE capability or change net.ipv4.ip_unprivileged_port_start. Do not make either change casually if the host is shared or tightly controlled.

This is also the right moment to decide whether the target workload truly fits rootless mode. A small internal service is a better first candidate than the busiest edge-facing stack on the box.

Step 5: Test a small workload first

Use an easy test container before migrating a real app:

docker run --rm -d --name rootless-nginx -p 8080:80 nginx:alpine
docker ps
curl http://127.0.0.1:8080
docker rm -f rootless-nginx

If that works, try one real but low-risk service. Validate:

  • The service starts under the rootless daemon.
  • The published port is reachable as expected.
  • Volume paths and file ownership behave the way the app needs.
  • The user-scoped service survives a reboot.

Rollback and recovery notes

If the workload is not a fit, do not force it. Roll back cleanly:

systemctl --user stop docker
systemctl --user disable docker
docker context use default

If you disabled the system-wide daemon earlier and need to return to it:

sudo systemctl enable --now docker.service docker.socket

The point of this guide is not to win an ideological argument. It is to give you a safer daemon option when the workload and host support it honestly.

Troubleshooting common rootless problems

The setup tool says prerequisites are missing.
Install uidmap and fix /etc/subuid and /etc/subgid for the user.

docker info still shows the rootful daemon.
Check the active context, and export DOCKER_HOST if the client is not talking to the user socket you expect.

A service needs port 80 or 443 directly.
Review Docker's documented privileged-port caveats. Many teams keep a reverse proxy or system service on low ports instead of forcing every app to bind there under rootless mode.

Resource flags do not seem to apply.
Docker's tips page says cgroup-related limits depend on cgroup v2 and systemd delegation. Verify that before you assume memory or CPU limits are active.

What to do next: After you clean up remote admin and daemon privilege, the next Linux-native secret-handling upgrade is moving sensitive values out of env files where practical. Continue with Pass Secrets to systemd Services with Credentials Instead of Env Vars.