Skip to content

fix: auto-fix upload-volume ownership in Docker, fail loudly on worker misuse - #22

Merged
veniplex merged 1 commit into
mainfrom
claude/docker-volume-permissions
Jul 18, 2026
Merged

veniplex merged 1 commit into
mainfrom
claude/docker-volume-permissions

Conversation

@veniplex

Copy link
Copy Markdown
Owner

Problem

A self-hosted deployment on the standard docker-compose.yml pattern hit "Der Datei-Speicher ist nicht beschreibbar" in the admin panel (the storage-writable health check added in v1.1). Root cause: Docker Compose creates a bind-mount source directory (./data/uploads) owned by root on the host the first time it doesn't already exist, but the image runs the app as a non-root app user — so every write fails with EACCES. This affects essentially every fresh deployment following the documented Quick Start, not just this one report.

Fix

docker-entrypoint.sh now starts as root, corrects ownership of UPLOAD_DIR/TUS_DIR if needed (skipped once already correct, so no recursive chown walk on every restart of a long-lived deployment), then drops to the app user via su-exec before running migrations and starting the app — the same privilege-drop pattern official images like postgres/mysql use. The Dockerfile no longer sets a permanent USER app, since the entrypoint now owns that.

Second finding (same file, same investigation)

While tracing this, I found the entrypoint unconditionally ran node server.js regardless of any command: override — so a worker: service configured per the docs (command: ["npm", "run", "worker"], pointed at the prebuilt image) doesn't actually run the worker. It silently starts a second, unreachable copy of the web server instead, since the prebuilt standalone image doesn't include tsx or the raw TypeScript sources the worker entrypoint needs. No crash, no error — just quietly wrong, which is worse than a visible failure.

The entrypoint now forwards a given command and lets it fail loudly (missing script / missing tsx) instead of silently substituting the web server. Documented in the new Troubleshooting section: don't point worker: at the prebuilt image — the web tier already runs jobs in-process by default, which is sufficient for most deployments; a real dedicated worker needs a custom image built from a full source checkout (out of scope here — happy to build that separately if there's demand).

Changes

  • docker-entrypoint.sh: root-then-drop-privileges + command forwarding (see above)
  • Dockerfile: adds su-exec, removes the now-redundant USER app
  • docs/self-hosting.md: new Troubleshooting section for both issues, including the manual one-off fix for anyone already on an older image

Verification

No Docker daemon available in this sandbox to run a real docker build, so I could not execute this end-to-end. I did:

  • Read the actual Alpine/Node base image behavior and the existing Dockerfile carefully; su-exec is a standard Alpine package used by numerous official images for exactly this pattern.
  • sh -n docker-entrypoint.sh — syntax check passes; script is plain POSIX sh (no bashisms), consistent with Alpine's ash.
  • Traced through both code paths by hand: the default (study-app, no args) path and the worker (npm run worker args) path, including what happens if UPLOAD_DIR/TUS_DIR are already correctly owned (fast top-level-only check, no full walk).
  • npm run typecheck && npm run lint && npm test — unaffected by this change, still green (126 passed).

Please have CI's Docker build validation (release-candidate label) confirm the image actually builds and starts before merging — that's the one thing I couldn't verify locally.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UrdkggYqhNCCZVHyJrdwBR


Generated by Claude Code

…misuse

Docker Compose creates a missing bind-mount source directory (e.g.
./data/uploads) owned by root on the host. The image runs as a non-root
"app" user, so every write there failed with EACCES — surfacing as "Upload
failed" / the new storage-writable health check going red, with no
indication of the actual cause.

- docker-entrypoint.sh now starts as root, chowns UPLOAD_DIR/TUS_DIR to
  "app" if they aren't already (skipped on later starts once ownership is
  correct, so no recursive walk on every restart), then drops to "app" via
  su-exec before running migrations and starting the app — the same
  pattern official images like postgres use. Dockerfile no longer sets a
  permanent USER, since the entrypoint now handles the privilege drop.
- the entrypoint also now forwards a custom command (e.g. compose's
  `command: ["npm", "run", "worker"]`) instead of silently ignoring it and
  always starting the web server. The prebuilt standalone image can't
  actually run the dedicated worker (needs tsx + raw TS sources it doesn't
  ship), so this turns a silent, hard-to-notice misconfiguration — a
  "worker" container that was actually just running an unreachable second
  copy of the web server — into an immediate, clear startup failure.
- docs/self-hosting.md: new Troubleshooting section covering both.
@veniplex veniplex added the release-candidate Creates a new release and deployment of docker image, once merged. label Jul 18, 2026 — with Claude
@veniplex
veniplex merged commit db28b21 into main Jul 18, 2026
1 of 2 checks passed
@veniplex
veniplex deleted the claude/docker-volume-permissions branch July 18, 2026 16:36
github-actions Bot added a commit that referenced this pull request Jul 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release-candidate Creates a new release and deployment of docker image, once merged.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants