Files
Schulcloud-MCP/Dockerfile
MechaCat02 e6f2258df9 Local dev setup; fix mirror volume ownership and a test footgun
Adds docker-compose.override.yml (local-only: publishes the server on
127.0.0.1:8080 and Postgres on 127.0.0.1:55432, crawls on demand) and
docs/LOCAL.md covering the stack, Claude Code registration over both
transports, the CLI, and the test suites.

Two bugs that only running the real container could find:

The mirror volume was root-owned while the container runs as node, so
every file write failed with EACCES. Docker initialises a named volume
from the image directory including its ownership, so the fix is to
create /data/mirror owned by node in the image. This was easy to miss
because the indexer records a per-file failure rather than crashing —
the crawl "succeeded" with 4 skipped. Earlier direct-node testing missed
it entirely by writing to a scratch dir owned by the developer.

The store tests TRUNCATE, and pointing TEST_DATABASE_URL at the dev
database put their fixtures into real data. They now refuse any database
whose name does not contain "test".

Verified against the rebuilt container: 4 files mirrored, image-only PDF
detection firing in the real pipeline, both transports showing
✔ Connected in `claude mcp list`, and a whoami tool call driven end to
end through `claude -p`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 21:57:35 +02:00

51 lines
1.8 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 is the one writable path. Creating it 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 per
# file rather than crashing, which makes it easy to miss.
RUN mkdir -p /data/mirror && chown -R node:node /data
# 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"]