fix(docker): keep SIGTERM reaching the server, pin root's PATH, probe writability

Three changes to how the Compose container starts as root and drops to
PUID:PGID, each reproduced on Docker 29.1.3 / Compose v5.5.0 with a minimal
image of the same shape as server.Dockerfile.

- cap_add gains KILL. `init: true` makes tini PID 1, and tini stays root while
  the entrypoint drops the server to PUID. Signalling a process of a different
  uid needs CAP_KILL, and `cap_drop: ALL` had removed it, so every `docker
  compose down`/`restart` ended in `[FATAL tini (1)] Unexpected error when
  forwarding signal: 'Operation not permitted'` and the server being SIGKILLed
  instead of running `server.stop()`. Measured: without KILL the trap never
  fires, with it the child logs `GOT SIGTERM`.
- /opt/codeman-cli/bin is appended to PATH, never prepended, and entrypoint.sh
  pins its own PATH to the system directories before its first command. The
  prefix is chowned to the runtime account so sessions can update the agent
  CLIs in place, and the root entrypoint resolved stat/chown/setpriv by bare
  name through it: a `setpriv` planted there by the unprivileged uid ran as
  uid 0 at the next start. The image's full PATH is handed back to the server
  at the exec (`env PATH=...`), since Codeman resolves the CLIs through it.
- The ownership gate becomes a writability probe. A directory owned by neither
  root nor PUID:PGID is no longer refused on ownership alone; it is tested with
  `setpriv --reuid PUID --regid PGID --groups <same groups> test -w`, the exact
  identity the server gets, so a group-writable tree, an ACL or a CIFS/NFS
  mount reporting some unrelated uid all pass, and the refusal names path,
  owner and PUID:PGID. Root-owned directories are still chowned first.

Also: a pre-flight runs the drop before touching anything and, when it fails,
prints the cap_add list the compose file needs, so an out-of-tree compose file
(Unraid's Compose Manager) gets a one-line diagnosis instead of a restart loop;
`--bounding-set -all` is gone, since it is a silent no-op without CAP_SETPCAP;
a root:root Docker socket now produces a warning that Docker cases will not
work rather than silently losing group 0 at the drop; and CODEMAN_ALLOWED_HOSTS
is forwarded from .env with an empty default (documented as a commented entry
in .env.example so the parity test and the updater's env gate both stay quiet).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-09-14 23:37:04 +02:00
parent a29e1f61ef
commit 1851d80f3a
4 changed files with 128 additions and 32 deletions
+14
View File
@@ -32,6 +32,10 @@ services:
CODEMAN_DOCKER_HOST_HOME: ${CODEMAN_APPDATA_PATH}
CODEMAN_DOCKER_DISABLE_SWAP_LIMIT: ${CODEMAN_DOCKER_DISABLE_SWAP_LIMIT}
CODEMAN_CASES_PATH: ${CODEMAN_CASES_PATH}
# Extra Host-header allowlist entries for a reverse-proxied deployment
# (docker/README.md, "Reverse-proxy host allowlist"). Optional, so it
# defaults to empty rather than requiring a line in every .env.
CODEMAN_ALLOWED_HOSTS: ${CODEMAN_ALLOWED_HOSTS:-}
CODEMAN_HOST: ${CODEMAN_HOST}
CODEMAN_PASSWORD: ${CODEMAN_PASSWORD}
CODEMAN_PORT: ${CODEMAN_PORT}
@@ -94,8 +98,18 @@ services:
cap_add:
# The entrypoint corrects bind-mount ownership as root before dropping to
# PUID:PGID. Everything not listed here remains dropped by cap_drop above.
# test/docker-entrypoint.test.ts pins this list against what the
# entrypoint and `init: true` actually need, so a capability cannot go
# missing silently again.
- CHOWN
- DAC_OVERRIDE
# `init: true` makes tini PID 1, and tini stays ROOT while the entrypoint
# drops the server to PUID. Signalling a process of a different uid needs
# CAP_KILL; without it tini's SIGTERM forward fails ("Unexpected error
# when forwarding signal: 'Operation not permitted'"), tini dies, and the
# PID namespace teardown SIGKILLs the server instead of letting
# `server.stop()` flush state on every `docker compose down`/`restart`.
- KILL
- SETGID
- SETUID
healthcheck: