Migrates 20 topic files from claude-foundations/best-practices/ to this standalone repo. Adds BESTPRACTICES.md index, CLAUDE.md conventions, and updated README.md. Container agents clone this repo to /best-practices. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
114 lines
5.6 KiB
Markdown
114 lines
5.6 KiB
Markdown
# 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:**
|
|
|
|
```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:**
|
|
|
|
```bash
|
|
#!/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`. **Simpler alternative:** Delete the conflicting user at build time (`RUN userdel ubuntu` in the Dockerfile). This avoids runtime conflict handling entirely and is preferred when you control the image.
|
|
|
|
- **SSH agent socket forwarding requires UID match.** When mounting `$SSH_AUTH_SOCK` into a container, the socket is mode 0600 owned by the host UID. The container process must run as the same UID to use it — which the UID wrapper handles naturally. Set `SSH_AUTH_SOCK` in the container env to the mounted path. Note: private keys cannot be extracted via the agent protocol — it only supports signing operations.
|
|
|
|
- **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.
|