A token lasts 30 days and only a browser login yields one — the account is federated, so the server cannot mint it. Replacing it meant editing .env and recreating the container, every month. `schulcloud token set` (a hidden prompt, or piped input) and a /token page both send it to PUT /api/token. The server checks it with Schulcloud first — well-formed, unexpired, still logged in, the same account — then swaps it into the config every request reads, restarts the keepalive and saves it in STATE_DIR, a new volume, with mode 0600. At startup the newer of the saved token and TSC_JWT_COOKIE wins, unless they belong to different accounts. A refused paste changes nothing, and the token is never logged. The keepalive's pings carry a generation, so a 401 for the old token that arrives after a swap cannot stop the new cycle. `schulcloud token`, whoami and the log report the expiry and warn a week ahead. Found on the way: a host that is off for more than two hours loses the session however long the token has left — this machine lost it overnight — which is what the always-on Pi is for. 174 tests. Smoke 72/72 on the local instance, and a real swap verified end to end there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
53 lines
1.9 KiB
Docker
53 lines
1.9 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 and the state directory (a replaced session token) 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,
|
|
# so only its owner may enter it.
|
|
RUN mkdir -p /data/mirror /data/state && chown -R node:node /data && chmod 700 /data/state
|
|
|
|
# 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"]
|