Skip to content

fix(rootless): auto-fix data-directory ownership for Docker rootless mode #1708

Open
IronicRayquaza wants to merge 3 commits into
Osmantic:mainfrom
IronicRayquaza:fix/rootless-docker-data-ownership
Open

fix(rootless): auto-fix data-directory ownership for Docker rootless mode #1708
IronicRayquaza wants to merge 3 commits into
Osmantic:mainfrom
IronicRayquaza:fix/rootless-docker-data-ownership

Conversation

@IronicRayquaza

Copy link
Copy Markdown
Contributor

Fixes #1702

Summary

Fixes permission issues when running ODS with Docker in rootless mode.

In rootless Docker, container UIDs are remapped through the host user's subordinate UID range (typically starting at 100000). As a result, data directories created by the installer are owned by the host user (for example, UID 1000) and become unwritable by containers running as non-root users.

This PR adds automatic detection and repair of rootless Docker ownership during installation, provides a standalone repair command, surfaces warnings in preflight and doctor diagnostics, and adds comprehensive test coverage.

Changes

lib/rootless-fix.sh (new)

  • Added ods_is_rootless_docker to detect Docker rootless mode using docker info.
  • Added _ods_rootless_chown_dir to repair ownership through a short-lived Alpine container without requiring sudo.
  • Added ods_fix_rootless_ownership, an idempotent repair routine that:
    • Runs only when Docker is operating in rootless mode.
    • Iterates the ODS_ROOTLESS_UID_MAP.
    • Repairs ownership for all affected service directories:
      • n8n
      • hermes
      • whisper
      • tts
      • token-spy
      • privacy-shield
      • ape
      • langfuse
      • openclaw
    • Repairs the OpenClaw workspace configuration directory.
    • Continues even if individual directory repairs fail.
  • Added ods_warn_rootless_docker to display a user-facing advisory.

installers/phases/06-directories.sh

  • Sources rootless-fix.sh during installation.
  • Runs ods_fix_rootless_ownership after all data directories are created and the existing Token Spy ownership adjustment completes.

ods-preflight.sh

  • Detects Docker rootless mode after daemon validation.
  • Displays a warning describing affected services.
  • Suggests the recovery command:
ods repair rootless-ownership

scripts/ods-doctor.sh

  • Detects rootless Docker (DOCKER_ROOTLESS).
  • Includes the value in the generated JSON report as:
runtime.docker_rootless
  • Increments runtime_warnings when rootless mode is detected.
  • Adds an autofix hint recommending:
ods repair rootless-ownership

ods-cli

  • Added:
ods repair rootless-ownership
  • Added alias:
ods rootless
  • Updated CLI help and examples.

tests/test-rootless-docker-ownership.sh (new)

Adds 30 tests covering:

  • Static validation across all modified files.
  • UID mapping correctness.
  • Filesystem simulation.
  • Processing of existing directories only.
  • No-op behavior when Docker is not running in rootless mode.

Release Lane

  • Stable hotfix targeting release/2.5.x
  • Mainline change targeting main
  • Next-minor work targeting the next feature/minor release
  • Not sure; reviewer should help classify

Stable hotfix reason:

N/A

Changed Surface

  • Docs only
  • Tests only
  • Dashboard UI
  • Dashboard API / host agent
  • Installer / bootstrap / lifecycle
  • Docker Compose / service manifests
  • Model routing / Hermes / capabilities
  • Network exposure / auth / proxy
  • Dependencies / runtime wiring

Risk And Validation

  • Risk level: Medium

Validation run:

  • git diff --check
  • Markdown/link sanity for docs
  • Focused tests listed below
  • Dashboard lint/test/build
  • Extension audit / compose validation
  • Release-grade fleet or scoped hardware validation
  • Stable-lane patch validation, if targeting release/2.5.x

Commands/results:

tests/test-rootless-docker-ownership.sh

30 tests passed.

Verified:
- Rootless detection logic
- UID mapping correctness
- Existing-directory filtering
- No-op behavior on non-rootless Docker
- CLI repair command wiring
- Doctor/preflight reporting

Operational Change Check

  • This is not an operational change.
  • This is an operational change and validation is recorded above.
  • This is an operational change and validation is intentionally deferred for:

