Running Docker containers as a non-root user is crucial for security, but it can feel like a minefield of permission errors and unexpected behavior.
Let’s see it in action. Imagine you have a simple Python web app that needs to write logs to a file inside the container.
# Dockerfile
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# This will fail if run as root and the user doesn't own /app
CMD ["python", "app.py"]
# app.py
import os
import datetime
log_dir = "/app/logs"
log_file_path = os.path.join(log_dir, "app.log")
if not os.path.exists(log_dir):
os.makedirs(log_dir)
with open(log_file_path, "a") as f:
f.write(f"[{datetime.datetime.now()}] Application started.\n")
If we build and run this with default root privileges:
# Build the image
docker build -t my-nonroot-app .
# Run the container
docker run --rm my-nonroot-app
You’ll likely get an Permission denied error when the app.py script tries to create the /app/logs directory or write to the log file. The problem is that the default user inside the container is root, and the /app directory (created by WORKDIR and populated by COPY) is owned by root.
The core problem this solves is the principle of least privilege. By default, containers run as root, meaning any vulnerability exploited within the container has root access to the container’s filesystem. If the container is compromised, the attacker has root privileges inside the container, which can then be escalated to the host system through various misconfigurations. Running as a non-root user significantly limits the blast radius of a security breach.
Here’s how to manage it:
1. Create a Non-Root User in the Dockerfile
The most common and recommended approach is to define a user and group within your Dockerfile and switch to it.
Diagnosis: You see Permission denied errors when the container tries to write to directories or files that were created by COPY or RUN commands executed as root.
Cause & Fix:
- Cause: The default user inside the container is
root(UID 0, GID 0), and the directories created byCOPYorRUN(like/appin our example) are owned byroot. - Diagnosis Command:
This will showdocker run --rm -it my-nonroot-app sh # Inside the container: ls -ld /app id/appowned byrootandidwill showuid=0(root) gid=0(root). - Fix: Add user creation and switching commands to your Dockerfile.
# Dockerfile FROM python:3.9-slim WORKDIR /app # Create a non-root user and group RUN groupadd --gid 1001 appgroup && \ useradd --uid 1001 --gid appgroup --shell /bin/bash --create-home appuser COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . # Change ownership of the app directory to the new user RUN chown -R appuser:appgroup /app # Switch to the non-root user USER appuser CMD ["python", "app.py"] - Why it works: We explicitly create a user (
appuser) and group (appgroup) with specific UIDs/GIDs. Then, we change the ownership of the/appdirectory (and everything within it) to this new user. Finally,USER appusertells Docker to run subsequent commands and the entrypoint/CMD as this user. TheUID 1001andGID 1001are arbitrary but good practice to use non-zero values.
2. Explicitly Set Permissions on Volumes
If you’re mounting host volumes into the container, you need to ensure the user inside the container has permissions to access them.
Diagnosis: Your application fails to write to a host-mounted directory within the container.
Cause & Fix:
- Cause: The host directory is owned by a user on your host machine, and the user inside the container (even if non-root) has a different UID/GID and therefore no write permissions.
- Diagnosis Command:
You’ll see the host directory ownership, and inside the container, the# On your host machine ls -ld /path/to/your/host/data/dir # Run container, then inside: docker exec -it <container_id> sh id ls -ld /path/in/container/mounted/diridcommand will show the container user’s UID/GID, which likely doesn’t match the host’s ownership. - Fix:
- Option A (Recommended): Ensure the UID/GID of the user inside your container matches the UID/GID of the user/group that owns the host directory. You can achieve this by passing the host user’s UID/GID to the
useraddcommand in your Dockerfile, or by dynamically creating the user with the correct UID/GID at runtime (more complex).- Dockerfile Modification:
# Example: Assume you know the host UID/GID is 1000 RUN groupadd --gid 1000 appgroup && \ useradd --uid 1000 --gid appgroup --shell /bin/bash --create-home appuser # ... rest of Dockerfile USER appuser - Runtime Override (less common for this specific problem):
This runs the container process with your host user’s UID and GID.docker run -u $(id -u):$(id -g) ... my-app
- Dockerfile Modification:
- Option B: Change the ownership of the host directory to match the container user’s UID/GID (if you control the host).
# On your host machine, assuming container user is UID 1001, GID 1001 sudo chown -R 1001:1001 /path/to/your/host/data/dir
- Option A (Recommended): Ensure the UID/GID of the user inside your container matches the UID/GID of the user/group that owns the host directory. You can achieve this by passing the host user’s UID/GID to the
- Why it works: File system permissions are based on UIDs and GIDs. By making the container user’s UID/GID match the owner of the host directory, the container process gains the necessary read/write access.
3. Using docker-compose for Volume Permissions
When using docker-compose, managing volume permissions can be simplified.
Diagnosis: Similar to the previous point, but specifically when using docker-compose and encountering permission issues with mounted volumes.
Cause & Fix:
- Cause: The default user in the container (or the user you’ve specified) doesn’t have permissions on the host-mounted volume because of UID/GID mismatches.
- Diagnosis Command: Same as point 2.
- Fix: You can use the
userdirective in yourdocker-compose.ymlto specify the user and group ID.
You would then run this with:# docker-compose.yml version: '3.8' services: app: build: . # Run as the current host user's UID and GID user: "${HOST_UID:-1000}:${HOST_GID:-1000}" volumes: - ./data:/app/data
Or, if your Dockerfile already creates a specific user (e.g.,# On your host, ensure DATA directory exists and is writable by your user mkdir data echo "HOST_UID=$(id -u)" > .env echo "HOST_GID=$(id -g)" >> .env docker-compose upappuserwith UID 1001), you can specify that:# docker-compose.yml version: '3.8' services: app: build: . user: "1001:1001" # Matches appuser:appgroup from Dockerfile volumes: - ./data:/app/data - Why it works: The
userdirective indocker-compose.ymloverrides theUSERinstruction in the Dockerfile for the entrypoint and CMD. It directly tells the Docker daemon to start the container’s primary process with the specified UID and GID. This is particularly useful for development environments where you want seamless access to host volumes.
4. Handling Mutable Data Directories
Sometimes, your application needs to write to directories that aren’t explicitly mounted volumes but are part of the container’s filesystem.
Diagnosis: Your application fails to create subdirectories or write files within a directory that is not a host-mounted volume but is expected to be writable.
Cause & Fix:
- Cause: The directory was created by a
RUNcommand (e.g.,mkdir /data) or inherited from the base image, and it’s owned byroot. Your non-rootUSERcannot write to it. - Diagnosis Command:
You’ll seedocker run --rm -it my-app sh # Inside the container: ls -ld /data id/dataowned byrootandidshowing your non-root user. - Fix: Use
RUN chownto change ownership of the directory after it’s created and before switching theUSER.# Dockerfile FROM python:3.9-slim # ... other setup ... # Create a directory that the app needs to write to RUN mkdir /app/writable_data # Change ownership to the non-root user/group RUN chown appuser:appgroup /app/writable_data # Switch user USER appuser # ... rest of Dockerfile ... - Why it works: By explicitly changing the ownership of the target directory to the non-root user before switching the
USERinstruction, you ensure that the non-root user will have write permissions when the container starts.
5. Beware of Entrypoint Scripts
If your container uses an ENTRYPOINT script, the permissions for that script itself, and the directories it interacts with, are critical.
Diagnosis: Your container fails to start, often with errors related to executing the entrypoint script or accessing files within the entrypoint script.
Cause & Fix:
- Cause: The
ENTRYPOINTscript might be owned byrootand not executable by the non-root user, or it might try to create/write to directories that are not owned by the non-root user. - Diagnosis Command:
Check if the script has execute permissions (docker run --rm -it my-app sh # Inside the container: ls -l /path/to/your/entrypoint.sh cat /path/to/your/entrypoint.sh idx) for your user. - Fix:
- Ensure the
ENTRYPOINTscript has execute permissions for the user. You can do this by addingRUN chmod +x /path/to/your/entrypoint.shin your Dockerfile before theUSERinstruction. - If the entrypoint script itself creates directories or files, ensure those are also
chowned appropriately before theUSERswitch. - If you’re using
CMD ["executable", "param1"]andENTRYPOINT ["/path/to/entrypoint.sh"], theENTRYPOINTscript runs first. If it thenexecs theCMD, theexeced process will inherit the user that ran theENTRYPOINT.
- Ensure the
- Why it works: The entrypoint script is the first thing executed. If it’s not executable by the intended user or fails due to permissions, the container will not start correctly. Ensuring its permissions and the permissions of any files/directories it manipulates is key.
The next hurdle you’ll likely encounter is managing the permissions of files created by the application itself at runtime, especially if your application generates files in shared volumes or temporary directories.