mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
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:
@@ -25,6 +25,7 @@ import { ensureAiDirs, recoverInterruptedInstalls } from "./lib/feature-status.j
|
||||
import { logger } from "./lib/logger.js";
|
||||
import { requestDuration } from "./lib/metrics.js";
|
||||
import { getSettingString } from "./lib/settings-helpers.js";
|
||||
import { assertStorageWritable } from "./lib/storage-writable.js";
|
||||
import { requirePermission } from "./permissions.js";
|
||||
import {
|
||||
authMiddleware,
|
||||
@@ -87,6 +88,18 @@ try {
|
||||
}
|
||||
console.log("Redis connected");
|
||||
|
||||
// Verify the local storage directories are writable before serving. A non-root
|
||||
// container launched against a volume it cannot write (TrueNAS, Kubernetes
|
||||
// runAsUser / OpenShift, or a bind mount owned by another user) would otherwise
|
||||
// boot "healthy" and fail with a cryptic EACCES on the first file operation.
|
||||
try {
|
||||
await assertStorageWritable();
|
||||
console.log("Storage directories writable");
|
||||
} catch (err) {
|
||||
console.error(`FATAL: ${(err as Error).message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Auto-import 1.x SQLite database on first boot (before default user creation)
|
||||
if (env.SQLITE_MIGRATE_PATH) {
|
||||
const { rows } = await db.execute(sql`SELECT count(*)::int AS n FROM users`);
|
||||
|
||||
Reference in New Issue
Block a user