Notes For Reviewers

  • The ownership repair is completely idempotent.
  • No behavior changes occur on standard (rootful) Docker installations.
  • Individual directory repair failures are intentionally non-fatal to avoid interrupting installation.
  • Existing installations can be repaired at any time using:
ods repair rootless-ownership

…mode

Fixes Osmantic#1702

In Docker rootless mode, container UIDs are shifted by the host user's
subuid offset (typically 100000).  Containers that run as a non-root
UID N map to host UID (100000 + N - 1).  The installer creates data
directories owned by the host user (UID 1000), making them unwritable
to those remapped container UIDs.

Changes
-------
lib/rootless-fix.sh  (new)
  - ods_is_rootless_docker     — detects rootless mode via 'docker info
    --format {{.SecurityOptions}} | grep rootless'
  - _ods_rootless_chown_dir    — chowns one dir using a short-lived
    Alpine container run as UID 0 (= host user in rootless mode), so
    no sudo is required
  - ods_fix_rootless_ownership — idempotent driver; only runs when
    rootless is detected; iterates the ODS_ROOTLESS_UID_MAP covering
    all 9 affected services (n8n, hermes, whisper, tts, token-spy,
    privacy-shield, ape, langfuse, openclaw) and the openclaw workspace
    config dir; non-fatal on individual dir failures
  - ods_warn_rootless_docker   — prints a human-readable advisory block

installers/phases/06-directories.sh
  - Sources rootless-fix.sh early in the live install path
  - Calls ods_fix_rootless_ownership after all data dirs are created
    and the existing token-spy chown is applied

ods-preflight.sh
  - After Docker daemon check, detects rootless mode and calls warn()
    with the affected services and the 'ods repair rootless-ownership'
    recovery command

scripts/ods-doctor.sh
  - Detects rootless mode (DOCKER_ROOTLESS bash variable)
  - Passes it to the Python report section as a new argument
  - Adds runtime.docker_rootless to the JSON report
  - Increments runtime_warnings counter when rootless is detected
  - Emits 'ods repair rootless-ownership' as an autofix_hint

ods-cli
  - Adds 'ods repair rootless-ownership' (alias: 'rootless') sub-command
    that sources lib/rootless-fix.sh and calls ods_fix_rootless_ownership
  - Updates help text and example block

tests/test-rootless-docker-ownership.sh  (new)
  - 30 tests: static checks across all 4 modified files, UID map
    correctness, and filesystem simulations verifying that only existing
    directories are processed and the fix is a no-op in non-rootless mode
In Docker rootless mode, host.docker.internal is not automatically registered
in containers' /etc/hosts. As a result, dashboard-api cannot reach the ODS
host agent.

This commit updates our rootless-fix library to detect rootless mode,
automatically bind the ODS host agent to 0.0.0.0 (so it listens on the
bridge network gateway), and auto-detect the host's LAN IP to populate
ODS_AGENT_HOST in the .env file.

- Added ods_fix_rootless_agent_network to lib/rootless-fix.sh
- Sourced and called in installers/phases/06-directories.sh
- Integrated into 'ods repair rootless-ownership'
- Added doctor check hints explaining the issue and recovery
- Added comprehensive unit tests validating host/bind setting and idempotence
@Lightheartdevs

Copy link
Copy Markdown
Collaborator

Thanks for putting a fuller rootless Docker repair path together, especially the separate helper and the preflight/doctor visibility.

Advisory status: not ready yet

Quickest path to merge:

  • ods/installers/phases/06-directories.sh:257-264: move the ods_fix_rootless_agent_network "$INSTALL_DIR" call until after phase 06 writes the final .env, or add ODS_AGENT_BIND / ODS_AGENT_HOST to the phase-06 generated/preserved values. Right now the helper runs before .env generation; on a fresh install it skips because .env is missing, and on a reinstall its edits can be overwritten by the later generated .env. That leaves dashboard-api still pointed at the rootless-unfriendly host-agent defaults even though the repair reported success.
  • Add a regression that exercises the phase-06 ordering, not only the helper directly: simulate rootless mode through the generated .env path and assert ODS_AGENT_BIND=0.0.0.0 plus the chosen ODS_AGENT_HOST survive at the end.
  • Please also sanity-check the Bash portability surface around ods/lib/rootless-fix.sh:47. The new helper uses an associative array; if it remains sourced from installer code, either keep it behind a Linux/Bash4+ guard or use the repo's Bash-3.2-safe style so future macOS/bootstrap checks do not regress.

