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>
268 lines
9.2 KiB
YAML
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:
|