# 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] 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: