mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix: set a writable HOME for the app user so PaddleOCR works in non-root deployments (#430)
The container dropped privileges to the non-root snapotter user via gosu
(external) and s6-setuidgid (embedded), both of which preserve the
environment without setting HOME. The app therefore kept root's HOME=/root,
which is not writable by snapotter, and PaddleOCR died with
PermissionError: '/root/.paddlex/temp' -- breaking the ocr tool at default
quality in every non-root deployment. Prior GPU QA ran the app as root, which
masked it.
Fix: export HOME=/data/.home (persistent, writable, hidden) at every
privilege-drop point:
- entrypoint.sh external gosu path and non-root tini path (the latter uses
$DD/.home so a DATA_DIR override stays consistent).
- the s6 snapotter/run service (scoped there, not globally before /init, so
postgres/redis do not inherit a snapotter-owned HOME).
The root preflight creates /data/.home and the existing chown sweep owns it as
the PUID/PGID-remapped snapotter; the dir is added to both ensure_writable
probes so an unwritable HOME fails fast with the storage-permission guidance
instead of crashing late. The Dockerfile passwd home moves from /app
(read-only) to /data/.home as the getpwuid fallback when HOME is unset.
Because bridge.ts forwards HOME to the Python sidecar, this also repairs the
expanduser("~") caches in inpaint/outpaint/restore/noise_removal/remove_bg,
not just PaddleOCR.
Also fixes a test-harness inconsistency: tool-default-settings passport-photo
countryCode "us" -> "US" (the route exact-matches uppercase PASSPORT_SPECS
codes; the UI already sends "US", so users were never affected).
Claude-Session: https://claude.ai/code/session_01XGB4pGvTvb7sUX4JN745U7
This commit is contained in:
+4
-1
@@ -557,7 +557,10 @@ ENV PYTHONWARNINGS=default \
|
|||||||
PADDLE_PDX_DISABLE_MODEL_SOURCE_CHECK=True
|
PADDLE_PDX_DISABLE_MODEL_SOURCE_CHECK=True
|
||||||
|
|
||||||
# Create non-root user for runtime
|
# Create non-root user for runtime
|
||||||
RUN groupadd -r snapotter && useradd -r -g snapotter -d /app -s /sbin/nologin snapotter
|
# Home is /data/.home (created + chowned by the entrypoint at runtime). Runtime
|
||||||
|
# also exports HOME; this passwd entry is the writable fallback for any code that
|
||||||
|
# resolves ~ via getpwuid when HOME is unset (the old /app home was read-only).
|
||||||
|
RUN groupadd -r snapotter && useradd -r -g snapotter -d /data/.home -s /sbin/nologin snapotter
|
||||||
# /app and /opt/venv are read-only at runtime -> owned by snapotter.
|
# /app and /opt/venv are read-only at runtime -> owned by snapotter.
|
||||||
# /data and /tmp/workspace are written at runtime: make them group-0 (root group)
|
# /data and /tmp/workspace are written at runtime: make them group-0 (root group)
|
||||||
# owned and group-writable with the setgid bit so the app can still write when the
|
# owned and group-writable with the setgid bit so the app can still write when the
|
||||||
|
|||||||
+13
-4
@@ -81,8 +81,10 @@ DD="${DATA_DIR:-/data}"
|
|||||||
# user and fail fast with actionable guidance -- otherwise the venv bootstrap
|
# user and fail fast with actionable guidance -- otherwise the venv bootstrap
|
||||||
# and the app below would die later with a cryptic EACCES.
|
# and the app below would die later with a cryptic EACCES.
|
||||||
if [ "$(id -u)" != "0" ]; then
|
if [ "$(id -u)" != "0" ]; then
|
||||||
mkdir -p "$DD/files" "$DD/logs" "$DD/ai/models" "$DD/ai/pip-cache" "$DD/ai/venv" "$WS" 2>/dev/null || true
|
mkdir -p "$DD/files" "$DD/logs" "$DD/ai/models" "$DD/ai/pip-cache" "$DD/ai/venv" "$WS" "$DD/.home" 2>/dev/null || true
|
||||||
ensure_writable "$WS" "$DD" || exit 1
|
# Group-writable so a second in-group-0 UID (OpenShift / fsGroup:0) can reuse HOME.
|
||||||
|
chmod g+rwX "$DD/.home" 2>/dev/null || true
|
||||||
|
ensure_writable "$WS" "$DD" "$DD/.home" || exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Clean up any interrupted bootstrap from a previous start
|
# Clean up any interrupted bootstrap from a previous start
|
||||||
@@ -216,7 +218,7 @@ if [ "$(id -u)" = "0" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Ensure all writable subdirectories exist before chown
|
# Ensure all writable subdirectories exist before chown
|
||||||
mkdir -p /data/files /data/logs /data/ai/models /data/ai/pip-cache /data/ai/venv /tmp/workspace
|
mkdir -p /data/files /data/logs /data/ai/models /data/ai/pip-cache /data/ai/venv /tmp/workspace /data/.home
|
||||||
[ -n "${EMBEDDED_MODE:-}" ] && mkdir -p /data/redis
|
[ -n "${EMBEDDED_MODE:-}" ] && mkdir -p /data/redis
|
||||||
|
|
||||||
# Chown writable directories (/data is the persistent volume, /tmp/workspace is
|
# Chown writable directories (/data is the persistent volume, /tmp/workspace is
|
||||||
@@ -238,7 +240,7 @@ if [ "$(id -u)" = "0" ]; then
|
|||||||
# Root can write anywhere, so verify as the unprivileged snapotter user that
|
# Root can write anywhere, so verify as the unprivileged snapotter user that
|
||||||
# actually runs the app. This catches root-squashed or foreign-owned mounts
|
# actually runs the app. This catches root-squashed or foreign-owned mounts
|
||||||
# where the chown above silently failed, and fails fast with guidance.
|
# where the chown above silently failed, and fails fast with guidance.
|
||||||
if ! gosu snapotter sh -c '. /usr/local/bin/entrypoint-lib.sh; ensure_writable "$@"' _ "$WS" "$DD"; then
|
if ! gosu snapotter sh -c '. /usr/local/bin/entrypoint-lib.sh; ensure_writable "$@"' _ "$WS" "$DD" /data/.home; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -250,10 +252,17 @@ if [ "$(id -u)" = "0" ]; then
|
|||||||
fi
|
fi
|
||||||
# External mode: tini becomes PID 1 (reaps zombies, forwards signals) and runs
|
# External mode: tini becomes PID 1 (reaps zombies, forwards signals) and runs
|
||||||
# the app as snapotter, the same end state as the prior tini ENTRYPOINT.
|
# the app as snapotter, the same end state as the prior tini ENTRYPOINT.
|
||||||
|
# gosu preserves the environment (unlike su), so HOME=/root would survive the
|
||||||
|
# drop; set a writable HOME or libraries that write under ~ (PaddleOCR's
|
||||||
|
# ~/.paddlex, plus the sidecar's expanduser caches) fail with EACCES.
|
||||||
|
export HOME=/data/.home
|
||||||
exec tini -- gosu snapotter "$@"
|
exec tini -- gosu snapotter "$@"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Already running as snapotter (e.g. Kubernetes runAsUser). External mode only:
|
# Already running as snapotter (e.g. Kubernetes runAsUser). External mode only:
|
||||||
# embedded mode requires root and exited earlier. tini becomes PID 1.
|
# embedded mode requires root and exited earlier. tini becomes PID 1.
|
||||||
|
# HOME points at the data volume (created and writability-checked in the
|
||||||
|
# non-root preflight above) so ~-based caches land somewhere writable.
|
||||||
print_banner
|
print_banner
|
||||||
|
export HOME="$DD/.home"
|
||||||
exec tini -- "$@"
|
exec tini -- "$@"
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
#!/command/with-contenv sh
|
#!/command/with-contenv sh
|
||||||
cd /app/apps/api
|
cd /app/apps/api
|
||||||
|
# s6-setuidgid preserves the environment and does not set HOME, so the app would
|
||||||
|
# inherit root's HOME=/root. Scope a writable HOME to this service only (not a
|
||||||
|
# global export before /init) so postgres/redis do not inherit a snapotter-owned
|
||||||
|
# home. Created and chowned by the entrypoint root preflight before s6 starts.
|
||||||
|
export HOME=/data/.home
|
||||||
exec s6-setuidgid snapotter ./node_modules/.bin/tsx --import ./src/tracing.ts --import ./src/instrument.ts src/index.ts
|
exec s6-setuidgid snapotter ./node_modules/.bin/tsx --import ./src/tracing.ts --import ./src/instrument.ts src/index.ts
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const TOOL_SETTINGS_OVERRIDES: Record<string, unknown> = {
|
|||||||
convert: { format: "png" },
|
convert: { format: "png" },
|
||||||
"watermark-text": { text: "Test" },
|
"watermark-text": { text: "Test" },
|
||||||
"text-overlay": { text: "Test" },
|
"text-overlay": { text: "Test" },
|
||||||
"passport-photo": { countryCode: "us" },
|
"passport-photo": { countryCode: "US" },
|
||||||
"trim-video": { startS: 0, endS: 5 },
|
"trim-video": { startS: 0, endS: 5 },
|
||||||
"trim-audio": { startS: 0, endS: 5 },
|
"trim-audio": { startS: 0, endS: 5 },
|
||||||
"split-pdf": { mode: "range", range: "1" },
|
"split-pdf": { mode: "range", range: "1" },
|
||||||
|
|||||||
Reference in New Issue
Block a user