From effe7e3e723562af5a2b70e88c51fe106b69adec Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 06:26:39 +0000 Subject: [PATCH 1/3] fix(docker): make bind-mounted data folders writable on startup The image switched to the node user (uid 1000) at build time, so a host folder mounted at /data kept its host ownership and SQLite failed with SQLITE_CANTOPEN in a restart loop. Named and anonymous volumes worked only because Docker copies the image's /data ownership into them. The container now starts as root, hands DATA_DIR and the database folder to node (only entries not already owned by it), and execs the app via setpriv as node. Containers started with an explicit `user:` skip this. Adds a CI step that runs the image with a host-owned bind mount. Fixes #93 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016MHECwBhBpM248EFSBi4L5 --- .github/workflows/ci.yml | 16 ++++++++++++++++ Dockerfile | 6 ++++-- crumbs-entrypoint.sh | 20 ++++++++++++++++++++ docs/DEPLOYMENT.md | 4 ++++ 4 files changed, 44 insertions(+), 2 deletions(-) create mode 100755 crumbs-entrypoint.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eca1040..e0522e3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -125,5 +125,21 @@ jobs: push: false provenance: false tags: crumbs:ci + load: true cache-from: type=gha,scope=ci-docker cache-to: type=gha,scope=ci-docker,mode=max + + # Regression for #93: a host folder bind-mounted at /data is owned by the + # host user (the runner, uid 1001), not the container's node user (1000) + - name: Start with a host-owned bind mount + run: | + mkdir -p "$RUNNER_TEMP/crumbs-data" + docker run -d --name crumbs -p 3000:3000 -v "$RUNNER_TEMP/crumbs-data:/data" crumbs:ci + for i in $(seq 1 30); do + if curl -fsS -o /dev/null http://localhost:3000/login; then + test -f "$RUNNER_TEMP/crumbs-data/crumbs.db" && exit 0 + fi + sleep 1 + done + docker logs crumbs + exit 1 diff --git a/Dockerfile b/Dockerfile index 00d629a..a2ea7b4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -37,8 +37,10 @@ COPY --from=build /app/package.json ./ # Create data directory RUN mkdir -p /data && chown -R node:node /data -# Use non-root user -USER node +# Runs as root only long enough to hand the data folder (often a host bind +# mount) to the node user, then drops privileges — see crumbs-entrypoint.sh +COPY crumbs-entrypoint.sh /usr/local/bin/crumbs-entrypoint.sh +ENTRYPOINT ["crumbs-entrypoint.sh"] # Configuration ENV NODE_ENV=production diff --git a/crumbs-entrypoint.sh b/crumbs-entrypoint.sh new file mode 100755 index 0000000..2803636 --- /dev/null +++ b/crumbs-entrypoint.sh @@ -0,0 +1,20 @@ +#!/bin/sh +# Starts as root so a bind-mounted data folder owned by the host user can be +# handed to the unprivileged `node` user, then drops privileges before running +# the app. Without this, a host folder mounted at /data is not writable by +# uid 1000 and SQLite fails with SQLITE_CANTOPEN. +set -e + +if [ "$(id -u)" = "0" ]; then + for dir in "${DATA_DIR:-/data}" "$(dirname "${DATABASE_URL:-/data/crumbs.db}")"; do + mkdir -p "$dir" + # Only touch entries not already owned by node, so restarts on a large + # attachment folder stay fast. + find "$dir" \! -user node -exec chown node:node {} + || + echo "warning: could not change ownership of $dir; it must be writable by uid 1000" >&2 + done + exec setpriv --reuid=node --regid=node --init-groups "$@" +fi + +# Started with an explicit non-root user (e.g. `user:` in compose): run as-is. +exec "$@" diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 83b973d..7665b15 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -26,6 +26,10 @@ services: restart: unless-stopped ``` +The app runs as the image's `node` user (uid 1000). On startup the container hands ownership of a +bind-mounted data folder to uid 1000, so a folder created by any host user works as-is. If you set +`user:` in compose, that step is skipped and the folder must already be writable by that user. + ### Reverse Proxy (nginx) ```nginx From d71dbe0f4a7f91652ccf9e268b8678d5999c9313 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 6 Oct 2026 07:05:30 +0000 Subject: [PATCH 2/3] chore: bump version to 0.35.1 --- package.json | 2 +- website/index.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index a3fd077..8a9c291 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "crumbs", "private": true, - "version": "0.35.0", + "version": "0.35.1", "type": "module", "packageManager": "pnpm@9.15.9", "license": "MIT", diff --git a/website/index.html b/website/index.html index 41c9eaa..3ca8ff4 100644 --- a/website/index.html +++ b/website/index.html @@ -292,7 +292,7 @@
  • Docs
  • License
  • - v0.35.0 + v0.35.1 From 31732e8144d8e08a044e62f1d7cee8aeec333070 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 6 Oct 2026 07:11:38 +0000 Subject: [PATCH 3/3] chore: pin docker-compose to v0.35.1 [skip ci] --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 4e1820c..817abf5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: crumbs: - image: ghcr.io/bretzel-app/crumbs:0.35.0 + image: ghcr.io/bretzel-app/crumbs:0.35.1 container_name: crumbs ports: - "3000:3000"