How to Run a Docker Container as a Non-Root User
By default, processes inside a Docker container run as root (UID 0). To set up a Docker non-root user, add a USER instruction to your Dockerfile pointing at an unprivileged account, or pass --user at runtime with docker run --user 1000:1000. For the strongest isolation, run the whole Docker daemon in rootless mode so a container escape never lands on a root-owned host.
Running as root inside a container is one of those defaults that works fine until it doesn't. The problem is that container root maps to host root unless you've set up user namespaces. If an attacker breaks out of a container running as UID 0, they land on your host with UID 0. That's the whole reason this matters.
Why a Docker non-root user matters
When you write a plain Dockerfile with no USER line, every command in the container runs as root. The container's root is not sandboxed from the host's root by default. They share the same UID. A bind-mounted directory written by container-root ends up owned by host-root, and a kernel exploit or a misconfigured --privileged flag turns a container breakout into full host compromise.
Most applications never need root. A web server, a worker process, a CLI tool, none of them require UID 0 once they're past binding to a low port or installing packages at build time. So the fix is to drop privileges before your app starts.
Method 1: The USER instruction in your Dockerfile
This is the approach you want for images you build yourself. Create a user during the build, then switch to it.
FROM node:20-slim
# Create a non-root user and group
RUN groupadd --gid 1001 appgroup \
&& useradd --uid 1001 --gid appgroup --shell /bin/bash --create-home appuser
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
# Copy app code and hand ownership to the non-root user
COPY --chown=appuser:appgroup . .
# Everything after this line runs as appuser
USER appuser
CMD ["node", "server.js"]
A few things that trip people up here.
Run apt-get install, npm ci, and any other privileged setup before the USER line. Once you switch users, those commands fail with permission errors because the unprivileged account can't write to system directories.
Use COPY --chown so your application files are owned by the runtime user. If you copy as root and then switch to appuser, your app may not be able to write logs, caches, or temp files.
Many official images already ship a non-root user. The node image has a node user (UID 1000). Postgres has postgres. Nginx is trickier because it needs to bind port 80, but the nginxinc/nginx-unprivileged image solves that by listening on 8080 as a non-root user.
Binding to low ports
If your app must listen on a port below 1024, you have two clean options. Either listen on a high port inside the container and map it (-p 80:8080), or grant the binary the capability to bind low ports:
RUN setcap 'cap_net_bind_service=+ep' /usr/local/bin/myapp
USER appuser
The port-mapping route is simpler and it's what I reach for first.
Method 2: The --user flag at runtime
When you can't modify the image, override the user at docker run time:
# Run as UID 1000, GID 1000
docker run --user 1000:1000 myimage
# Match your own host user so bind-mounted files stay writable
docker run --user "$(id -u):$(id -g)" -v "$PWD:/data" myimage
That second command is the one that saves you from the classic "files created in the container are owned by root on my host" headache. By passing your own UID and GID, files written to a bind mount come out owned by you.
The catch: the UID you pass has to make sense inside the container. If the app expects a home directory or a matching entry in /etc/passwd and there isn't one for that UID, some tools complain. For most stateless processes it just works.
In Docker Compose the same setting looks like this:
services:
app:
image: myimage
user: "1000:1000"
Method 3: Rootless Docker
The two methods above stop your container process from being root. Rootless mode goes further and runs the Docker daemon itself without root privileges. The daemon and containers run inside a user namespace, so even container-root maps to an unprivileged UID on the host.
Install it for your user:
curl -fsSL https://get.docker.com/rootless | sh
# Point the CLI at the rootless socket
export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock
systemctl --user start docker
Rootless mode has real limits. Some networking features and storage drivers behave differently, and binding privileged ports needs extra configuration. But for CI runners, shared dev boxes, and multi-tenant hosts, it removes an entire class of risk. If you're deciding how to lock down a production host, it belongs in the conversation alongside the other controls in our guide to Docker security best practices for production.
Verifying it actually worked
Don't assume. Check.
# Should print your non-root UID, not 0
docker exec my-container id
# Or inspect the configured user without entering the container
docker inspect --format '{{.Config.User}}' my-container
If id returns uid=0(root), something overrode your intent. Common causes: an entrypoint script that runs USER root, a base image that resets the user, or a docker run --user root in a script you forgot about.
When you manage servers you don't sit in front of, spot-checking this by hand gets old. Docker HQ lets you open a shell into any container on a remote host straight from your phone, so running id on that box you deployed to last week takes about ten seconds. It's the same check, just without SSHing in from a laptop you don't have on you.
Extra hardening worth doing at the same time
Once you're running non-root, a few flags cost nothing and close more gaps:
docker run \
--user 1000:1000 \
--read-only \
--cap-drop ALL \
--security-opt no-new-privileges \
--tmpfs /tmp \
myimage
--cap-drop ALL strips every Linux capability, then you add back only what you need with --cap-add. --read-only makes the root filesystem immutable, with a writable --tmpfs for scratch space. --security-opt no-new-privileges stops a process from gaining privileges through setuid binaries. Together with a non-root user, these make a breakout much less useful to an attacker.
If your remote host exposes the Docker API over the network, none of the above helps if that endpoint is open, so pair this work with securing the Docker remote API. And if you administer these hosts from a phone, the SSH keys on that device are worth protecting properly, which we cover in how to protect SSH keys on a mobile device.
Frequently asked questions
How do I check which user a Docker container is running as?
Run docker exec <container> id to see the live UID and GID, or docker inspect --format '{{.Config.User}}' <container> to read the configured user without entering the container. If id returns uid=0(root), the container is still running as root regardless of what you intended.
Why do I get permission denied errors after adding a USER instruction?
Usually because a privileged step runs after the USER line, or because your app files are still owned by root. Move all apt-get, npm install, and similar commands above the USER instruction, and copy application code with COPY --chown=appuser:appgroup. For writable runtime directories, RUN mkdir /app/data && chown appuser /app/data before switching users.
Does running as a non-root user fully isolate the container from the host?
No. A non-root USER inside a container reduces the blast radius of a breakout, but the container still shares the host kernel. For stronger isolation, combine it with rootless Docker, dropped capabilities, no-new-privileges, and user namespace remapping (userns-remap in the daemon config).
Can I run an existing image as non-root without rebuilding it?
Yes, use docker run --user 1000:1000 <image> or set user: in Compose. This works for most stateless apps. It can fail if the image hardcodes paths owned by root or expects a matching /etc/passwd entry, in which case rebuilding with a proper USER and correct ownership is the reliable fix.
Start with one service. Add a USER line to its Dockerfile, rebuild, run docker exec <container> id, and confirm it comes back non-zero. Once that pattern is proven, apply it across the rest of your images and make the id check part of your deploy checklist.