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>
81 lines
2.3 KiB
YAML
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
|