Files
Schulcloud-MCP/docker-compose.yml
MechaCat02 973b82ebf5 Replace the Schulcloud token without a restart
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>
2026-09-16 20:19:16 +02:00

81 lines
2.3 KiB
YAML

# Standalone Compose file for the Pi.
#
# If you already run Caddy and PostgreSQL from another Compose project, either
# merge the `schulcloud-mcp` service below into that project's file, or keep
# this file separate and attach it to the existing Caddy network — see the
# `networks` block at the bottom and deploy/Caddyfile.snippet.
services:
# Dev/standalone Postgres. On the Pi, point DATABASE_URL at the existing
# instance instead and remove this service — the schema lives in its own
# database and user, so it coexists with whatever else is already there.
postgres:
image: postgres:17-alpine
container_name: schulcloud-mcp-db
restart: unless-stopped
environment:
POSTGRES_DB: schulcloud
POSTGRES_USER: schulcloud
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-schulcloud}
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- caddy
healthcheck:
test: ["CMD-SHELL", "pg_isready -U schulcloud -d schulcloud"]
interval: 10s
timeout: 5s
retries: 5
schulcloud-mcp:
build: .
image: schulcloud-mcp:latest
container_name: schulcloud-mcp
restart: unless-stopped
env_file: .env
environment:
PORT: 8080
BIND_HOST: 0.0.0.0
MIRROR_DIR: /data/mirror
STATE_DIR: /data/state
depends_on:
postgres:
condition: service_healthy
volumes:
# The mirror and a replaced Schulcloud token are the only things this
# server writes; everything else stays read-only, so each gets its own
# volume rather than loosening read_only.
- mirror:/data/mirror
- state:/data/state
# No ports are published to the host: Caddy reaches the container over the
# shared Docker network, so the only way in from the internet is through
# Caddy's TLS and this server's bearer check.
expose:
- "8080"
networks:
- caddy
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
security_opt:
- no-new-privileges:true
read_only: true
tmpfs:
- /tmp
cap_drop:
- ALL
volumes:
pgdata:
mirror:
state:
networks:
caddy:
# Set to true once this joins the network your existing Caddy already uses,
# and change the name to match (`docker network ls` to find it).
external: false
name: caddy