Files
Schulcloud-MCP/local-instance/docker-compose.yml
MechaCat02 5ae2210459 Close the gaps an audit of courses, tasks, files and grades turned up
Every area — courses, rooms, boards, topics, tasks, files, quizzes, teams,
groups, submissions, grades — was checked for data the instance has and the
tools did not show.

Grades and feedback. A teacher's /homework page is a different page from a
student's: grade and comment live in the grading form, one block per
submission, so a teacher account reported every graded submission as having
neither. parseTeacherGrading reads the form, and list_submissions can now
include the written feedback and who handed the work in.

Names. /api/v1 is partly served: courses, users and classes survive in the
deployment's ingress table, and users/{id} is the only route from an id to a
name. Submitters, file creators and course teachers resolve through it, and
degrade to "not visible to this account" where a student may not read them.

Courses, rooms and classes. get_course adds the description, teachers,
member count and weekly timetable from /api/v1/courses. list_classes is new.
get_room reports what the account may do — allowedOperations is an object of
booleans, not the list it was typed as — and applicants and invitation links
where it may manage them.

Board and topic content. Link descriptions, image alt text, drawing and
video-conference titles, the ids behind external tools and H5P content (the
only thing resembling a quiz), and what a deleted element used to be. Topic
Etherpad pads are read like board pads, and htmlToText keeps table columns
apart and drops template indentation.

Files. A scan with no text layer falls back to the preview endpoint, whose
width and outputFormat are undocumented enums, so Claude gets a picture of
the page; list_files reports counts and sizes. Teams stay documented as
unreadable at any API version; their files come later.

What the crawl missed. Tasks attached to topics (18 of 60 on the live
account), each course's own file area, and — behind INDEX_PERSONAL_FILES —
personal files and submissions with their grade comments, so search and
what_changed cover grading. A submission hit points at get_task.

The local instance's preview profile gets an ImageMagick policy that allows
the coders its 7.1.2 build needs; the image's own denies them all.

110 tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 20:19:16 +02:00

301 lines
11 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 + 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-storage-consumer:
# The AMQP half of files-storage, and a separate entrypoint from the HTTP
# one: only `files-storage-consumer.app` registers FilesStorageConsumer, so
# running the HTTP app alone leaves the `files-storage` exchange with no
# queue bound to it.
#
# The symptom is not a file problem. TaskService.delete awaits
# deleteFilesOfParent over AMQP before touching the task, so with nothing
# consuming, deleting a task or a topic hangs until the request timeout and
# answers 408 REQUEST_TIMEOUT with the entity still there. Copying a course
# goes the same way.
image: quay.io/schulcloudverbund/file-storage:${SC_VERSION:-33.40}
command: ["dist/apps/files-storage-consumer.app.js"]
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}
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]
volumes:
# The image's own ImageMagick policy denies every coder it needs; see the
# comment in the file. Without this the profile runs but produces nothing.
- ./file-preview/policy.xml:/etc/ImageMagick-7/policy.xml:ro
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
etherpad:
# The collaborative text editor element.
#
# Core, not a "tool", however much it looks like one: the legacy client asks
# the server for an Etherpad session on *every* topic page whose lesson has
# contents, without checking whether the lesson contains a pad at all
# (controllers/topics.js builds `etherpadPads` and then never reads it).
# Unreachable, that call fails, `validUntil` arrives undefined, and
# `new Date(undefined * 1000)` makes Express reject the session cookie —
# "option expires is invalid", a 500 on every topic page. The live
# deployment always runs Etherpad (ETHERPAD_REPLICAS: 1), so keeping it in
# the default profile is both the working and the faithful choice.
image: docker.io/etherpad/etherpad:3.3.3
env_file: [env/etherpad.env]
volumes:
- ./etherpad/APIKEY.txt:/opt/etherpad-lite/APIKEY.txt:ro
depends_on:
mongo: {condition: service_healthy}
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 -------
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: