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
This commit is contained in:
Bas Nijholt
2025-12-21 22:19:40 -08:00
committed by GitHub
parent 56d64bfe7a
commit 26dea691ca
5 changed files with 71 additions and 6 deletions

6
.envrc.example Normal file
View File

@@ -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

View File

@@ -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 /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/ 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"] ENTRYPOINT ["cf"]
CMD ["--help"] CMD ["--help"]

View File

@@ -177,6 +177,24 @@ docker run --rm \
ghcr.io/basnijholt/compose-farm up --all 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
```
</details> </details>
## SSH Authentication ## 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` **Option 1: Host path (default)** - keys at `~/.ssh/compose-farm/id_ed25519`
```yaml ```yaml
volumes: volumes:
- ~/.ssh/compose-farm:/root/.ssh - ~/.ssh/compose-farm:${CF_HOME:-/root}/.ssh
``` ```
**Option 2: Named volume** - managed by Docker **Option 2: Named volume** - managed by Docker
```yaml ```yaml
volumes: volumes:
- cf-ssh:/root/.ssh - cf-ssh:${CF_HOME:-/root}/.ssh
``` ```
Run setup once after starting the container (while the SSH agent still works): 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. 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.
</details> </details>
## Configuration ## Configuration

View File

@@ -1,6 +1,10 @@
services: services:
cf: cf:
image: ghcr.io/basnijholt/compose-farm:latest 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: volumes:
- ${SSH_AUTH_SOCK}:/ssh-agent:ro - ${SSH_AUTH_SOCK}:/ssh-agent:ro
# Compose directory (contains compose files AND compose-farm.yaml config) # 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`) # SSH keys for passwordless auth (generated by `cf ssh setup`)
# Choose ONE option below (use the same option for both cf and web services): # 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 # 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 # Option 2: Named volume - managed by Docker, shared between services
# - cf-ssh:/root/.ssh # - cf-ssh:${CF_HOME:-/root}/.ssh
environment: environment:
- SSH_AUTH_SOCK=/ssh-agent - SSH_AUTH_SOCK=/ssh-agent
# Config file path (state stored alongside it) # Config file path (state stored alongside it)
- CF_CONFIG=${CF_COMPOSE_DIR:-/opt/stacks}/compose-farm.yaml - 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: web:
image: ghcr.io/basnijholt/compose-farm:latest image: ghcr.io/basnijholt/compose-farm:latest
restart: unless-stopped restart: unless-stopped
command: web --host 0.0.0.0 --port 9000 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: volumes:
- ${SSH_AUTH_SOCK}:/ssh-agent:ro - ${SSH_AUTH_SOCK}:/ssh-agent:ro
- ${CF_COMPOSE_DIR:-/opt/stacks}:${CF_COMPOSE_DIR:-/opt/stacks} - ${CF_COMPOSE_DIR:-/opt/stacks}:${CF_COMPOSE_DIR:-/opt/stacks}
# SSH keys - use the SAME option as cf service above # SSH keys - use the SAME option as cf service above
# Option 1: Host path (default) # 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 # 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: environment:
- SSH_AUTH_SOCK=/ssh-agent - SSH_AUTH_SOCK=/ssh-agent
- CF_CONFIG=${CF_COMPOSE_DIR:-/opt/stacks}/compose-farm.yaml - CF_CONFIG=${CF_COMPOSE_DIR:-/opt/stacks}/compose-farm.yaml
# Used to detect self-updates and run via SSH to survive container restart # Used to detect self-updates and run via SSH to survive container restart
- CF_WEB_STACK=compose-farm - 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: labels:
- traefik.enable=true - traefik.enable=true
- traefik.http.routers.compose-farm.rule=Host(`compose-farm.${DOMAIN}`) - traefik.http.routers.compose-farm.rule=Host(`compose-farm.${DOMAIN}`)

View File

@@ -54,6 +54,25 @@ docker run --rm \
ghcr.io/basnijholt/compose-farm up --all 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 ### Verify Installation
```bash ```bash