#303 follow-up: the fonts volume must mount node-owned
All checks were successful
CI / Auth e2e pack (pull_request) Successful in 8m49s
CD / Build and push images (push) Successful in 14s
CD / Deploy to Test (push) Successful in 17s
CD / Smoke tests against Test (push) Successful in 1m21s
CD / Promote to Int (push) Successful in 12s
CI / Lint, typecheck, test (push) Successful in 6m35s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m30s
CI / Import/export fidelity gate (push) Successful in 57s
CI / Build container images (pull_request) Successful in 2m52s
CI / Lint, typecheck, test (pull_request) Successful in 6m28s
CI / Import/export fidelity gate (pull_request) Successful in 57s

Found on the real deploy, not in any test: `/data/fonts` in the running
api container was `root:root` and the non-root `node` user could not
write to it. Every upload would have failed with EACCES at runtime while
the api reported ready.

The api Dockerfile already explains the mechanism for uploads and
plugins — Docker copies an image directory's ownership into a fresh named
volume on first mount — and pre-creates them chowned. #303 added
`CUSTOM_FONTS_DIR` to the ENV but not to that mkdir/chown line.

Adds a CI fence so it cannot recur: every `/data/…` path the api image
defaults to must also appear in the mkdir AND the chown. Verified against
the actual defect — removing `/data/fonts` from the chown makes it fail.
This commit is contained in:
Claude Opus 5 2026-08-01 15:12:06 +02:00
parent b96997501a
commit 485c8fa538
2 changed files with 21 additions and 2 deletions

View File

@ -107,6 +107,24 @@ jobs:
exit 1
fi
# A fresh named volume inherits the ownership of the image directory it
# is mounted over. Every /data/… path the api image defaults to must
# therefore be pre-created AND chowned to `node`, or the non-root user
# cannot write to it — found on a real deploy in #303, where the env
# entry was added but the mkdir/chown line was not.
- name: api image pre-creates its data directories node-owned
run: |
set -euo pipefail
dirs=$(grep -oE '[A-Z_]+_DIR=/data/[a-z]+' apps/api/Dockerfile | cut -d= -f2 | sort -u)
bad=0
for d in $dirs; do
grep -q "mkdir -p .*$d" apps/api/Dockerfile || {
echo "$d is not pre-created in apps/api/Dockerfile"; bad=1; }
grep -q "chown -R node:node .*$d" apps/api/Dockerfile || {
echo "$d is not chowned to node in apps/api/Dockerfile"; bad=1; }
done
exit "$bad"
- name: Set up pnpm
uses: pnpm/action-setup@v4

View File

@ -34,13 +34,14 @@ WORKDIR /app
COPY --from=build --chown=node:node /out /app
# Generate the Prisma client for this image's platform.
RUN node node_modules/prisma/build/index.js generate
# A fresh named volume mounted at /data/uploads or /data/plugins is created
# A fresh named volume mounted at /data/uploads, /data/plugins or /data/fonts
# is created
# root-owned; pre-creating them here (Docker copies an image directory's
# ownership into a new volume on first mount) lets the non-root `node` user
# write to them. /data/backups is mounted read-only here, but pre-creating it
# node-owned keeps the shared `backups` volume writable for the backup
# sidecar even when the api container is the one that initializes it.
RUN mkdir -p /data/uploads /data/plugins /data/secrets /data/backups && chown -R node:node /data/uploads /data/plugins /data/secrets /data/backups
RUN mkdir -p /data/uploads /data/plugins /data/fonts /data/secrets /data/backups && chown -R node:node /data/uploads /data/plugins /data/fonts /data/secrets /data/backups
USER node
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \