Files
Schulcloud-MCP/local-instance/docker-compose.yml
MechaCat02 a3aded110c Add a local Schulcloud instance modelled on the live target
A Docker Compose stack that runs the deployed images
(quay.io/schulcloudverbund/*, thr theme, tag 33.40 — the versions
schulcloud-thueringen.de reports) rather than a rebuild of main, so what we
develop against is the deployed artefact. It exists to produce the states we
can otherwise only observe read-only: log in as the teacher, grade, then read
it back the way the MCP server does.

Faithful where it matters and honest where it isn't:

- Feature flags in env/api.env are a replay of GET /api/v3/config/public from
  the live instance, not a hand-picked set; instance identity mirrors the thr
  group_vars from dof_app_deploy.
- The proxy is generated from the deployment's own ingress table
  (scripts/gen-proxy-conf.py) so the legacy-client / SPA / API path split
  matches production; getting it wrong tests a different application.
- Valkey runs in `single` mode so the JWT whitelist expires sessions the way
  production does, rather than the in-memory shortcut that hides it.
- No external OAuth / Schulportal login (excluded by request and not
  reproducible locally), no BigBlueButton; each divergence is marked at the
  line it affects. Everything binds to 127.0.0.1 and uses the upstream dev
  credentials, which are public.

Profiles keep the heavy pieces opt-in: `tools` adds Etherpad/H5P/tldraw/
Collabora, `av` adds ClamAV, `preview` adds thumbnailing.

seed.sh loads the upstream demo school (the same call the deployment's init job
makes) and registers MinIO as the legacy storage provider, which has no seed
data on purpose. The demo data already contains the grading states that are
hard to obtain from the real account — a feedback-only grade and a 100% one —
which is what surfaced the past-due submitted-text scrape gap.

One config finding baked in: file-storage and h5p validate a token's
issuer/audience against JWT_DOMAIN (default "localhost"), while the API stamps
SC_DOMAIN; without keeping them equal, the homework page's file lookups 401.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-13 14:17:30 +02:00

268 lines
9.2 KiB
YAML

# A local Schulcloud, as close to schulcloud-thueringen.de as it can be made
# without its external identity provider.
#
# The application images are the *same* images the real instance runs
# (quay.io/schulcloudverbund, thr theme, tag 33.40 — see README.md), so the
# behaviour under test is the deployed behaviour, not a rebuild of main.
#
# docker compose up -d core: login, courses, boards, files
# docker compose --profile tools up -d + etherpad, h5p, tldraw, collabora
# docker compose --profile av up -d + virus scanning of uploads
#
# Everything here is a throwaway dev instance: the credentials are the upstream
# development defaults and are published in the upstream repositories. Do not
# expose any of it beyond localhost.
x-sc-version: &sc-version "${SC_VERSION:-33.40}"
x-server-image: &server-image
image: quay.io/schulcloudverbund/schulcloud-server:${SC_VERSION:-33.40}
env_file: [env/shared.env, env/jwt.env, env/api.env]
depends_on:
mongo: {condition: service_healthy}
valkey: {condition: service_started}
rabbitmq: {condition: service_healthy}
restart: unless-stopped
services:
# ---------------------------------------------------------------- infra ---
mongo:
image: docker.io/mongo:7
# Single-node replica set rather than a bare mongod: the server's migration
# runner opens transactions, which mongo refuses outside a replica set.
command: ["--replSet", "rs0", "--bind_ip_all"]
volumes:
- mongo-data:/data/db
ports: ["127.0.0.1:27019:27017"]
healthcheck:
# Initiates the replica set on first start and reports healthy once the
# node is actually primary, which is what every other service waits for.
test: >-
mongosh --quiet --eval '
try { rs.status() } catch (e) { rs.initiate({_id:"rs0",members:[{_id:0,host:"mongo:27017"}]}) }
quit(db.hello().isWritablePrimary ? 0 : 1)'
interval: 5s
timeout: 10s
retries: 30
start_period: 10s
restart: unless-stopped
valkey:
# The JWT whitelist. Sessions die when their key expires, exactly as in
# production — this is the piece that makes local session testing honest.
image: docker.io/valkey/valkey:8-alpine
ports: ["127.0.0.1:6381:6379"]
restart: unless-stopped
rabbitmq:
image: docker.io/rabbitmq:4-management-alpine
ports: ["127.0.0.1:15673:15672"]
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
interval: 10s
timeout: 10s
retries: 20
start_period: 20s
restart: unless-stopped
minio:
# Stands in for the S3 provider the real instance uses. Buckets are created
# by minio-init below.
image: quay.io/minio/minio:latest
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: miniouser
MINIO_ROOT_PASSWORD: miniouser
volumes:
- minio-data:/data
ports:
- "127.0.0.1:9900:9000" # S3 API
- "127.0.0.1:9901:9001" # console (miniouser / miniouser)
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 5s
timeout: 5s
retries: 30
restart: unless-stopped
minio-init:
image: quay.io/minio/mc:latest
depends_on:
minio: {condition: service_healthy}
entrypoint: ["/bin/sh", "/init.sh"]
volumes:
- ./scripts/minio-init.sh:/init.sh:ro
restart: "no"
# ------------------------------------------------------- schulcloud api ---
api:
<<: *server-image
container_name: sc-api
command: ["dist/apps/server/apps/server.app"]
ports: ["127.0.0.1:3030:3030"]
management:
# Not part of the running instance — it exposes the seeding and migration
# endpoints that the real deployment's init job calls, and nothing else.
<<: *server-image
command: ["dist/apps/server/apps/management.app"]
# Port and base path are hardcoded to 3333 and /api in management.app.ts;
# PORT is not read here.
ports: ["127.0.0.1:3333:3333"]
board-collaboration:
# The websocket behind column boards. Without it a board renders once and
# then never updates.
<<: *server-image
command: ["dist/apps/server/apps/board-collaboration.app"]
environment:
PORT: "4450"
admin-api:
<<: *server-image
command: ["dist/apps/server/apps/admin-api-server.app"]
environment:
PORT: "4030"
file-storage:
image: quay.io/schulcloudverbund/file-storage:${SC_VERSION:-33.40}
env_file: [env/shared.env, env/jwt.env, env/file-storage.env]
depends_on:
mongo: {condition: service_healthy}
rabbitmq: {condition: service_healthy}
minio: {condition: service_healthy}
ports: ["127.0.0.1:4444:4444"]
restart: unless-stopped
file-preview:
# Generates thumbnails via ImageMagick, driven off RabbitMQ. Optional: with
# it absent, files still upload and download, they just have no preview.
image: quay.io/schulcloudverbund/file-storage:file-preview-${SC_VERSION:-33.40}
profiles: ["preview"]
env_file: [env/shared.env, env/jwt.env, env/file-storage.env]
depends_on:
rabbitmq: {condition: service_healthy}
minio: {condition: service_healthy}
restart: unless-stopped
# -------------------------------------------------------------- clients ---
client:
# The legacy UI. Still owns "/" and much of the course view.
image: quay.io/schulcloudverbund/schulcloud-client-thr:${SC_VERSION:-33.40}
env_file: [env/shared.env, env/jwt.env, env/client.env]
depends_on: [api]
ports: ["127.0.0.1:3100:3100"]
restart: unless-stopped
nuxt:
# The Vue SPA, built for the thr theme. The image is an nginx that
# templates env vars into its config at start.
image: quay.io/schulcloudverbund/schulcloud-frontend-thr:${SC_VERSION:-33.40}
env_file: [env/nuxt.env]
ports: ["127.0.0.1:4000:4000"]
restart: unless-stopped
proxy:
# The single origin. Everything a browser touches goes through here, so the
# app sees one host the way it does in production.
image: docker.io/nginx:1.29-alpine
volumes:
- ./proxy/nginx.conf:/etc/nginx/conf.d/default.conf:ro
ports: ["127.0.0.1:4400:4400"]
depends_on: [api, client, nuxt]
restart: unless-stopped
# -------------------------------------------------- external tools -------
etherpad:
# The collaborative text editor element on column boards.
image: docker.io/etherpad/etherpad:3.3.3
profiles: ["tools"]
env_file: [env/etherpad.env]
volumes:
- ./etherpad/APIKEY.txt:/opt/etherpad-lite/APIKEY.txt:ro
depends_on:
mongo: {condition: service_healthy}
restart: unless-stopped
h5p-editor:
image: quay.io/schulcloudverbund/h5p-server:${SC_VERSION:-33.40}
profiles: ["tools"]
command: ["dist/apps/h5p-editor.app"]
env_file: [env/shared.env, env/jwt.env, env/h5p.env]
environment:
PORT: "4448"
depends_on:
mongo: {condition: service_healthy}
minio: {condition: service_healthy}
restart: unless-stopped
h5p-staticfiles:
image: quay.io/schulcloudverbund/h5p-server:static-files-${SC_VERSION:-33.40}
profiles: ["tools"]
restart: unless-stopped
h5p-libraries:
# One-shot: installs the H5P content types listed in env/h5p.env into the
# library bucket. Exits when done; re-run it after changing that list.
image: quay.io/schulcloudverbund/h5p-server:${SC_VERSION:-33.40}
profiles: ["tools"]
command: ["dist/apps/h5p-library-management.app"]
env_file: [env/shared.env, env/jwt.env, env/h5p.env]
depends_on:
mongo: {condition: service_healthy}
minio-init: {condition: service_completed_successfully}
restart: "no"
tldraw-server:
# The whiteboard element.
image: quay.io/schulcloudverbund/tldraw-server:${SC_VERSION:-33.40}
profiles: ["tools"]
command: ["dist/apps/tldraw-server.app.js"]
env_file: [env/shared.env, env/jwt.env, env/tldraw.env]
depends_on: [valkey, minio]
restart: unless-stopped
tldraw-worker:
image: quay.io/schulcloudverbund/tldraw-server:${SC_VERSION:-33.40}
profiles: ["tools"]
command: ["dist/apps/tldraw-worker.app.js"]
env_file: [env/shared.env, env/jwt.env, env/tldraw.env]
depends_on: [valkey, minio]
restart: unless-stopped
collabora:
# Office document editing. Reached by the browser directly on :9980, the
# way the real deployment puts it on its own hostname.
image: docker.io/collabora/code:latest
profiles: ["tools"]
environment:
extra_params: --o:ssl.enable=false --o:ssl.termination=false
domain: ".*"
aliasgroup1: "http://localhost:4400"
ports: ["127.0.0.1:9980:9980"]
cap_add: ["MKNOD"]
restart: unless-stopped
# ------------------------------------------------------------ antivirus ---
clamav:
# ~1.5 GB resident once the signature database loads, hence its own profile.
image: docker.io/clamav/clamav:1.5.3
profiles: ["av"]
volumes:
- clamav-db:/var/lib/clamav
restart: unless-stopped
clammit:
image: ghcr.io/dbildungsplattform/clammit:0.9.1
profiles: ["av"]
environment:
CLAMMIT_CLAMD_URL: tcp://clamav:3310
CLAMMIT_LISTEN: 0.0.0.0:8438
depends_on: [clamav]
restart: unless-stopped
volumes:
mongo-data:
minio-data:
clamav-db: