Docker UIs That Work With Rootless Docker
Which container UIs work when the Docker daemon is rootless: Portainer, Dockge, Lazydocker, Cockpit, Komodo, and the socket path that breaks them.
Which Docker UIs work with rootless Docker
A container UI works with rootless Docker when you can point it at a different socket path. It fails when it hardcodes /var/run/docker.sock. Lazydocker, Portainer CE, Dockge and Komodo can all be pointed at the rootless socket. Cockpit with the cockpit-podman add-on manages rootless Podman rather than Docker, and Yacht has not shipped a release since January 2023.
Versions checked in September 2026: Portainer CE on the lts tag, which was 2.45 in August 2026, Dockge 1.5.0, Lazydocker 0.25.0, the current Komodo Periphery install script, and cockpit-podman from the Ubuntu 24.04 archive. These projects change. Treat the version as part of the answer, and re-check the socket handling after a major upgrade.
Where does the rootless Docker socket live
A rootful daemon listens on /var/run/docker.sock. That file is owned by root with group docker, so membership of the docker group is what grants access to it.
A rootless daemon listens on $XDG_RUNTIME_DIR/docker.sock instead. XDG_RUNTIME_DIR (the per-user runtime directory) is created by systemd at login, and on a normal Linux system it is /run/user/<your uid>. Run id -u to get the number. On the first non-root account of a fresh VPS that is usually 1000, so the socket is /run/user/1000/docker.sock. No docker group is involved, because the daemon is not shared. Access is plain file ownership: the account that started the daemon owns the socket.
Install rootless mode with the packaged setup tool. docker-ce-rootless-extras comes from Docker's own apt repository, the one the engine install adds, so the first line below checks that repository is configured before the second line needs it.
grep -rl download.docker.com /etc/apt/sources.list.d/
sudo apt-get install -y docker-ce-rootless-extras
dockerd-rootless-setuptool.sh installA printed file name, usually /etc/apt/sources.list.d/docker.list or docker.sources, means the repository is there. No output means it was never added, and the install then stops at E: Unable to locate package docker-ce-rootless-extras however many times you run apt-get update, because Ubuntu's own archive carries nothing under that name. The Docker Engine install for a VPS adds the repository.
Its output carries the settings the rest of this guide depends on.
[INFO] Creating CLI context "rootless"
Current context is now "rootless"
[INFO] Make sure the following environment variable(s) are set (or add them to ~/.bashrc):
export PATH=/usr/bin:$PATH
export DOCKER_HOST=unix:///run/user/1000/docker.sock
[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 testuserDOCKER_HOST is the setting worth learning, because every program that speaks the Docker API reads it. The rootless CLI context is a Docker CLI feature stored under ~/.docker, and a UI honours it only if it uses the Docker SDK's context resolution. Set DOCKER_HOST and you do not need to know which one a given UI does.
Check which daemon you are actually talking to.
id -u
echo "$DOCKER_HOST"
docker info --format '{{.SecurityOptions}}'The last command should list name=rootless. If it prints security options without it, you reached the system daemon. If docker version instead fails with Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?, then DOCKER_HOST is unset and the CLI is still looking at the rootful path.
Two habits break rootless setups. The first is sudo. sudo docker ps builds a fresh environment without DOCKER_HOST, so the CLI falls back to /var/run/docker.sock and reaches the system daemon or nothing at all. Under rootless you never put sudo in front of docker.
The second is logging out. systemd stops your user manager shortly after your last session ends, which stops docker.service, which stops every container you started. Enable lingering once.
sudo loginctl enable-linger $USER
loginctl show-user $USER --property=LingerThe second command should print Linger=yes. Anything else means your containers die with your SSH session.
Portainer CE: change one path in the run command
Portainer's documented install mounts the system socket.
docker volume create portainer_data
docker run -d -p 8000:8000 -p 9443:9443 --name portainer --restart=always \
-v /var/run/docker.sock:/var/run/docker.sock \
-v portainer_data:/data \
portainer/portainer-ce:ltsFor a rootless daemon, change the source side of the socket mount and leave the container side alone, because Portainer looks for /var/run/docker.sock inside itself.
docker run -d -p 8000:8000 -p 9443:9443 --name portainer --restart=always \
-v "$XDG_RUNTIME_DIR/docker.sock:/var/run/docker.sock" \
-v portainer_data:/data \
portainer/portainer-ce:ltsPortainer publishes this same substitution, so it is a supported path rather than a trick. Before you run it, confirm the variable is set: echo "$XDG_RUNTIME_DIR" should print /run/user/1000. Some non-login shells and scheduled jobs do not set it, and an empty variable turns the mount source into /docker.sock, which is neither a socket nor a path your user can create. Write the full path when in doubt.
Open https://your-server:9443, create the admin account, and the local environment should show a container count on the home page. An environment shown as down, or one reporting zero containers while your shell lists several, means the socket mount is wrong.
Two rootless limits appear inside Portainer rather than at install time. Swarm does not work, because rootless Docker does not support overlay networks, so stay on the standalone environment type and ignore the Swarm screens. Publishing a container port below 1024 also fails, because the rootless port forwarder cannot bind privileged ports. The daemon's error names net.ipv4.ip_unprivileged_port_start and CAP_NET_BIND_SERVICE, which are the two available fixes: lower the unprivileged port floor with a file in /etc/sysctl.d/, or grant that capability to the rootlesskit binary. A floor of 0 lets any account on the box bind port 80, so set the lowest port you actually need instead.
Bind mounts behave differently too. Under a rootful daemon Portainer could mount any host path into a container. Under rootless the daemon runs as your account, so a stack that mounts /etc/ssl/private fails on permission. That refusal is the feature you installed rootless mode to get.
For the UI itself, Portainer on a VPS covers the setup and the day to day screens. The socket source is the only line that changes here.
Dockge: the stacks path matters more than the socket
Dockge manages compose stacks as files on disk. Its published compose.yaml mounts /var/run/docker.sock, mounts /opt/stacks on both sides of the bind, and sets DOCKGE_STACKS_DIR=/opt/stacks.
Under rootless both paths change. mkdir -p /opt/stacks needs root, so put the stacks directory inside your home instead, and keep the host path and the container path identical.
mkdir -p ~/dockge ~/stacksservices:
dockge:
image: louislam/dockge:1
restart: unless-stopped
ports:
- 127.0.0.1:5001:5001
volumes:
- /run/user/1000/docker.sock:/var/run/docker.sock
- ./data:/app/data
- /home/deploy/stacks:/home/deploy/stacks
environment:
- DOCKGE_STACKS_DIR=/home/deploy/stacksWrite the socket path literally rather than as a variable. Compose replaces an unset variable with an empty string and only prints a warning, so a missing XDG_RUNTIME_DIR gives you a broken mount instead of a stopped deploy. Replace 1000 with your id -u and deploy with your account name.
The matching stacks paths are not decoration. Dockge runs docker compose inside its own container against your daemon. The compose client turns a relative bind such as ./data into an absolute path using the directory the stack file sits in, which is a path inside the Dockge container. Your daemon then resolves that absolute path on the host. When the two sides differ, a stack's data lands somewhere you did not choose, and Dockge's own README warns about exactly this.
Bring it up, then check both ends.
cd ~/dockge && docker compose up -d
docker psDockge on port 5001 should list the same containers docker ps prints in your shell. An empty list in the browser next to a populated list in the terminal means Dockge holds a socket belonging to a different daemon, or to none.
Lazydocker: nothing to configure
Lazydocker is a terminal UI, and it is the least trouble of the group because it resolves the daemon the way the Docker CLI does. It reads DOCKER_HOST first, then the Docker context named by DOCKER_CONTEXT or set in its config file, then the platform default. Export the variable and it connects.
curl https://raw.githubusercontent.com/jesseduffield/lazydocker/master/scripts/install_update_linux.sh | bash
export PATH="$HOME/.local/bin:$PATH"
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock
lazydockerThe install script places the binary in $HOME/.local/bin when it is not run as root, so no part of this needs privileges. Put both export lines in ~/.bashrc so the next login still works.
Lazydocker never mounts a socket into a container, so it hands no authority to anything. Its config file lives at ~/.config/lazydocker/config.yml, and the compose command it shells out to defaults to docker compose, which picks up DOCKER_HOST from the same environment you started it in.
Cockpit with cockpit-podman: rootless is a switch in the UI
cockpit-podman speaks to Podman, not Docker, so it is not an answer for a rootless Docker daemon. It is a good answer if you have not committed to a runtime yet, because Podman has no long-running daemon and rootless is its ordinary mode rather than an opt-in.
sudo apt update && sudo apt install -y cockpit cockpit-podman
sudo systemctl enable --now cockpit.socket
systemctl --user enable --now podman.socket
sudo loginctl enable-linger $USERCockpit serves on port 9090. Its Podman page has a switch between system containers and user containers, and the user containers are the rootless ones belonging to the account you logged into Cockpit with. If the page reports Podman service is not active and offers a Start podman button, the user socket is not running, which is what systemctl --user enable --now podman.socket fixes. Confirm from the same account with systemctl --user status podman.socket and podman ps.
Lingering matters here for the same reason it does under Docker. Podman's API socket is a systemd user unit, so it stops when your last session ends unless the account lingers.
The runtime underneath is a real fork in the road, and the differences between Podman and Docker on a VPS are worth reading before you choose a UI to match. For a worked rootless Podman deployment, running Ollama under rootless Podman shows the user unit and the lingering step end to end.
Komodo: install the agent as a user service
Komodo splits into Core, the web UI, and Periphery, the agent that runs on each managed host. Periphery is the part rootless mode affects, because Periphery is the part that talks to the container runtime.
Install Periphery as a systemd user service with the --user flag.
curl -sSL https://raw.githubusercontent.com/moghtech/komodo/main/scripts/setup-periphery.py | python3 - --user --core-address "https://your-core-address" --onboarding-key "your-onboarding-key"
sudo loginctl enable-linger $USERKomodo's documentation says the account running Periphery must belong to the docker group. Under rootless there is no docker group to join, so hand Periphery the socket path instead.
systemctl --user edit periphery.service[Service]
Environment=DOCKER_HOST=unix:///run/user/1000/docker.sockA systemd user unit does not inherit exports from your interactive shell. Run systemctl --user show-environment and DOCKER_HOST will not be listed unless you put it there. Without the drop-in, the Docker CLI that Periphery calls falls back to /var/run/docker.sock, and Core then shows the server as connected with no containers on it, which reads like a Komodo bug and is not one.
systemctl --user restart periphery
systemctl --user status peripheryKomodo documents the group membership route, not this drop-in. The drop-in follows from how the Docker CLI picks its host, so confirm it on your own box before you build anything on top of it.
Yacht: check the release date first
Yacht still appears in older comparisons of container UIs. Its last published release is v0.0.7-alpha from January 2023, and it never reached a stable version. You could change its socket mount the same way you change Portainer's, but a container manager with no releases in over three years is a poor place to debug an unfamiliar socket path. Dockge and Portainer cover the same job with maintained code.
What mounting the socket into a UI actually grants
Every UI above except Lazydocker and Cockpit reaches the daemon through a socket you mounted into a container. That mount is not a narrow permission. Anything the Docker API can do, the UI container can do, including starting a second container that bind mounts the host filesystem and reads it. Nothing sits between "can reach the socket" and "controls the daemon".
Rootless mode changes what that authority is worth. The daemon runs as your account, so a compromised UI container cannot write /etc/shadow or load a kernel module. It can read everything your account can read: the private keys in ~/.ssh, the registry credentials in ~/.docker/config.json, the .env files beside your stacks, and the contents of every volume you own. The blast radius is smaller. It is not zero, so the account that owns the daemon should own nothing else you care about. Running it as a dedicated least privilege account is the difference between losing your containers and losing your server.
Mounting the socket read-only does not help. :ro makes the socket file read-only. It does not stop writes over the socket, because a Unix socket carries traffic in both directions once a process connects to it. A container with a read-only socket mount can still create and delete containers.
One rootless detail catches people who mix the two modes. Inside a rootless container the daemon's own user id maps to root, so a socket your account owns appears as uid 0 and the UI can use it. Mount a rootful socket into a rootless container and the same file appears owned by nobody, because host uid 0 sits outside the range mapped into the container, and the UI logs permission denied. Check which one you mounted before blaming the UI.
docker run --rm -v "$XDG_RUNTIME_DIR/docker.sock:/var/run/docker.sock" alpine ls -l /var/run/docker.sockA healthy rootless mount prints root root for owner and group. nobody nobody means you mounted the system daemon's socket into a rootless container.
Why the UI belongs on the host rather than in a container
Once the daemon is rootless, the strongest argument for a containerised UI weakens. Under a rootful daemon, putting the UI in a container kept root-level tooling off the host filesystem. Under a rootless daemon the UI runs as the same unprivileged account either way, so the container buys you packaging and costs you a mounted socket.
Running the UI as a systemd user service removes the mount. The process already runs as the account that owns the daemon, so it holds exactly the authority it would have held anyway, and no long-lived container sits there with an open API path.
[Unit]
Description=Container management UI
After=docker.service
[Service]
Environment=DOCKER_HOST=unix:///run/user/1000/docker.sock
ExecStart=/home/deploy/.local/bin/your-ui
Restart=on-failure
[Install]
WantedBy=default.targetSave that as ~/.config/systemd/user/your-ui.service, then run systemctl --user daemon-reload and systemctl --user enable --now your-ui. Lingering keeps it alive after logout, exactly as it does for docker.service and podman.socket.
There is a second reason, and it shows up during upgrades. A UI running inside a container that manages its own stack stops itself in the middle of the update, so the redeploy never finishes and you are left with a half-updated stack and no UI to fix it from. A UI on the host can restart every stack on the box, including one it is not part of. Komodo's Periphery agent runs on the host by default for this kind of reason.
Whichever shape you pick, bind the UI to localhost and reach it over an SSH tunnel rather than publishing the port.
ssh -L 9443:127.0.0.1:9443 deploy@your-serverRootless mode limits what a stolen session can do to the host. It does nothing about an unauthenticated web UI on a public port. If you are building the machine from scratch, installing Docker on a VPS covers the rootful baseline that all of these paths differ from.
FAQ
Why does my container UI show no containers under rootless Docker?
The UI is connected to a different daemon, or to none. A rootless daemon listens on $XDG_RUNTIME_DIR/docker.sock, usually /run/user/1000/docker.sock, while most install instructions mount or point at /var/run/docker.sock. Run docker info in your own shell and look for rootless in the Security Options block. If it is there and the UI still shows nothing, the UI is reading the system path. Change the socket source in its run command, or set DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock in the environment the UI actually starts in, which for a systemd user unit means a drop-in file and not your ~/.bashrc.
Does Portainer CE work with rootless Docker?
Yes, with one change. Mount $XDG_RUNTIME_DIR/docker.sock as the source and keep /var/run/docker.sock as the path inside the container, because that is where Portainer looks. Portainer publishes this substitution itself. Two rootless limits carry into the UI: Swarm does not work, because rootless Docker has no overlay network support, and publishing container ports below 1024 fails until you lower net.ipv4.ip_unprivileged_port_start or grant CAP_NET_BIND_SERVICE to the rootlesskit binary.
Is mounting the Docker socket into a UI safe when the daemon is rootless?
It is safer, and it is not safe. The socket is the full Docker API, so any container holding it can start another container that mounts the host filesystem. Under rootless, that authority is your user account rather than root, so an attacker gets your SSH keys, your registry credentials and your volumes rather than the whole machine. Adding :ro to the mount changes nothing, because it makes the socket file read-only while the API conversation still runs in both directions.
Why do my rootless containers stop when I log out of SSH?
The rootless daemon is a systemd user service, and systemd stops your user manager shortly after your last session ends. Run sudo loginctl enable-linger $USER, then confirm with loginctl show-user $USER --property=Linger, which should print Linger=yes. The same applies to Podman's podman.socket and to any UI you run as a user unit. A container's --restart=always policy does not save you here, because the daemon that would restart it has stopped too.
Can Cockpit manage a rootless Docker daemon?
No. The cockpit-podman add-on talks to Podman's API socket rather than Docker's, so it will not see a rootless Docker daemon at all. If you want a Cockpit page for your containers, run Podman rootless and enable the user socket with systemctl --user enable --now podman.socket. For Docker, use Lazydocker in a terminal or Portainer in a browser.