What I checked:

  • Diff/static review of ods/lib/rootless-fix.sh, phase 06, ods-cli, ods-preflight.sh, ods-doctor.sh, and the new rootless test.
  • Local Git Bash syntax checks for ods/ods-cli and ods/lib/rootless-fix.sh passed.
  • GitHub checks are green, including shell lint, Python lint, dashboard API/frontend, matrix smoke, integration smoke, PowerShell lint, and env validation.

The ownership repair shape is close; the remaining issue is making sure the generated install config actually retains the rootless host-agent networking fix.

…nd portability

- Moved the ods_fix_rootless_agent_network call to run after phase 06 writes and chmods the .env file.
- Added ODS_AGENT_BIND and ODS_AGENT_HOST to the phase 06 generated .env template. This ensures that their values are preserved correctly across reinstalls/upgrades.
- Replaced the declare -A associative array with a Bash 3.2-compatible flat array and function helper, resolving potential parse errors on macOS.
- Added two integration regression tests simulating phase 06 ordering on fresh installs and reinstalls to verify that agent bindings are set correctly and preserved.
@IronicRayquaza

Copy link
Copy Markdown
Contributor Author

Thanks for the great feedback! I've implemented the suggested changes, verified that the full test suite passes, and pushed the updates to the branch.

Phase 06 .env Template Integration & Ordering

  • Moved the ods_fix_rootless_agent_network invocation to run after the .env file has been generated and secured with chmod 600.
  • Added ODS_AGENT_BIND and ODS_AGENT_HOST to the variables parsed at the start of 06-directories.sh via _env_get, and included them in the generated .env template.
  • This ensures existing values are correctly merged and preserved during upgrades and reinstalls rather than being dropped when the template is regenerated.

Integration Regression Tests

Added two integration test cases to test-rootless-docker-ownership.sh that exercise the actual 06-directories.sh installation flow:

  • Fresh Install

    • Simulates installation without an existing .env.
    • Verifies the template is generated with defaults and then updated by the rootless network helper to use ODS_AGENT_BIND=0.0.0.0 and the detected host LAN IP.
  • Reinstall / Upgrade

    • Simulates an existing .env containing rootless networking settings.
    • Verifies those values are read, preserved in the regenerated template, and not overwritten.

Bash Portability (macOS / Bash 3.2)

  • Refactored ods/lib/rootless-fix.sh to remove the declare -A associative array, which requires Bash 4+ and is incompatible with the default Bash 3.2 shipped on macOS.
  • Replaced it with a standard flat array and a case-based helper (_ods_rootless_get_uid) for UID lookups.
  • Updated the static assertion tests to exercise the new _ods_rootless_get_uid helper.

Thanks again for the thorough review and suggestions—they helped make the implementation more robust and portable.

@Lightheartdevs

Copy link
Copy Markdown
Collaborator

Thanks for tightening the rootless Docker repair flow after the earlier pass; the phase-06 ordering and Bash portability fixes are clear improvements.

Advisory status: not ready yet

Quickest path to merge:

  • ods/lib/rootless-fix.sh:139-145: avoid chowning data/langfuse recursively as UID 1001. The existing Langfuse ownership contract requires data/langfuse/postgres as UID 70 and data/langfuse/clickhouse as UID 101; a broad data/langfuse repair can undo that on rootless installs or ods repair rootless-ownership. Handle those nested dirs explicitly, or skip Langfuse in the generic helper and make the Langfuse hook rootless-aware.
  • ods/Makefile:19-72: wire tests/test-rootless-docker-ownership.sh into make test or a CI workflow. The new test passes locally, but it is not part of the gate, so the regression this PR adds is easy to miss on later changes. Add coverage for the Langfuse nested UID case above.

What I checked:

  • Diff/static review of phase 06, ods/lib/rootless-fix.sh, ods-cli, preflight/doctor, and the new rootless test.
  • GitHub checks for this head are green.
  • Local Git Bash: changed shell files passed bash -n, and bash tests/test-rootless-docker-ownership.sh passed.

Thanks again for keeping at this rootless path.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Enhancement: Support Docker rootless mode — fix data directory ownership for non-root container users

2 participants