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 by COPY or RUN (like /app in our example) are owned by root.
  • Diagnosis Command:
    docker run --rm -it my-nonroot-app sh
    # Inside the container:
    ls -ld /app
    id
    
    This will show /app owned by root and id will show uid=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 /app directory (and everything within it) to this new user. Finally, USER appuser tells Docker to run subsequent commands and the entrypoint/CMD as this user. The UID 1001 and GID 1001 are 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:
    # 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/dir
    
    You’ll see the host directory ownership, and inside the container, the id command 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 useradd command 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):
        docker run -u $(id -u):$(id -g) ... my-app
        
        This runs the container process with your host user’s UID and GID.
    • 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
      
  • 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 user directive in your docker-compose.yml to specify the user and group ID.
    # 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
    
    You would then run this with:
    # 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 up
    
    Or, if your Dockerfile already creates a specific user (e.g., appuser with 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 user directive in docker-compose.yml overrides the USER instruction 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 RUN command (e.g., mkdir /data) or inherited from the base image, and it’s owned by root. Your non-root USER cannot write to it.
  • Diagnosis Command:
    docker run --rm -it my-app sh
    # Inside the container:
    ls -ld /data
    id
    
    You’ll see /data owned by root and id showing your non-root user.
  • Fix: Use RUN chown to change ownership of the directory after it’s created and before switching the USER.
    # 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 USER instruction, 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 ENTRYPOINT script might be owned by root and 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:
    docker run --rm -it my-app sh
    # Inside the container:
    ls -l /path/to/your/entrypoint.sh
    cat /path/to/your/entrypoint.sh
    id
    
    Check if the script has execute permissions (x) for your user.
  • Fix:
    • Ensure the ENTRYPOINT script has execute permissions for the user. You can do this by adding RUN chmod +x /path/to/your/entrypoint.sh in your Dockerfile before the USER instruction.
    • If the entrypoint script itself creates directories or files, ensure those are also chowned appropriately before the USER switch.
    • If you’re using CMD ["executable", "param1"] and ENTRYPOINT ["/path/to/entrypoint.sh"], the ENTRYPOINT script runs first. If it then execs the CMD, the execed process will inherit the user that ran the ENTRYPOINT.
  • 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.

Want structured learning?

Take the full Docker course →