Skip to content

feat: allow running the Docker image as a non-root user - #11469

Open
nicololuescher wants to merge 2 commits into
opsmill:developfrom
nicololuescher:nl-docker-non-root
Open

nicololuescher wants to merge 2 commits into
opsmill:developfrom
nicololuescher:nl-docker-non-root

Conversation

@nicololuescher

@nicololuescher nicololuescher commented Nov 6, 2026 •

Copy link
Copy Markdown

Why

The Infrahub image only works as root. Its runtime directories are root-owned, the git settings are in root's ~/.gitconfig, and the task worker writes its own git config file into /opt/infrahub at startup (git.global_config_file). Clusters that require runAsNonRoot (Pod Security Standards "restricted", OpenShift, Kyverno or Gatekeeper policies) cannot run the server and the task worker without patching the image.

What goes wrong with another uid today:

  • the task worker cannot create /opt/infrahub/.gitconfig, logs Failed to set git user.name … (and the other keys), keeps running, and repository operations later lack the credential helper;
  • the task manager cannot build its UI files inside the virtualenv: Failed to create V1 UI static directory … [Errno 13] Permission denied.

Goal: the image can run as a non-root user without changes. Non-goal: changing the default user. It stays root, so existing deployments and volumes are untouched.

Supersedes #11468, which targeted stable; this is a feature, so it goes to develop.

What changed

development/Dockerfile:

  • An infrahub user: uid 1000, gid 0, home /home/infrahub.
  • /prom_shared, /remote, /opt/infrahub (and its subdirectories) and /home/infrahub are owned by infrahub:0, and group 0 has the owner's permissions (chown -R infrahub:0 + chmod -R g=u). uid 1000 with any gid can write them, and so can an arbitrary uid in group 0 as OpenShift assigns.
  • HOME=/home/infrahub for every user, so a uid without a passwd entry gets a writable home instead of / (Prefect creates ~/.prefect).
  • The image's git settings stay --global, now in /home/infrahub/.gitconfig, so they apply to whichever user runs the container. At startup the task worker points GIT_CONFIG_GLOBAL at its own file and writes the same settings there, as before, so credential.helper is set once.
  • PREFECT_UI_STATIC_DIRECTORY=/opt/infrahub/prefect-ui, a writable directory, for the task manager. prefect-helm sets its own value (/ui_build on a scratch volume), which still takes precedence.

Docs: a new guide, Run Infrahub as a non-root user (deploy-manage/install-configure/production-deployment/non-root.mdx, Docker Compose, Podman and Helm), linked from the production deployment overview and the sidebar.

What stays the same: the default user is root; no code, schema or config-model changes; volume mount points are unchanged.

How to test

docker build -f development/Dockerfile --target backend --build-arg UV_VER=<UV_VERSION> -t infrahub-nonroot:test .

cat > compose.nonroot.yml <<'YAML'
services:
  task-manager:    {image: infrahub-nonroot:test, user: "1000:0"}
  infrahub-server: {image: infrahub-nonroot:test, user: "1000:0"}
  task-worker:     {image: infrahub-nonroot:test, user: "1000:0"}
YAML
docker compose -f docker-compose.yml -f compose.nonroot.yml up -d --wait

export INFRAHUB_ADDRESS=http://localhost:8000 INFRAHUB_API_TOKEN=06438eb2-8019-4776-878c-0941b1f1d1ec
infrahubctl schema load models/base --wait 60
infrahubctl branch create test && infrahubctl branch delete test
infrahubctl repository add demo-edge https://github.com/opsmill/infrahub-demo-edge.git --read-only --ref main

Results on develop (Docker 29.9.2, upstream docker-compose.yml, image from this branch), each as a fresh stack, for 1000:0, 1000:1000, 1000700000:0 and root:

  • all services healthy, id -u as set in infrahub-server, task-worker and task-manager;
  • no Permission denied, Errno 13, Failed to set git or Failed to create the Prefect home in any log, and both task workers log Set git config file to /opt/infrahub/.gitconfig;
  • one credential.helper in each task worker's effective git config (the first commit of this PR had two: system and global; the released 1.11.10 image has one);
  • schema load syncs across workers, branch create/delete works;
  • the worker clones the repository into /opt/infrahub/git (owned by the running uid) and resolves the commit. The demo repository then stops at error-import because it references a CoreGroup (backbone_services) that only exists with its demo data loaded; this is unrelated to the user.

