fix(docker): make storage writable under non-root/foreign UIDs (TrueNAS, OpenShift) (#299)

The entrypoint only fixed volume permissions when started as root (chown +
gosu-drop to snapotter). Launched under a non-root/foreign UID (TrueNAS app
user, Kubernetes runAsUser, OpenShift) it did no permission setup, so /data and
/tmp/workspace -- owned by uid 999 from the image -- were not writable by the
running user. Uploads and processing then failed with a cryptic EACCES
("workspace folder is not writable") and AI bundle installs failed the same way,
while health checks still reported the container healthy.

- entrypoint: source new entrypoint-lib.sh; verify writability up front when
  non-root, and as snapotter after chown when root (catches root-squashed
  mounts), failing fast with an actionable message (which dir, uid/gid, how to
  fix) instead of a late, cryptic EACCES
- Dockerfile: own /data and /tmp/workspace as snapotter:0, group-writable with
  setgid, so an arbitrary UID with the root supplementary group (OpenShift /
  Kubernetes fsGroup) can write; keep /opt/venv world-readable for the AI venv
  bootstrap under arbitrary UIDs
- api: assert storage writability at boot (lib/storage-writable.ts), failing
  fast with the same guidance even when the entrypoint is bypassed
- docs: add a Storage permissions section (named volumes, bind mounts, TrueNAS,
  Kubernetes/OpenShift) and cross-link it from the security guide

Fixes #230
This commit is contained in:
SnapOtter
2026-06-22 16:58:59 +08:00
committed by GitHub
parent 5a6368db89
commit 1fec97111b
9 changed files with 392 additions and 3 deletions
+14 -2
View File
@@ -393,10 +393,22 @@ ENV PYTHONWARNINGS=default \
# Create non-root user for runtime
RUN groupadd -r snapotter && useradd -r -g snapotter -d /app -s /sbin/nologin snapotter
RUN chown -R snapotter:snapotter /app /data /tmp/workspace /opt/venv
# /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)
# owned and group-writable with the setgid bit so the app can still write when the
# container is launched under an arbitrary/foreign UID (Kubernetes runAsUser,
# OpenShift, TrueNAS), which always lands in the root (GID 0) supplementary group.
# The root entrypoint re-chowns these to snapotter for the default gosu path.
RUN chown -R snapotter:snapotter /app /opt/venv && \
chmod -R a+rX /opt/venv && \
chown -R snapotter:0 /data /tmp/workspace && \
chmod -R g+rwX /data /tmp/workspace && \
find /data /tmp/workspace -type d -exec chmod g+s {} +
# Entrypoint fixes volume permissions then drops to snapotter via gosu
# Entrypoint fixes volume permissions then drops to snapotter via gosu.
# entrypoint-lib.sh holds the writability helpers it sources at startup.
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
COPY docker/entrypoint-lib.sh /usr/local/bin/entrypoint-lib.sh
COPY docker/wait-for-postgres.mjs /app/docker/wait-for-postgres.mjs
RUN chmod +x /usr/local/bin/entrypoint.sh