mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
fix(docker): correct bind-mount ownership before dropping privileges
Compose binds CODEMAN_APPDATA_PATH and CODEMAN_CASES_PATH from the host. When either path does not exist yet - a first run, a cleared application-data directory, a restored backup - the Docker daemon creates it owned by root. The server runs unprivileged as CODEMAN_RUNTIME_USER, so it cannot create its own state directory, and the container restarts forever on: Failed to start web server: EACCES: permission denied, mkdir '/home/<user>/.codeman' Start-Codeman.sh already worked around this by preparing the directory on the host, so the failure only appears when Compose is run directly, which the README documents as a supported path. Add docker/entrypoint.sh, which starts as root, corrects the ownership of both bind mounts, then drops to PUID:PGID with setpriv. The Dockerfile's USER instruction is replaced by that entrypoint and CMD is unchanged. docker-compose.yaml adds back only the four capabilities the chown and the privilege drop require, so cap_drop: ALL continues to remove everything else. Two guards keep existing deployments working: - A container started with an explicit `user:` is left alone. The entrypoint execs straight through, with no elevation and no chown. - A chown that fails is a warning, not an error. Bind mounts backed by NFS, CIFS or a rootless daemon can refuse chown while remaining perfectly writable, and those deployments must keep starting. PUID and PGID are also exported as runtime environment defaults so the image behaves correctly when run without Compose, rather than depending on build args alone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -91,6 +91,13 @@ services:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- ALL
|
||||
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.
|
||||
- CHOWN
|
||||
- DAC_OVERRIDE
|
||||
- SETGID
|
||||
- SETUID
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD-SHELL
|
||||
|
||||
Executable
+48
@@ -0,0 +1,48 @@
|
||||
#!/bin/sh
|
||||
# Corrects the ownership of the host bind mounts, then drops to PUID:PGID.
|
||||
#
|
||||
# Compose binds CODEMAN_APPDATA_PATH and CODEMAN_CASES_PATH from the host. When
|
||||
# either path does not exist yet - a first run, a cleared application-data
|
||||
# directory, a restored backup - the Docker daemon creates it owned by root,
|
||||
# and an unprivileged server cannot then create its own state directory. The
|
||||
# result is a container that restarts forever on:
|
||||
#
|
||||
# Failed to start web server: EACCES: permission denied, mkdir '/home/<user>/.codeman'
|
||||
#
|
||||
# Running this as root and dropping afterwards removes that failure mode without
|
||||
# leaving the server privileged.
|
||||
|
||||
set -eu
|
||||
|
||||
# Honour an explicit `user:` in Compose: when the container was not started as
|
||||
# root there is nothing to correct and no privilege to drop.
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
exec "$@"
|
||||
fi
|
||||
|
||||
: "${PUID:=1000}"
|
||||
: "${PGID:=1000}"
|
||||
|
||||
for target in "${HOME:-}" "${CODEMAN_CASES_PATH:-}"; do
|
||||
[ -n "$target" ] && [ -d "$target" ] || continue
|
||||
[ "$(stat -c '%u:%g' "$target")" = "${PUID}:${PGID}" ] && continue
|
||||
|
||||
# Deliberately not fatal. A bind mount backed by NFS, CIFS or a rootless
|
||||
# daemon can refuse chown while still being perfectly writable, and those
|
||||
# deployments must keep working. A warning is more useful than a container
|
||||
# that will not start.
|
||||
if chown -R "${PUID}:${PGID}" "$target" 2>/dev/null; then
|
||||
printf 'entrypoint: corrected ownership of %s to %s:%s\n' "$target" "$PUID" "$PGID"
|
||||
else
|
||||
printf 'entrypoint: warning: cannot change ownership of %s to %s:%s\n' \
|
||||
"$target" "$PUID" "$PGID" >&2
|
||||
printf 'entrypoint: warning: continuing; set the ownership on the host if startup fails\n' >&2
|
||||
fi
|
||||
done
|
||||
|
||||
# Preserve the supplementary groups Compose granted through group_add - that is
|
||||
# how the Docker socket stays reachable - while discarding root's own group.
|
||||
supplementary=$(id -G | tr ' ' '\n' | grep -vx 0 | paste -sd, -)
|
||||
[ -n "$supplementary" ] || supplementary="$PGID"
|
||||
|
||||
exec setpriv --reuid "$PUID" --regid "$PGID" --groups "$supplementary" "$@"
|
||||
@@ -135,8 +135,19 @@ ENV CODEMAN_IN_CONTAINER=1 \
|
||||
HOME=/home/${CODEMAN_RUNTIME_USER} \
|
||||
NODE_ENV=production
|
||||
|
||||
# Runtime defaults for the entrypoint, matching the account created above.
|
||||
ENV PGID=${PGID} PUID=${PUID}
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
USER ${CODEMAN_RUNTIME_USER}
|
||||
# The container starts as root so the entrypoint can correct the ownership of
|
||||
# the host bind mounts, which the daemon creates as root whenever they do not
|
||||
# already exist. The entrypoint then drops to PUID:PGID with setpriv, so the
|
||||
# server itself never runs privileged. Setting `user:` in Compose bypasses both
|
||||
# steps, leaving the caller in full control.
|
||||
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
RUN chmod 0755 /usr/local/bin/entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
||||
|
||||
CMD ["node", "dist/index.js", "web"]
|
||||
|
||||
Reference in New Issue
Block a user