Also checked:

  • Upgrade path from the guide: the released 1.11.10 image as root, with a schema loaded and files written to the storage and workflow volumes (0:0 644); then the guide's Step 1 command; then this image as 1000:1000 on the same volumes: healthy, reads the old files, writes new ones, the schema is still there.
  • Write matrix (docker run --user …, every runtime path): root, 1000:0, 1000:1000, 12345:0 and 1000700000:0 write all of them; 1000860000:1000860000 writes none, as the guide says; /.venv stays root-only.
  • Rootless Podman 4.9.3 + podman-compose 1.6.0 (Ubuntu 24.04, runc):
    • named volumes as 1000:0 and as 12345:0: work;
    • bind-mounted storage and workflow directories with the guide's userns_mode: "keep-id:uid=1000" and x-podman: {in_pod: false}: work, and the files on the host belong to the host user;
    • setups the guide advises against fail as expected: user: "1000:0" without keep-id (the host directories appear root-owned) and keep-id:uid=503,gid=20 (Errno 13 on /prom_shared, as in the review);
    • keep-id:uid=1000,gid=0 starts no container at all on this Podman/runc, whatever the image, which is why the guide does not use it;
    • without in_pod: false, podman-compose refuses userns_mode (--userns and --pod cannot be set together).
  • Not tested: Podman 5 or 6, a host uid other than 1000 with Podman, OpenShift or Kubernetes with this image.

Docs: markdownlint-cli2 0.23.3 and Vale 3.12.0 (the CI versions) report no issues on the new and changed pages.

Why group 0

The infrahub user's primary group is root (gid 0) on purpose. OpenShift runs containers with an arbitrary uid that is always a member of the root group, and Red Hat's image guidelines ask for exactly this layout: directories the image writes to "must be owned by the root group and be read/writable by that group", set with chgrp -R 0 and chmod -R g=u (OpenShift: Support arbitrary user ids; the section is unchanged in the current docs source). Using one group for uid 1000 and for an arbitrary uid keeps a single supported layout. The directories are additionally owned by uid 1000, so a deployment that sets a different gid for uid 1000 (1000:1000) can write them too.

