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