Self-healing volume ownership: no more manual chown, no more EACCES
Sync upstream and build images / sync-and-build (push) Failing after 22s
Sync upstream and build images / sync-and-build (push) Failing after 22s
A bind mount arrives with the host's ownership, so a freshly created appdata directory killed dsh with EACCES on mkdir /home/node/.dsh/profiles/web. Only root inside the container can fix that, so: - docker-entrypoint.sh starts as root, aligns the node account with PUID/PGID (default 1000:1000, Unraid convention), takes ownership of the state volume when needed, then setpriv's down before exec'ing dsh. The network-reachable process is never root and no setuid binary is left behind. The workspace mount point is made writable but its CONTENTS are left alone unless DSH_CHOWN_WORKSPACE=1 — never silently rewrite ownership across user data. - Dropped USER node (root is required to chown); dsh is wrapped so a root re-execs as PUID:PGID and cannot leave root-owned files in the volume. Wrapper resolves the real bin.js at build time and self-tests. - Explicit --user is honored: ownership cannot be repaired, so the entrypoint verifies writability and exits with one actionable line instead of a Node stack trace. - XDG_CONFIG_HOME pinned into the volume too; pnpm was reaching for /root/.config when HOME was unset. - CI: labels now carry a recipe hash, and the freshness check cuts a -rN revision when the recipe changes — so an image fix reaches the pinned deployment without waiting for an upstream release. - README/template: chown step removed, PUID/PGID/DSH_CHOWN_WORKSPACE documented, template pinned to 0.1.1-rc.2-r1. Verified with a 23-case matrix: root-owned volume self-heals and serves 200, already-correct volume skips the chown, 99:100 + PUID/PGID works, plugin add/remove via root docker exec leaves no root-owned files, explicit --user still works and fails clearly when it can't, both argument forms serve. Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
1 parent
d6a97dd8ea
commit
acd81b2f79
5 files changed
+243
-61
No files matched your search
@@ -0,0 +1,94 @@
|
||||
#!/bin/sh
|
||||
# Make the container's mounted volumes usable no matter how the host owns them.
|
||||
#
|
||||
# A bind mount arrives with the HOST's ownership, so an image that simply runs
|
||||
# as uid 1000 dies with `EACCES … mkdir '/home/node/.dsh/profiles/web'` on any
|
||||
# freshly created appdata directory. The fix has to happen inside the
|
||||
# container, and only root can chown. So this entrypoint starts as root, takes
|
||||
# ownership of the state volume, and then drops to PUID:PGID (default
|
||||
# 1000:1000) with setpriv — dsh itself never runs as root, and there is no
|
||||
# setuid binary left behind for it to climb back through.
|
||||
#
|
||||
# PUID/PGID follow the usual Unraid convention: set them if your appdata is
|
||||
# owned by something else (Unraid's share default is 99:100) and the volume is
|
||||
# adjusted to match at startup.
|
||||
#
|
||||
# If the container is started with an explicit user (`--user`), repairing
|
||||
# ownership is impossible; this prints one actionable line instead of a Node
|
||||
# stack trace.
|
||||
set -eu
|
||||
|
||||
PUID="${PUID:-1000}"
|
||||
PGID="${PGID:-1000}"
|
||||
STATE="${DSH_HOME:-/home/node/.dsh}"
|
||||
HOME_DIR=/home/node
|
||||
WORKSPACE=/workspace
|
||||
PATCH=/opt/deepseek-harness/web.cordis.patch.yml
|
||||
|
||||
log() { printf 'dsh-entrypoint: %s\n' "$*" >&2; }
|
||||
|
||||
# Unraid Post Arguments replace the image CMD wholesale, so a bare flag list
|
||||
# ("--trusted-host tower.local") would otherwise reach dsh with no subcommand.
|
||||
# Prepend the default web invocation in that case; a full command line
|
||||
# ("web --patch … --no-open") is passed through untouched.
|
||||
if [ "$#" -eq 0 ]; then
|
||||
set -- web --patch "$PATCH" --no-open
|
||||
else
|
||||
case "$1" in
|
||||
-*) set -- web --patch "$PATCH" --no-open "$@" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# True when anything under $1 is not owned by PUID:PGID. `-print -quit` stops
|
||||
# at the first hit, so the common "already correct" case costs one stat.
|
||||
needs_chown() {
|
||||
[ -n "$(find "$1" \( ! -uid "$PUID" -o ! -gid "$PGID" \) -print -quit 2>/dev/null)" ]
|
||||
}
|
||||
|
||||
if [ "$(id -u)" = 0 ]; then
|
||||
# Keep the `node` account aligned with the requested ids so $HOME, group
|
||||
# lookups and setpriv all resolve even for ids with no passwd entry.
|
||||
CUR_UID="$(id -u node)"
|
||||
CUR_GID="$(id -g node)"
|
||||
[ "$PGID" = "$CUR_GID" ] || { log "moving group node to gid $PGID"; groupmod -o -g "$PGID" node; }
|
||||
[ "$PUID" = "$CUR_UID" ] || { log "moving user node to uid $PUID"; usermod -o -u "$PUID" node; }
|
||||
|
||||
mkdir -p "$STATE" "$WORKSPACE"
|
||||
chown "$PUID:$PGID" "$HOME_DIR" 2>/dev/null || true
|
||||
|
||||
# State volume is entirely dsh's own (settings, credentials, sessions,
|
||||
# plugins, pnpm store) — safe to take wholesale.
|
||||
if needs_chown "$STATE"; then
|
||||
log "taking ownership of $STATE for ${PUID}:${PGID}"
|
||||
chown -R "$PUID:$PGID" "$STATE"
|
||||
fi
|
||||
|
||||
# The workspace holds the operator's data, so only the mount point itself
|
||||
# is adjusted (enough for the agent to create files). Rewriting ownership
|
||||
# across someone's project tree is opt-in, never a silent side effect.
|
||||
if [ "$(stat -c '%u:%g' "$WORKSPACE")" != "${PUID}:${PGID}" ]; then
|
||||
chown "$PUID:$PGID" "$WORKSPACE"
|
||||
fi
|
||||
if [ "${DSH_CHOWN_WORKSPACE:-0}" = 1 ]; then
|
||||
if needs_chown "$WORKSPACE"; then
|
||||
log "DSH_CHOWN_WORKSPACE=1: taking ownership of everything under $WORKSPACE"
|
||||
chown -R "$PUID:$PGID" "$WORKSPACE"
|
||||
fi
|
||||
elif needs_chown "$WORKSPACE"; then
|
||||
log "note: some files under $WORKSPACE are not owned by ${PUID}:${PGID}; the agent"
|
||||
log " may be unable to modify them. Set DSH_CHOWN_WORKSPACE=1 to take ownership."
|
||||
fi
|
||||
|
||||
export HOME="$HOME_DIR"
|
||||
exec setpriv --reuid "$PUID" --regid "$PGID" --init-groups dsh "$@"
|
||||
fi
|
||||
|
||||
# Non-root start (explicit --user): verify, don't guess.
|
||||
if [ ! -w "$STATE" ]; then
|
||||
log "ERROR: $STATE is not writable by uid $(id -u):$(id -g) — it is owned by $(stat -c '%u:%g' "$STATE" 2>/dev/null || echo '?')."
|
||||
log "This container was started with an explicit --user, so it cannot repair that itself."
|
||||
log "Either drop --user (the entrypoint then takes ownership at startup), or on the host run:"
|
||||
log " chown -R $(id -u):$(id -g) <host path mounted at $STATE>"
|
||||
exit 1
|
||||
fi
|
||||
exec dsh "$@"
|
||||
Reference in new issue
Block a user