Group 0 is not root privilege. The kernel treats a process as privileged only when its effective user ID is 0. Every other process "is subject to full permission checking based on the process's credentials (usually: effective UID, effective GID, and supplementary group list)" (capabilities(7)). Group 0 therefore only grants what the group permission bits of a file allow. In this image, outside the directories this PR opens up, that is:

  • writable by group 0: a few /usr/share/man/* directories and /var/cache/debconf/tmp.ci from the Debian base image, and the empty lock file /.venv/.lock;
  • readable only by group 0: three dpkg/apt lock files.

The virtualenv, the system binaries and /etc stay read-only for the group. Per Red Hat's warning in the same section, the PR does not widen permissions on sensitive paths such as /etc/passwd.

How to review

  • development/Dockerfile is the whole runtime change.
  • Open question: a non-root run could be added to the Docker integration tests, but that touches CI, so I left it out. Happy to add it if you want it.

Impact & rollout

  • Backward compatibility: the default user is still root. HOME is /home/infrahub for root too, so the image's git settings move from /root/.gitconfig to /home/infrahub/.gitconfig and Prefect's home from /root/.prefect to /home/infrahub/.prefect; the task worker uses its own git config file as before.
  • Config/env changes: new default PREFECT_UI_STATIC_DIRECTORY=/opt/infrahub/prefect-ui.
  • Deployment notes: switching an existing deployment to the non-root user needs the existing volume data handed to 1000:0 once (chown -R 1000:0 + chmod -R g=u); the new guide covers it.

Checklist

  • Changelog entry added (changelog/+docker-non-root.added.md, created with towncrier create)
  • External docs updated
  • I have reviewed AI generated content

The image runs as root and every runtime path assumes it: the directories
under /opt/infrahub, /prom_shared and /remote are root-owned, the git
settings are in root's ~/.gitconfig, and the task worker creates its own
git config file in /opt/infrahub at startup. Run with another uid, the
worker cannot write that file and repository operations later lack the
credential helper, and the task manager cannot build its UI files inside
the virtualenv.

- Add an `infrahub` user (uid 1000, gid 0).
- Make the runtime directories owned by group 0 with the owner's
  permissions, so uid 1000 or an arbitrary uid in group 0 can write them.
- Write the image's git settings to the system config instead of root's.
- Point PREFECT_UI_STATIC_DIRECTORY at a writable directory.
- Document running as a non-root user with Docker Compose and Helm.

The default user stays root, so existing deployments and volumes are
unaffected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nicololuescher
nicololuescher requested review from a team as code owners November 6, 2026 13:50
@nicololuescher
nicololuescher requested review from minitriga and removed request for a team November 6, 2026 13:50
@nicololuescher

Copy link
Copy Markdown
Author

This is my first PR here, so the CI workflows are waiting for approval. Could a maintainer approve the runs? Happy to add a non-root run to the Docker integration tests if you'd like one in this PR.

@DharmaBytesX

Copy link
Copy Markdown
Contributor

I built the image from this PR (289da9e) and ran the stack on Docker and on rootless Podman 6.1.4 (macOS, arm64). One change and one gap in the guide are suggested below, followed by three observations.

Test results on the PR as it is

  • Docker Compose, podman compose and podman-compose 1.6.0, with user: "1000:0" on infrahub-server, task-worker and task-manager: all services start, the ones with a healthcheck are healthy, and no log has Permission denied or Failed to set git. Schema load and branch create/delete work, and a repository is cloned into /opt/infrahub/git and owned by infrahub.
  • An arbitrary uid in group 0 (12345:0 on Podman, 1000700000:0 on Docker) writes every runtime path and reads the git config.
  • podman run --user 1000700000:0 on rootless Podman fails with crun: setresuid to 1000700000: Invalid argument. My Podman machine maps 1,000,000 uids (/etc/subuid), so this uid does not exist in the container. After I widened the range to 2,000,000,000, the same command works. The image is not the cause.

Suggested change: make infrahub the owner of the runtime directories

chgrp -R 0 leaves root as the owner. For example, /opt/infrahub is root:0 with mode 775 (docker run --rm --entrypoint sh <image> -c 'ls -ldn /opt/infrahub'). A process can write there only when gid 0 is in its group list.

  • On the PR image, docker run --user 1000:1000 cannot write /opt/infrahub, /prom_shared, /remote or the storage and workflow mount points.
  • With chown -R infrahub:0 ... in place of chgrp -R 0 ..., I built the image again and ran it as 1000:1000, 1000:0, 12345:0 and root. All four write every runtime path. Only /.venv stays read-only.
  • Why it matters: a deployment that sets both a uid and a different gid (user: "1000:1000" in Compose, runAsGroup in a pod security context) cannot write the runtime directories without this change. Which deployments do this is not known.

Gap in the guide: Podman with bind mounts

The guide has no Podman section. With bind-mounted data directories, --userns=keep-id is one way to give the container access to host-owned files. The Podman documentation says it maps the current user's UID:GID to the same values in the container.

  • Plain keep-id runs the container as the host user's uid (503 on my test machine). infrahub-server exits with Worker failed to boot, and the task manager logs PermissionError: [Errno 13] Permission denied: '/prom_shared/counter_2.db'.
  • --userns=keep-id:uid=1000,gid=0 works: with bind-mounted storage and workflow directories, all services are healthy and a branch create works.
  • Suggestion: add a Podman tab to the guide with userns_mode: "keep-id:uid=1000,gid=0" (Compose) and --userns=keep-id:uid=1000,gid=0 (podman run).

Other observations on the PR as it is

  • A uid with a non-zero gid is not covered, with or without the chown change. --user 1000860000:1000860000 (the setup in #7709) cannot write any runtime path. The guide already says group 0 is required, and it could say so for the gid explicitly.
  • HOME is / for a uid without a passwd entry (--user 12345:0). python -c "import prefect" then prints UserWarning: Failed to create the Prefect home directory at /.prefect. I did not test a fix such as ENV HOME=/home/infrahub.
  • When the task worker writes its own global git config at startup, credential.helper exists in two scopes: git config --get-all --show-scope credential.helper prints one system and one global entry. With two stub helpers in those two scopes, git credential fill calls only the system one when it returns a username and password, and calls both when it returns none (for example a repository without a stored credential), so Infrahub would be asked twice. This is minor.

Not verified

  • The GitHub Actions runs: they wait for approval.
  • OpenShift or any Kubernetes cluster: I did not test one.
  • The upgrade claim for volumes created by 1.11.10 as root.
  • A non-root run in the Docker integration tests: left to the maintainers.

@polmichel

Copy link
Copy Markdown
Contributor

This is my first PR here, so the CI workflows are waiting for approval. Could a maintainer approve the runs? Happy to add a non-root run to the Docker integration tests if you'd like one in this PR.

Hey @nicololuescher, thanks for your contribution. I just approved the workflows so they can run.

@codspeed

codspeed Bot commented Nov 9, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

1 warning

⚠️ 1 benchmark is dominated by system calls

System calls cannot be simulated consistently and are left out of the measure. The reported value understates the real cost. Switch to the Walltime instrument to measure them.

Learn more about system calls

See affected benchmarks

✅ 13 untouched benchmarks


Comparing nicololuescher:nl-docker-non-root (fab33b6) with develop (1f14e39)

Open in CodSpeed

… $HOME

- `chown -R infrahub:0` instead of `chgrp -R 0`, so uid 1000 with any gid can write, as well as any uid in group 0.
- `HOME=/home/infrahub` for every user, so a uid without a passwd entry no longer gets HOME=/ (Prefect could not create ~/.prefect).
- The image's git settings go back to `--global` ($HOME/.gitconfig): the task worker replaces that file with its own at startup, so `credential.helper` is set once again instead of in both the system and the global scope.
- Guide: which user and group combinations work, `chown` in the upgrade step, and a Podman tab for bind mounts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nicololuescher

Copy link
Copy Markdown
Author

Thanks @DharmaBytesX for testing this so thoroughly, on Podman too! I pushed fab33b6:

  • Ownership: switched to chown -R infrahub:0 as you suggested, so 1000:1000 works now as well.
  • HOME and the double credential helper: both came from me moving the git settings to --system, which put them on top of the config file the task worker writes at startup. The image now sets HOME=/home/infrahub and keeps the settings in ~/[.gitconfig](https://github.com/opsmill/infrahub/pull/~/.gitconfig) again, as before this PR, so there's one helper and Prefect has a writable home. The one visible change is that root's HOME is now /home/[infrahub](https://github.com/home/infrahub) too.
  • Guide: it now spells out which uid/gid combinations work, and Step 1 uses chown.

For the Podman tab I installed Podman here (4.9.3 rootless with podman-compose 1.6.0) and ran into two things, so the tab differs a bit from your suggestion:

  • podman-compose runs the services in a pod, and Podman then refuses userns_mode (--userns and --pod cannot be set together). The tab sets x-podman: {in_pod: false}.
  • On Podman 4.9 with runc, keep-id:uid=1000,gid=0 doesn't start any container at all. Since uid 1000 now owns the runtime directories, the tab uses keep-id:uid=1000 instead, which works there. I couldn't test it on Podman 5/6 or with a host uid other than 1000.

The details of what I tested (Docker as 1000:0, 1000:1000, an arbitrary uid and root, the upgrade path, and the Podman setups) are in the updated PR description.

@DharmaBytesX

Copy link
Copy Markdown
Contributor

Thanks for the changes. I tested fab33b6 on a clean stable base and on Podman 6.1.4 (podman-compose 1.6.0):

  • 1000:1000, 1000:5555, 1000:0 and 12345:0 write every runtime path. 12345:12345 does not, as the guide says.
  • An existing root deployment moved to the new image keeps its data. The guide's Step 1 and 1000:0 then work over the same volumes.
  • Your Podman tab works with a host uid of 503: files written in the container are owned by 503 on the host.
  • Not reproduced: the userns_mode and pod conflict. On Podman 6.1.4 the same override also starts without in_pod: false. I did not check this on 4.9, so it may depend on the Podman version.
  • keep-id:uid=1000,gid=0 also works on 6.1.4. I could not test 4.9.

Not tested: Kubernetes and amd64.

This branch has not been deployed

No deployments
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.

3 participants