From 26dea691cae2a50494402f06ee5725981163bbe1 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 21 Dec 2025 22:19:40 -0800 Subject: [PATCH] feat(docker): make container user configurable via CF_UID/CF_GID (#118) * feat(docker): make container user configurable via CF_UID/CF_GID Add support for running compose-farm containers as a non-root user to preserve file ownership on mounted volumes. This prevents files like compose-farm-state.yaml and web UI config edits from being owned by root on NFS mounts. Set CF_UID, CF_GID, and CF_HOME environment variables to run as your user. Defaults to root (0:0) for backwards compatibility. * docs: document non-root user configuration for Docker - Add CF_UID/CF_GID/CF_HOME documentation to README and getting-started - Add XDG config volume mount for backup/log persistence across restarts - Update SSH volume examples to use CF_HOME variable * fix(docker): allow non-root user access and add USER env for SSH - Add `chmod 755 /root` to Dockerfile so non-root users can access the installed tool at /root/.local/share/uv/tools/compose-farm - Add USER environment variable to docker-compose.yml for SSH to work when running as non-root (UID not in /etc/passwd) - Update docs to include CF_USER in the setup instructions - Support building from local source with SETUPTOOLS_SCM_PRETEND_VERSION * fix(docker): revert local build changes, keep only chmod 755 /root Remove the local source build logic that was added during testing. The only required change is `chmod 755 /root` to allow non-root users to access the installed tool. * docs: add .envrc.example for direnv users * docs: mention direnv option in README and getting-started --- .envrc.example | 6 ++++++ Dockerfile | 4 ++++ README.md | 24 ++++++++++++++++++++++-- docker-compose.yml | 24 ++++++++++++++++++++---- docs/getting-started.md | 19 +++++++++++++++++++ 5 files changed, 71 insertions(+), 6 deletions(-) create mode 100644 .envrc.example diff --git a/.envrc.example b/.envrc.example new file mode 100644 index 0000000..a915cde --- /dev/null +++ b/.envrc.example @@ -0,0 +1,6 @@ +# Run containers as current user (preserves file ownership on NFS mounts) +# Copy this file to .envrc and run: direnv allow +export CF_UID=$(id -u) +export CF_GID=$(id -g) +export CF_HOME=$HOME +export CF_USER=$USER diff --git a/Dockerfile b/Dockerfile index 07b7468..0ebde53 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,5 +16,9 @@ RUN apk add --no-cache openssh-client COPY --from=builder /root/.local/share/uv/tools/compose-farm /root/.local/share/uv/tools/compose-farm COPY --from=builder /usr/local/bin/cf /usr/local/bin/compose-farm /usr/local/bin/ +# Allow non-root users to access the installed tool +# (required when running with user: "${CF_UID:-0}:${CF_GID:-0}") +RUN chmod 755 /root + ENTRYPOINT ["cf"] CMD ["--help"] diff --git a/README.md b/README.md index 5235c4e..2968ed0 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,24 @@ docker run --rm \ ghcr.io/basnijholt/compose-farm up --all ``` +**Running as non-root user** (recommended for NFS mounts): + +By default, containers run as root. To preserve file ownership on mounted volumes +(e.g., `compose-farm-state.yaml`, config edits), set these environment variables: + +```bash +# Add to .env file (one-time setup) +echo "CF_UID=$(id -u)" >> .env +echo "CF_GID=$(id -g)" >> .env +echo "CF_HOME=$HOME" >> .env +echo "CF_USER=$USER" >> .env +``` + +Or use [direnv](https://direnv.net/) (copies `.envrc.example` to `.envrc`): +```bash +cp .envrc.example .envrc && direnv allow +``` + ## SSH Authentication @@ -216,13 +234,13 @@ When running in Docker, mount a volume to persist the SSH keys. Choose ONE optio **Option 1: Host path (default)** - keys at `~/.ssh/compose-farm/id_ed25519` ```yaml volumes: - - ~/.ssh/compose-farm:/root/.ssh + - ~/.ssh/compose-farm:${CF_HOME:-/root}/.ssh ``` **Option 2: Named volume** - managed by Docker ```yaml volumes: - - cf-ssh:/root/.ssh + - cf-ssh:${CF_HOME:-/root}/.ssh ``` Run setup once after starting the container (while the SSH agent still works): @@ -233,6 +251,8 @@ docker compose exec web cf ssh setup The keys will persist across restarts. +**Note:** When running as non-root (with `CF_UID`/`CF_GID`), set `CF_HOME` to your home directory so SSH finds the keys at the correct path. + ## Configuration diff --git a/docker-compose.yml b/docker-compose.yml index 8d78ca1..a6b178c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,10 @@ services: cf: image: ghcr.io/basnijholt/compose-farm:latest + # Run as current user to preserve file ownership on mounted volumes + # Set CF_UID=$(id -u) CF_GID=$(id -g) in your environment or .env file + # Defaults to root (0:0) for backwards compatibility + user: "${CF_UID:-0}:${CF_GID:-0}" volumes: - ${SSH_AUTH_SOCK}:/ssh-agent:ro # Compose directory (contains compose files AND compose-farm.yaml config) @@ -8,31 +12,43 @@ services: # SSH keys for passwordless auth (generated by `cf ssh setup`) # Choose ONE option below (use the same option for both cf and web services): # Option 1: Host path (default) - keys at ~/.ssh/compose-farm/id_ed25519 - - ${CF_SSH_DIR:-~/.ssh/compose-farm}:/root/.ssh + - ${CF_SSH_DIR:-~/.ssh/compose-farm}:${CF_HOME:-/root}/.ssh # Option 2: Named volume - managed by Docker, shared between services - # - cf-ssh:/root/.ssh + # - cf-ssh:${CF_HOME:-/root}/.ssh environment: - SSH_AUTH_SOCK=/ssh-agent # Config file path (state stored alongside it) - CF_CONFIG=${CF_COMPOSE_DIR:-/opt/stacks}/compose-farm.yaml + # HOME must match the user running the container for SSH to find keys + - HOME=${CF_HOME:-/root} + # USER is required for SSH when running as non-root (UID not in /etc/passwd) + - USER=${CF_USER:-root} web: image: ghcr.io/basnijholt/compose-farm:latest restart: unless-stopped command: web --host 0.0.0.0 --port 9000 + # Run as current user to preserve file ownership on mounted volumes + user: "${CF_UID:-0}:${CF_GID:-0}" volumes: - ${SSH_AUTH_SOCK}:/ssh-agent:ro - ${CF_COMPOSE_DIR:-/opt/stacks}:${CF_COMPOSE_DIR:-/opt/stacks} # SSH keys - use the SAME option as cf service above # Option 1: Host path (default) - - ${CF_SSH_DIR:-~/.ssh/compose-farm}:/root/.ssh + - ${CF_SSH_DIR:-~/.ssh/compose-farm}:${CF_HOME:-/root}/.ssh # Option 2: Named volume - # - cf-ssh:/root/.ssh + # - cf-ssh:${CF_HOME:-/root}/.ssh + # XDG config dir for backups and image digest logs (persists across restarts) + - ${CF_XDG_CONFIG:-~/.config/compose-farm}:${CF_HOME:-/root}/.config/compose-farm environment: - SSH_AUTH_SOCK=/ssh-agent - CF_CONFIG=${CF_COMPOSE_DIR:-/opt/stacks}/compose-farm.yaml # Used to detect self-updates and run via SSH to survive container restart - CF_WEB_STACK=compose-farm + # HOME must match the user running the container for SSH to find keys + - HOME=${CF_HOME:-/root} + # USER is required for SSH when running as non-root (UID not in /etc/passwd) + - USER=${CF_USER:-root} labels: - traefik.enable=true - traefik.http.routers.compose-farm.rule=Host(`compose-farm.${DOMAIN}`) diff --git a/docs/getting-started.md b/docs/getting-started.md index 9b7796f..02d8c06 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -54,6 +54,25 @@ docker run --rm \ ghcr.io/basnijholt/compose-farm up --all ``` +**Running as non-root user** (recommended for NFS mounts): + +By default, containers run as root. To preserve file ownership on mounted volumes, set these environment variables in your `.env` file: + +```bash +# Add to .env file (one-time setup) +echo "CF_UID=$(id -u)" >> .env +echo "CF_GID=$(id -g)" >> .env +echo "CF_HOME=$HOME" >> .env +echo "CF_USER=$USER" >> .env +``` + +Or use [direnv](https://direnv.net/) to auto-set these variables when entering the directory: +```bash +cp .envrc.example .envrc && direnv allow +``` + +This ensures files like `compose-farm-state.yaml` and web UI edits are owned by your user instead of root. The `CF_USER` variable is required for SSH to work when running as a non-root user. + ### Verify Installation ```bash