docker/README.md and docs/docker-compose.md now say that the container starts
as root, corrects a daemon-created bind source and drops to PUID:PGID with
setpriv, which capabilities that needs, and that a compose file written
elsewhere must carry them. The README's PowerShell example runs Compose from
inside docker/ so the override file is discovered, instead of the `-f
docker/docker-compose.yaml` form its own Local customisation section warns
silently drops it, and the reverse-proxy section no longer asks for an override
file now that docker-compose.yaml forwards CODEMAN_ALLOWED_HOSTS itself.
.dockerignore excludes docker-compose.override.* everywhere: it is the
documented home for host-specific settings and rode `COPY . .` into the image,
the same shape as the docker/.env exclusion above it (verified with a scratch
build context: the override files and docker/.env are absent, .env.example and
the compose file present).
CLAUDE.md's Compose paragraph carries the corrected cap list, the writability
probe, and the two traps behind it (KILL is for tini, the CLI prefix is
appended to PATH).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Two real bugs the review caught, both verified live against a real
build on the Unraid host:
1. entrypoint.sh's chown fired on ANY ownership mismatch, not just a
directory the daemon itself created root-owned. A host tree
legitimately owned by some other account - an existing
CODEMAN_CASES_PATH the README already allows pointing at a normal
projects directory, or appdata under a different PUID/PGID
convention than the one in use - got silently recursively re-owned
with one log line to explain it. Now gated on the target actually
being root-owned; anything else is a clean refusal naming the
directory, its owner, and PUID/PGID. Start-Codeman.sh also now
pre-creates CODEMAN_CASES_PATH the same way it already did
CODEMAN_APPDATA_PATH, so Compose never has to materialise a missing
bind source as root in the first place - the in-container chown
becomes a safety net, not the primary mechanism.
2. The CLI-update chown (chown -R .../node_modules /usr/local/bin)
handed the runtime account write access to entrypoint.sh itself
(root-owned, executed as root on every container start with
CHOWN/DAC_OVERRIDE/SETUID/SETGID) and the node binary - owning the
DIRECTORY is enough to rename it aside and drop a replacement, which
would let a compromised session arrange for its own script to run
as root at the next restart. The four CLIs now install into a
dedicated /opt/codeman-cli prefix (NPM_CONFIG_PREFIX); only that
directory is chowned, /usr/local stays root-owned throughout.
Smaller fixes from the same review:
- Start-Codeman.sh's volume-refresh label filter wasn't project-scoped:
a second Compose stack on the same host sharing the `codeman-dist`
volume KEY could have had ITS volume deleted. Added a
com.docker.compose.project filter, resolved from this stack's own
`compose config --format json`.
- Override-file precedence was backwards (checked .yaml before .yml;
Compose actually prefers .yml) - swapped, plus a warning when both
exist.
- entrypoint.sh's setpriv now also passes --bounding-set -all, so
CapBnd actually clears post-drop rather than just CapPrm/CapEff.
- A comment on git_head_commit() noting it returns nothing for a
worktree checkout (.git as a file), consistent with the script's
existing -d .git convention elsewhere.
- Doc drift: CLAUDE.md's Docker Compose section still described the
old pre-created-and-chowned-by-hand model and didn't mention the
root-then-drop entrypoint; the state-files list was missing
docker-build-source.json; docs/docker-compose.md and
docker/.env.example still had the pre-rename `Coding/codeman` path
in one place each.
Verified end to end against a real build on the Unraid host: a
root-owned bind source is corrected as before; a directory owned by
neither root nor PUID:PGID is refused rather than silently rewritten;
a correctly-owned directory is left alone entirely; the four CLIs
resolve via PATH from /opt/codeman-cli while /usr/local/bin,
/usr/local/lib/node_modules and entrypoint.sh itself stay root-owned;
CapBnd is fully cleared post-drop.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R9ZSTEenc8soSu9bTi8Xru
Codeman running under docker/docker-compose.yaml lost the ability to update
itself from App Settings -> Updates. The image had no .git (excluded by
.dockerignore), so the install reported as "unknown"; there was no init system
for detectSupervisor() to find; the runtime stage had neither devDependencies
nor a build toolchain; and a pull into the baked /opt/codeman would have landed
in the container's writable layer and been discarded by the next `up`.
Restore it through configuration rather than a second updater, so the release
channel, auto-stash, status file and boot reconcile are all reused unchanged:
- The checkout Compose builds from is bind-mounted over /opt/codeman, so the
update's git checkout and rebuild land on the host and survive recreation.
- The restart is the server exiting; `restart: unless-stopped` relaunches the
container on the new dist/. This is the one supervisor whose updater does NOT
outlive the restart, which is safe only because the terminal "restarting"
marker is written first.
- node_modules and dist are named volumes over the bind mount, so
container-compiled native modules never enter the host checkout.
- The runtime image keeps devDependencies and gains python3/make/g++, since
`npm run build` is tsc + esbuild and node-pty has no Linux prebuild.
An in-place container update applies code only, because a restart reuses the
existing image and config. evaluateEnvironmentGate() reads the target release's
own files with `git show <tag>:<path>` and refuses when server.Dockerfile or
docker-compose.yaml changed, when .env.example gained keys the user's .env
lacks, or when the restart policy would not bring the container back. The
missing-key check matters most: Compose resolves an unset ${VAR} to the empty
string and starts anyway, so a new required setting would otherwise arrive as a
silently blank variable. Every unknown fails open, and the gate is re-evaluated
server-side on POST /api/system/update.
The four global agent CLIs are pinned, because an unpinned CLI bump is the one
environment change no diff-derived gate can see; pinning turns it into a
Dockerfile change the gate already detects.
Adds test/docker-compose-env-parity.test.ts as the merge-side guard (every
compose ${VAR} has an .env.example entry and the reverse) and
test/docker-self-update.test.ts for the pure gate decisions.
Documented in docs/docker-self-update.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yAQ2y9t81jzSfpStUxx5T