Stop Running Docker Containers as Root on Internet-Facing Hosts

If you run a Docker host that is reachable from the internet, one of the most effective security changes you can make costs nothing and takes five minutes: stop running your containers as root.

This post walks through creating a dedicated, tightly-scoped service account for running Docker workloads that need hardware access, such as GPU transcoding via /dev/dri. It explains why each permission is justified and why the account deliberately does not get sudo.

Why This Matters on an Internet-Facing Host

Docker containers, by default, run as root inside the container. If an attacker compromises a containerized application through a vulnerable web app, a bad dependency, or a malicious image, root inside the container is often a short path to root on the host. The route can be through kernel exploits, misconfigured volume mounts, or a Docker socket that was carelessly exposed.

The mitigation is straightforward: run the container processes as an unprivileged user, and make sure that user only has access to the specific resources it needs.

The Plan

The approach is to create a dedicated local user, localadmin, with a fixed UID/GID of 1000:1000. This account gets added to the docker group so it can manage containers without full root access. It also gets added to render and possibly video so it can access /dev/dri/* for GPU-accelerated transcoding. The account explicitly does not get sudo. Containers run with --user 1000:1000 and group_add so the container’s internal process inherits the same restricted access.

Step by Step

Create the User with a Fixed UID/GID

sudo groupadd -g 1000 localadmin
sudo useradd -u 1000 -g 1000 -m -s /bin/bash localadmin
sudo passwd localadmin

A fixed UID/GID of 1000:1000 matters because file permissions in Linux are stored as numeric IDs, not usernames. If your containers write to bind-mounted volumes such as config directories or media libraries, you want the numeric UID inside the container to match the UID outside so file ownership lines up cleanly on both sides. Picking a stable, well-known ID like 1000:1000 rather than whatever the next free ID happens to be makes this predictable across rebuilds, backups, and container migrations.

Add the User to the Docker Group

sudo usermod -aG docker localadmin

The docker group exists so users can talk to the Docker daemon socket at /var/run/docker.sock without sudo on every command. This is necessary for localadmin to manage containers directly.

Membership in the docker group is functionally equivalent to root on the host. Anyone in that group can mount the host filesystem into a container and read or write anything as root. This is not a sandboxed privilege. It is a well-known Docker footgun. It is granted here only because it is unavoidable for a user whose job is to operate Docker, and it is a deliberate, audited exception, not a default extended to every account on the box.

Add the User to Render and Video Groups for GPU Access

sudo usermod -aG render,video localadmin

The device nodes at /dev/dri/* used for hardware-accelerated transcoding, such as via Intel Quick Sync or VAAPI, are owned by the render and video groups on most distributions. You can check with ls -l /dev/dri/*. This grants localadmin exactly the device access needed for transcoding workloads and nothing else. It cannot read arbitrary devices, only these specific nodes.

This is the kind of grant that should be narrow. It is a device-level permission, not a filesystem or process-level one, so it does not expand the blast radius of a compromise beyond the ability to use the GPU.

Deliberately Withhold Sudo

This is arguably the most important line in the whole setup: the one you do not write.

# sudo usermod -aG sudo localadmin  <-- do NOT do this

The account localadmin exists to run Docker workloads, not to administer the host. It has no legitimate reason to install packages, edit system config, or become root. If a container process or an application running as localadmin is ever compromised, the attacker’s ceiling is control of Docker, which is already a serious problem as noted above, not the ability to become root outright and rewrite /etc/passwd, sshd_config, or cron jobs.

This keeps a clean separation of concerns. System administration happens under your personal sudo-capable account, with logging, MFA, or whatever hardening you have applied there. The localadmin account is a pure service account with an auditable, minimal set of group memberships. If you do need to fix something as localadmin, such as permissions on a mounted volume, do it via sudo -u localadmin from your own admin account, rather than giving localadmin its own path to root.

Carry the Same Restrictions into the Container

Group membership on the host does not automatically apply inside a container unless you tell Docker to carry it over. Here is how to do that in a Docker Compose file:

services:
  transcoder:
    image: your-image
    user: "1000:1000"
    devices:
      - /dev/dri:/dev/dri
    group_add:
      - "video"
      - "render"

The user: "1000:1000" setting ensures the process inside the container drops to the unprivileged UID immediately, rather than running as root-in-container, which is uncomfortably close to root-on-host if anything ever escapes the container boundary. The group_add directive passes through the specific supplementary groups the container needs, matching what you set up on the host in the earlier step.

Summary: The Reasoning at a Glance

PermissionGranted?Why
Fixed UID/GID 1000:1000YesPredictable file ownership across host and bind mounts
docker groupYesRequired to manage containers; accepted as a known, audited exception, not root-equivalent by accident, but by necessity
render / video groupYesNarrow, device-specific access for GPU transcoding only
sudoNoNo administrative need; limits the blast radius of any compromise to Docker access, not full root

The Bigger Picture

None of this makes a Docker host bulletproof. Membership in the docker group is still a significant privilege. If you want to go further, look into rootless Docker, user namespace remapping via userns-remap, or tools like Podman that avoid a privileged daemon entirely. But for a typical self-hosted setup exposed to the internet, a dedicated service account with exactly the permissions it needs, and explicitly not the ones it does not, is a solid, low-effort baseline that meaningfully reduces what an attacker gains from a single compromised container.

AI assistance is used on this site for language, formatting, and turning research into a consistent template. It is not used to perform the underlying research or verify technical claims. Every command, configuration, and step in this post is tested by hand before publication.

Leave a Reply

Your email address will not be published. Required fields are marked *

2 × one =