Skip to content

fix: run supervisord as root so services start under rootful Docker#582

Merged
pancacake merged 2 commits into
HKUDS:devfrom
IliaAvdeev:fix/docker-rootful-supervisord
Jun 23, 2026
Merged

fix: run supervisord as root so services start under rootful Docker#582
pancacake merged 2 commits into
HKUDS:devfrom
IliaAvdeev:fix/docker-rootful-supervisord

Conversation

@IliaAvdeev

@IliaAvdeev IliaAvdeev commented Jun 22, 2026

Copy link
Copy Markdown

Description

The published image does not boot under a rootful Docker daemon (Docker
Desktop on Windows/macOS, or plain Docker on Linux): supervisord fails to spawn
its children and neither the backend nor the frontend starts. The container
stays "up" but :8001 and :3782 never respond.

Root cause

The entrypoint dropped supervisord itself to the unprivileged deeptutor
user (UID 1000) via exec gosu deeptutor /usr/bin/supervisord. Supervisord
logs its programs to /dev/fd/1,2 (the container's stdout/stderr pipes).
Under a rootful daemon those pipes are owned by root, so after the gosu
drop UID 1000 cannot open them:

CRIT could not write pidfile /var/run/supervisord.pid
INFO spawnerr: unknown error making dispatchers for 'backend': EACCES
INFO spawnerr: unknown error making dispatchers for 'frontend': EACCES
INFO gave up: backend entered FATAL state, too many start retries
INFO gave up: frontend entered FATAL state, too many start retries

The gosu approach only worked under rootless podman (userns_mode: keep-id), where UID 1000 is mapped to the host user that owns those FDs.

Relation to #580 / #581

Issue #580 and PR #581 address a different, cosmetic symptom of the same
gosu hardening (PR #577): under rootless podman the pidfile CRIT appears
in the log, but children still spawn and the stack is fully functional. Their
fix (/var/run tmpfs mode=1777) is correct for that case.

This PR addresses the fatal case: under a rootful daemon the children
never spawn at all because /dev/fd/1,2 — not the pidfile — are
inaccessible to UID 1000. Changing the tmpfs mode does not help here; the
file descriptors are inherited from the container runtime, not a tmpfs path.

PR #581 This PR
Runtime rootless podman + keep-id rootful Docker Desktop / Engine
Symptom CRIT pidfile in logs (cosmetic) EACCES on fd/1,2 — app never starts (fatal)
Fix compose.yaml tmpfs mode Dockerfile supervisord config
Files changed compose.yaml Dockerfile, test

Fix

Run supervisord as root and drop only the app processes to deeptutor
via supervisord's own user= directive — applied to both the production and
development supervisord configs:

  • entrypoint: exec /usr/bin/supervisord … (no gosu prefix)
  • [supervisord] section: user=root
  • [program:backend] and [program:frontend]: user=deeptutor

Root opens the log FDs and pidfile, then supervisord setuids each child —
so the backend and frontend processes stay non-root in both runtimes,
preserving the security goal of PR #577.

The dev stage also chowns /app/web/.next so next dev can write its
build cache as the unprivileged user.

Related Issues

Module(s) Affected

  • agents
  • api
  • config
  • core
  • knowledge
  • logging
  • services
  • tools
  • utils
  • web (Frontend)
  • docs (Documentation)
  • scripts
  • tests
  • Other: Docker (Dockerfile / container runtime)

Checklist

  • I have read and followed the contribution guidelines.
  • My code follows the project's coding standards.
  • I have run pre-commit run --all-files and fixed any issues.
  • I have added relevant tests for my changes.
  • I have updated the documentation (if necessary).
  • My changes do not introduce any new security vulnerabilities.

Additional Notes

Reproduction (released image, no overrides needed):

docker run ghcr.io/hkuds/deeptutor:latest
# CRIT could not write pidfile /var/run/supervisord.pid
# INFO spawnerr: unknown error making dispatchers for 'backend': EACCES
# INFO spawnerr: unknown error making dispatchers for 'frontend': EACCES
# INFO gave up: backend entered FATAL state, too many start retries

Verification (rebuilt --target production, plain docker run, no overrides):

INFO supervisord started with pid 1
INFO spawned: 'backend' with pid 18
INFO spawned: 'frontend' with pid 19
INFO success: backend entered RUNNING state
▲ Next.js 16.2.3  ✓ Ready

GET /api/v1/auth/status200, GET /200. App processes confirmed
running as deeptutor (UID 1000) via the user= directive.

Tests: added test_supervisord_runs_as_root_with_unprivileged_children in
tests/scripts/test_docker_compose.py. Parses the Dockerfile and asserts: (1)
no gosu deeptutor before supervisord, (2) every [program:*] declares
user=deeptutor. Runs without a Docker build. pytest -q tests/scripts/test_docker_compose.py → 6 passed.


Open to any feedback — if this doesn't fit the project's direction, no worries at all, happy to discuss and rework as needed.

pancacake added a commit that referenced this pull request Jun 23, 2026
…root-supervisord model

#582 made supervisord run as root with per-program user=deeptutor, so the
gosu privilege-drop binary is no longer invoked. Remove the gosu and
libcap2-bin apt packages and the setcap step (a setuid-capable binary that
was left unused == needless attack surface), and refresh the Dockerfile
comments.

Realign the authoritative docs that still described the old gosu model:
- CONTAINERIZATION.md: supervisord runs as root and drops each program to
  deeptutor via user=; fix the verification snippet.
- compose.yaml: correct the (now wrong + dangerous) note that cap_drop ALL
  is safe — root supervisord NEEDS CAP_SETUID/CAP_SETGID to setuid its
  children, so cap_drop ALL would stop the backend/frontend from starting.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@pancacake
pancacake merged commit 32a22e7 into HKUDS:dev Jun 23, 2026
11 checks passed
vaskoyudha added a commit to vaskoyudha/deeptutor-for-programmer-fork that referenced this pull request Jul 25, 2026
…visord

fix: run supervisord as root so services start under rootful Docker

PR HKUDS#577 dropped supervisord itself to UID 1000 via gosu; under a rootful
daemon (Docker Desktop / Docker Engine) the root-owned /dev/fd/1,2 and
pidfile become unopenable, so backend+frontend never spawn (EACCES). Run
supervisord as root and drop each child to deeptutor via per-program
user= instead — app processes stay non-root in both rootful Docker and
rootless podman.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
vaskoyudha added a commit to vaskoyudha/deeptutor-for-programmer-fork that referenced this pull request Jul 25, 2026
…root-supervisord model

HKUDS#582 made supervisord run as root with per-program user=deeptutor, so the
gosu privilege-drop binary is no longer invoked. Remove the gosu and
libcap2-bin apt packages and the setcap step (a setuid-capable binary that
was left unused == needless attack surface), and refresh the Dockerfile
comments.

Realign the authoritative docs that still described the old gosu model:
- CONTAINERIZATION.md: supervisord runs as root and drops each program to
  deeptutor via user=; fix the verification snippet.
- compose.yaml: correct the (now wrong + dangerous) note that cap_drop ALL
  is safe — root supervisord NEEDS CAP_SETUID/CAP_SETGID to setuid its
  children, so cap_drop ALL would stop the backend/frontend from starting.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
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.

2 participants