Files
claude-foundations/best-practices/docker-uid-matching.md
Paul O'Reilly 752fdfd82e Add Docker UID matching best practice
Pattern for matching container user UID/GID to mounted volume owner
via a gosu-based entrypoint wrapper. Covers UID conflicts (Ubuntu 24.04
ships ubuntu:1000), K8s securityContext compatibility, and alternatives.

Learned from agent-runtimes M1 where Claude Code refuses
--dangerously-skip-permissions as root.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 15:31:40 +13:00

5.0 KiB

Docker UID Matching for Mounted Volumes

When a host directory is mounted into a Docker container, files created by the container process are owned by the container user's UID/GID. If this doesn't match the host user, you get one of two problems:

  1. Permission denied — the container can't write to the mounted directory
  2. Wrong ownership — files created inside the container are owned by a different user on the host (e.g., root or UID 1001)

Both are common sources of friction in developer-facing container tools and CI/CD pipelines.

The Pattern: UID Wrapper Entrypoint

The solution is a small entrypoint wrapper script that:

  1. Starts as root
  2. Detects the UID/GID of the mounted directory via stat
  3. Adjusts the container user's UID/GID to match using usermod/groupmod
  4. Drops privileges via gosu and execs the real command

This is transparent to the user — they don't need to pass --user flags or know their UID.

Implementation

Dockerfile:

FROM ubuntu:24.04

RUN apt-get update && apt-get install -y --no-install-recommends gosu \
    && rm -rf /var/lib/apt/lists/*

# Create non-root user
RUN useradd -m -s /bin/bash agent
RUN mkdir -p /project && chown agent:agent /project

COPY uid-wrapper.sh /usr/local/bin/uid-wrapper.sh

# Start as root — the wrapper drops privileges after UID adjustment
ENTRYPOINT ["uid-wrapper.sh", "your-actual-command"]

uid-wrapper.sh:

#!/bin/bash
set -e

TARGET_DIR="/project"    # The mount point to match
AGENT_USER="agent"       # The non-root user to adjust

# If not root, skip adjustment (e.g., K8s securityContext sets UID)
if [ "$(id -u)" != "0" ]; then
    exec "$@"
fi

if [ -d "$TARGET_DIR" ]; then
    HOST_UID=$(stat -c '%u' "$TARGET_DIR")
    HOST_GID=$(stat -c '%g' "$TARGET_DIR")

    if [ "$HOST_UID" != "0" ]; then
        # Adjust GID if different
        if [ "$HOST_GID" != "$(id -g $AGENT_USER)" ]; then
            groupmod -g "$HOST_GID" "$AGENT_USER" 2>/dev/null || true
        fi
        # Adjust UID if different
        if [ "$HOST_UID" != "$(id -u $AGENT_USER)" ]; then
            usermod -u "$HOST_UID" "$AGENT_USER" 2>/dev/null || true
        fi
        # Fix home directory ownership
        chown -R "$AGENT_USER:$(id -g $AGENT_USER)" /home/"$AGENT_USER" 2>/dev/null || true
    fi
fi

exec gosu "$AGENT_USER" "$@"

Key Details

  • gosu over su/sudo. gosu execs directly (PID 1 becomes the real process), while su creates a child process that breaks signal handling. gosu is the standard tool for this pattern.

  • Handle existing UIDs/GIDs. The host UID may already be taken by another user in the container. Ubuntu 24.04 images ship with a ubuntu user at UID 1000 — the most common host UID. The wrapper must evict the conflicting user to a high unused UID (e.g., 59999 and search downward) before assigning the target UID to the agent user. usermod -u <new> <conflicting_user> followed by usermod -u <target> agent. Same applies to GIDs — use getent group to check before groupmod.

  • Skip when not root. In Kubernetes, securityContext.runAsUser sets the UID before the container starts. The wrapper detects this (id -u != 0) and skips adjustment — the platform is handling it.

  • Mount point owned by root. If the mounted directory is owned by root (UID 0), don't adjust — running as root defeats the purpose. The wrapper only adjusts for non-root UIDs.

  • Fix home directory. After usermod, the user's home directory still has the old UID. chown -R fixes this. Skip other directories — only fix what the user needs.

When to Use

  • Developer-facing container tools where users mount local project directories (e.g., running a CLI tool inside a container)
  • CI/CD containers that write build artifacts back to a mounted workspace
  • Any container that writes to a host-mounted volume and the output needs correct ownership

When NOT to Use

  • Kubernetes with securityContext — the platform handles UID assignment; the wrapper correctly skips adjustment in this case
  • Containers that don't write to mounted volumes — unnecessary overhead
  • Images that must run as root — the wrapper's purpose is to run as non-root; if you need root, you don't need the wrapper

Alternatives

Approach Pros Cons
docker run --user $(id -u):$(id -g) Simple, no image changes No home directory, no /etc/passwd entry, breaks tools that need a user identity
fixuid Purpose-built tool Another binary to install and maintain
UID wrapper (this pattern) Transparent, handles edge cases, works in Docker and K8s Requires gosu in image, starts as root
Named volumes only Docker manages ownership Can't mount host directories

Source

Learned from the agent-runtimes project (M1) where Claude Code refuses --dangerously-skip-permissions as root for security reasons. The container needed to run as non-root, but files created in mounted project directories needed to be owned by the host user. The UID wrapper solved both problems transparently.