Files
Schulcloud-MCP/Dockerfile
MechaCat02 ac61aea870 Create /data/notes in the image, and write the rollout runbook
The Dockerfile creates /data/mirror and /data/state with the right owner,
and the comment above it says exactly why: Docker initialises a new named
volume from the image directory, so a mount point the image does not have
lands root-owned and the unprivileged user gets EACCES on every write.
/data/notes was added to docker-compose.yml without being added here, so
every save on a fresh deployment would have failed that way — verified
both directions before fixing it.

docs/DEPLOY-NOTES.md is the runbook for putting this on a server that is
already running: publish, decide where the notes live *before* anything
writes one, set WEB_PASSWORD, verify, migrate, index. Plus rollback,
which is uneventful — no migration, and the old image simply ignores the
new settings and leaves the notes volume alone.

PI.md's backup table needed the bigger change. Everything else this
server stores is a copy of something upstream and a crawl rebuilds it;
the notes are not, and nothing can. They are now the one entry in that
table marked irreplaceable, and the EACCES row says to fix the volume's
ownership rather than delete it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 17:32:30 +02:00

55 lines
2.0 KiB
Docker

# syntax=docker/dockerfile:1
# Build stage: full dependency tree, compile TypeScript to dist/.
FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY tsconfig.json ./
COPY src ./src
# tsc emits only .js, so the .sql migrations are copied by this step.
COPY scripts/copy-assets.mjs ./scripts/copy-assets.mjs
RUN npm run build
# Prune to runtime dependencies only, in its own stage so the build tree
# (typescript, @types) never reaches the final image.
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev && npm cache clean --force
FROM node:22-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
# Every extractor is pure JavaScript, so the runtime image needs no build
# toolchain — just a signal-forwarding init so SIGTERM reaches node.
RUN apk add --no-cache tini
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY package.json ./
# The mirror, the state directory (a replaced session token) and the notes are
# the only writable paths. Creating them in the image with the right owner
# matters: Docker initialises a new named volume from the image directory,
# including its ownership, so without this the volume lands root-owned and the
# unprivileged user gets EACCES on every write — with the failure recorded
# rather than crashing, which makes it easy to miss. The state directory holds a
# credential and the notes are personal, so only their owner may enter either.
RUN mkdir -p /data/mirror /data/state /data/notes \
&& chown -R node:node /data \
&& chmod 700 /data/state /data/notes
# node:alpine ships an unprivileged `node` user.
USER node
EXPOSE 8080
ENV PORT=8080 BIND_HOST=0.0.0.0
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD node -e "fetch('http://127.0.0.1:'+(process.env.PORT||8080)+'/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "dist/bin/http.js"]