# Docker Compose file for testing Keycloak, Temporal, and Fedwiki integration # Remember, this is for testing purposes only and not for production use services: # Session Store valkey: image: valkey/valkey:8.1 ports: - "${VALKEY_PORT:-6379}:6379" # Member Console DB postgres: image: postgres:18.1 environment: - POSTGRES_USER=member_console - POSTGRES_PASSWORD=member_console - POSTGRES_DB=member_console volumes: - ./testdata/postgres:/var/lib/postgresql ports: - "${POSTGRES_PORT:-5432}:5432" # Identity Provider keycloak: image: quay.io/keycloak/keycloak:26.4.7 command: start-dev environment: - KC_BOOTSTRAP_ADMIN_USERNAME=admin - KC_BOOTSTRAP_ADMIN_PASSWORD=admin - KC_HEALTH_ENABLED=true - KC_HOSTNAME=${KC_HOSTNAME:-keycloak.localhost} - KC_HOSTNAME_STRICT=false healthcheck: test: - "CMD-SHELL" - | exec 3<>/dev/tcp/127.0.0.1/9000; echo -e "GET /health/ready HTTP/1.1\r\nHost: localhost:9000\r\nConnection: close\r\n\r\n" >&3; if cat <&3 | grep -q "\"status\": \"UP\""; then exit 0 else exit 1 fi interval: 10s timeout: 5s retries: 5 start_period: 30s networks: default: aliases: - ${KC_HOSTNAME:-keycloak.localhost} ports: - "${KEYCLOAK_PORT:-8080}:8080" # Seed Keycloak with clients, roles, and test users keycloak-seed: image: alpine/curl:latest depends_on: keycloak: condition: service_healthy volumes: - ./seed/keycloak/seed-keycloak.sh:/seed/seed-keycloak.sh:ro environment: - KC_URL=http://keycloak:8080 - KC_ADMIN_USER=admin - KC_ADMIN_PASSWORD=admin - MC_BASE_URL=${MC_BASE_URL:-http://localhost:8081} - TEMPORAL_UI_URL=${TEMPORAL_UI_URL:-http://localhost:8233} # Host-facing forum URL for the "discourse" OIDC client's redirect # URIs. Seeded unconditionally (the client is inert when the # profile-gated forum isn't running). - DISCOURSE_URL=http://${DISCOURSE_HOSTNAME:-discourse.localhost}:${DISCOURSE_PORT:-9292} entrypoint: [ "/bin/sh", "-c", "apk add --no-cache jq bash >/dev/null 2>&1 && bash /seed/seed-keycloak.sh", ] networks: default: # Temporal temporal-db: image: postgres:18.1 environment: - POSTGRES_USER=temporal - POSTGRES_PASSWORD=temporal - POSTGRES_DB=temporal volumes: - ./testdata/temporal:/var/lib/postgresql healthcheck: test: ["CMD-SHELL", "pg_isready -U temporal -d temporal"] interval: 5s timeout: 5s retries: 10 start_period: 10s temporal: image: temporalio/auto-setup:1.29.1 # temporal-db must be *accepting connections* (not merely started) before # auto-setup runs schema setup, or the container fails and exits; and keycloak # must be serving before temporal's first JWKS fetch, or the token key cache # starts empty. Short-form `depends_on` waits only for container start, so # gate on service health for both. depends_on: temporal-db: condition: service_healthy keycloak: condition: service_healthy command: "autosetup" environment: - SERVICES=frontend:history:matching:worker:internal-frontend - DB=postgres12 - DB_PORT=5432 - POSTGRES_USER=temporal - POSTGRES_PWD=temporal - POSTGRES_SEEDS=temporal-db - SKIP_DEFAULT_NAMESPACE_CREATION=true - TEMPORAL_AUTH_AUTHORIZER=default - TEMPORAL_AUTH_CLAIM_MAPPER=default - TEMPORAL_JWT_KEY_SOURCE1=http://${KC_HOSTNAME:-keycloak.localhost}:8080/realms/test/protocol/openid-connect/certs - USE_INTERNAL_FRONTEND=true ports: - "${TEMPORAL_PORT:-7233}:7233" temporal-admin-tools: image: temporalio/admin-tools:1.29 depends_on: - temporal environment: - TEMPORAL_ADDRESS=temporal:7236 - TEMPORAL_CLI_ADDRESS=temporal:7236 # Registers the "default" namespace via the internal frontend (port 7236 # bypasses JWT auth). Idempotent: a "namespace already exists" failure is # treated as success. Runs once on `up` and exits. temporal-seed: image: temporalio/admin-tools:1.29 depends_on: - temporal environment: - TEMPORAL_ADDRESS=temporal:7236 - TEMPORAL_CLI_ADDRESS=temporal:7236 restart: "no" entrypoint: - /bin/sh - -c - | set -eu for i in $$(seq 1 60); do if temporal operator namespace describe -n default >/dev/null 2>&1; then echo "namespace 'default' already exists"; exit 0 fi out=$$(temporal operator namespace create -n default --retention 24h 2>&1) && rc=0 || rc=$$? echo "$$out" if [ "$$rc" -eq 0 ]; then echo "namespace 'default' created"; exit 0 fi if echo "$$out" | grep -q "already exists"; then echo "namespace 'default' already exists"; exit 0 fi echo "waiting for temporal frontend... ($$i/60)"; sleep 2 done echo "ERROR: temporal-seed gave up" >&2; exit 1 temporal-ui: image: temporalio/ui:2.41.0 depends_on: temporal: condition: service_started keycloak: condition: service_healthy # Wait for the seed to create the app realm + temporal-ui client — Temporal UI # does OIDC discovery on /realms/test at startup and exits if it 404s. keycloak-seed: condition: service_completed_successfully environment: - TEMPORAL_ADDRESS=temporal:7233 - TEMPORAL_UI_PORT=8233 - TEMPORAL_CORS_ORIGINS=${TEMPORAL_UI_URL:-http://localhost:8233} - TEMPORAL_AUTH_ENABLED=true - TEMPORAL_AUTH_PROVIDER_URL=http://${KC_HOSTNAME:-keycloak.localhost}:${KEYCLOAK_PORT:-8080}/realms/test - TEMPORAL_AUTH_ISSUER_URL=http://${KC_HOSTNAME:-keycloak.localhost}:${KEYCLOAK_PORT:-8080}/realms/test - TEMPORAL_AUTH_CLIENT_ID=temporal-ui - TEMPORAL_AUTH_CLIENT_SECRET=TEST-ONLY-temporal-client-secret - TEMPORAL_AUTH_CALLBACK_URL=${TEMPORAL_UI_URL:-http://localhost:8233}/auth/sso/callback - TEMPORAL_AUTH_SCOPES=openid,profile,email - LOG_LEVEL=debug # Temporal UI's OIDC login fetches discovery from inside this container and the # browser then follows the same endpoints, so both must reach Keycloak at the # exact host-facing URL (${KC_HOSTNAME}:${KEYCLOAK_PORT}) — otherwise the token # issuer won't match. host-gateway resolves that hostname to the host, where the # port is published; without it the compose network alias resolves the name to # Keycloak's container, whose published port is not reachable in-network. extra_hosts: - "${KC_HOSTNAME:-keycloak.localhost}:host-gateway" ports: - "${TEMPORAL_UI_PORT:-8233}:8233" # --------------------------------------------------------------------------- # FedWiki farm — OPT-IN via the `fedwiki` compose profile. Gated for the same # reason Discourse is (below): the stack must compose cleanly for a deployment # that runs no wiki farm, and the app has booted FedWiki-less since # integration-config-parity made farm-api-url/admin-token a required-together # group that is valid when empty. The whole chain carries the profile — # fedwiki-init, fedwiki-render, fedwiki, and the caddy TLS proxy, which exists # solely to front this farm and nothing else depends on. # # Select it through the COMPOSE_PROFILES knob in test/.env (bootstrap writes # it), not with a per-command --profile flag: seed-stack.sh and # teardown-stack.sh resolve the same composition from the environment. fedwiki-init: profiles: [fedwiki] image: busybox # Copy seed tree, then strip render.sh and *.tpl files — those are # rendered into /data by the fedwiki-render service after KC seed. # chown to uid 1000 so the fedwiki container's `node` user can mkdir # site subdirs (pages/, recycle/, ...) at runtime. command: sh -c 'cp -rn /seed/. /data/ && find /data -name "*.tpl" -delete && rm -f /data/render.sh && chown -R 1000:1000 /data' volumes: - ./seed/fedwiki:/seed:ro - ./testdata/fedwiki:/data # Resolves real Keycloak UUIDs and templates seed/fedwiki/*.tpl into # testdata/fedwiki/. Must run after keycloak-seed completes (users # exist) and after fedwiki-init completes (directory structure in # place). Writes files as root; fedwiki-init's chown -R 1000:1000 # runs first, but any files this service writes will be root-owned. # The fedwiki node user (uid 1000) needs read-only access to these, # which 644/755 root-owned files satisfy. fedwiki-render: profiles: [fedwiki] image: alpine/curl:latest depends_on: keycloak-seed: condition: service_completed_successfully fedwiki-init: condition: service_completed_successfully volumes: - ./seed/fedwiki:/seed:ro - ./testdata/fedwiki:/data environment: - KC_URL=http://keycloak:8080 - KC_ADMIN_REALM=master - KC_REALM=test - KC_ADMIN_USER=admin - KC_ADMIN_PASSWORD=admin # Browser-facing Keycloak host/port — rendered into config.json's # oauth2_discoveryUrl so the OIDC endpoints match what the browser uses. - KC_HOSTNAME=${KC_HOSTNAME:-keycloak.localhost} - KEYCLOAK_PORT=${KEYCLOAK_PORT:-8080} entrypoint: [ "/bin/sh", "-c", "apk add --no-cache jq gettext >/dev/null 2>&1 && sh /seed/render.sh", ] networks: default: fedwiki: profiles: [fedwiki] image: git.coopcloud.tech/wiki-cafe/fedwiki-oci-image:0.41.0-rc.1-3 depends_on: fedwiki-init: condition: service_completed_successfully fedwiki-render: condition: service_completed_successfully # Port 80 keeps the host out of OAuth callback URLs, so wiki-security-social's # better-auth callback (http://.localtest.me/auth/oauth2/callback/oauth2) # has no port to mismatch. Composable still layers useraccesstokens on top of # the social auth_provider so the farmmanager admin API keeps its bearer-token path. command: wiki -p 80 --farm --security_type composable --auth_provider wiki-security-social --authz_enhancers wiki-plugin-useraccesstokens # better-auth fetches OIDC discovery from inside this container, and the # browser then follows the same endpoints — so fedwiki must reach Keycloak at # the exact host-facing URL the browser uses (${KC_HOSTNAME}:${KEYCLOAK_PORT}). # host-gateway resolves that hostname to the host, where the port is published. extra_hosts: - "${KC_HOSTNAME:-keycloak.localhost}:host-gateway" volumes: - ./testdata/fedwiki:/home/node/.wiki ports: - "${FEDWIKI_PORT:-80}:80" # TLS termination for the FedWiki farm. wiki-security-social (better-auth) sets # Secure cookies, which browsers only store over HTTPS — so browsers reach farm # sites through Caddy on 443. member-console keeps using fedwiki's plain-HTTP # admin API directly (bearer token, no cookies), so it needs no cert trust. # Caddy's internal CA signs the wildcard cert; trust the root for manual # browsing (see seed/caddy/Caddyfile). Host 443 ⇒ one HTTPS farm at a time — # but only among stacks that select the `fedwiki` profile; a core-only stack # binds nothing on 443. caddy: profiles: [fedwiki] image: caddy:2-alpine depends_on: - fedwiki ports: - "${FEDWIKI_HTTPS_PORT:-443}:443" volumes: - ./seed/caddy/Caddyfile:/etc/caddy/Caddyfile:ro - ./testdata/caddy:/data # --------------------------------------------------------------------------- # Live Discourse — OPT-IN via `docker compose --profile discourse up -d`. # Profile-gated because the image is ~1.5 GB and first boot (migrations) # takes minutes; the default stack stays fast. Mirrors the Co-op Cloud # recipe's topology (coop-cloud/discourse: official discourse/discourse # image + discourse/postgres + redis sidecars) so the test forum matches # what production deployments run. The in-process fake # (internal/integrations/discourse/discoursetest) remains the default for # unit/DB tests; this is for walkthroughs and live API verification. discourse-db: profiles: [discourse] # discourse/postgres = pgvector + Discourse's postgres management layer # (auto-pg_upgrade on version bumps). Same image the recipe pins. image: discourse/postgres:pg18 environment: # Internal-only network; trust keeps the seed and app connecting # without password juggling (recipe does the same). - POSTGRES_HOST_AUTH_METHOD=trust - POSTGRES_DB=discourse - DB_USER=discourse - DB_PASSWORD=discourse entrypoint: ["/bin/bash", "-c", "exec run-postgres.sh postgres"] volumes: # The image expects the whole cluster tree mounted here (not the data # subdir) — versioned PGDATA lives at /var/lib/postgresql/18/docker. - ./testdata/discourse/postgres:/var/lib/postgresql healthcheck: test: "pg_isready -U discourse -d discourse" interval: 5s timeout: 5s retries: 20 start_period: 30s discourse-redis: profiles: [discourse] image: redis:7.4-alpine healthcheck: test: "redis-cli ping | grep -q PONG" interval: 10s timeout: 5s retries: 5 start_period: 15s # One-shot: migrates the DB, then seeds an active+approved admin, mints a # master API key into ./testdata/discourse/api-key, enables the bundled # OIDC authenticator, and creates the non-automatic walkthrough group. # Runs BEFORE the app service so Discourse boots into a migrated DB. discourse-seed: profiles: [discourse] image: discourse/discourse:3.5.3 depends_on: discourse-db: condition: service_healthy discourse-redis: condition: service_healthy restart: "no" environment: - DISCOURSE_HOSTNAME=${DISCOURSE_HOSTNAME:-discourse.localhost} - DISCOURSE_DB_HOST=discourse-db - DISCOURSE_DB_PORT=5432 - DISCOURSE_DB_NAME=discourse - DISCOURSE_DB_USERNAME=discourse - DISCOURSE_DB_PASSWORD=discourse - DISCOURSE_REDIS_HOST=discourse-redis - DISCOURSE_REDIS_PORT=6379 # Webhook → the member-console app on the host (see the discourse # service's extra_hosts). Port must track the worktree's MC_PORT slot. - MC_WEBHOOK_URL=http://member-console.localhost:${MC_PORT:-8081}/webhooks/discourse - MC_WEBHOOK_SECRET=${MC_DISCOURSE_WEBHOOK_SECRET:-test-only-webhook-secret} # Browser SSO: OIDC discovery must use the host-facing Keycloak URL so # the endpoints it advertises are reachable from the user's browser; # the discourse service's extra_hosts makes the same URL resolvable # for server-side fetches. The published forum port feeds the hidden # `port` site setting so the OmniAuth callback URL is host-reachable. - KC_HOSTNAME=${KC_HOSTNAME:-keycloak.localhost} - KEYCLOAK_PORT=${KEYCLOAK_PORT:-8080} - DISCOURSE_SSO_PORT=${DISCOURSE_PORT:-9292} volumes: - ./seed/discourse/seed-forum.sh:/mc-seed/seed-forum.sh:ro - ./testdata/discourse:/mc-seed-out entrypoint: ["/bin/bash", "/mc-seed/seed-forum.sh"] discourse: profiles: [discourse] # Official web-only image (published since 2025-08; "experimental" per # Docker Hub, but it's what the coop-cloud recipe deploys). Tag matches # the recipe pin. CMD /sbin/boot brings up nginx+rails on port 80. image: discourse/discourse:3.5.3 depends_on: discourse-db: condition: service_healthy discourse-redis: condition: service_healthy discourse-seed: condition: service_completed_successfully environment: - DISCOURSE_HOSTNAME=${DISCOURSE_HOSTNAME:-discourse.localhost} - DISCOURSE_DEVELOPER_EMAILS=admin@example.com - DISCOURSE_DB_HOST=discourse-db - DISCOURSE_DB_PORT=5432 - DISCOURSE_DB_NAME=discourse - DISCOURSE_DB_USERNAME=discourse - DISCOURSE_DB_PASSWORD=discourse - DISCOURSE_REDIS_HOST=discourse-redis - DISCOURSE_REDIS_PORT=6379 volumes: - ./testdata/discourse/shared:/shared # No-op the image's SSL bootstrap: stock install-ssl reconfigures # nginx to `listen 443 ssl` against a cert that doesn't exist and # crash-loops; the test stack talks plain HTTP (recipe does the same # behind Traefik). - ./seed/discourse/install-ssl-noop.sh:/etc/runit/1.d/install-ssl:ro networks: default: aliases: - ${DISCOURSE_HOSTNAME:-discourse.localhost} # Webhook deliveries target the app running on the HOST under its # canonical hostname (anything else bounces off the app's redirect). # Keycloak likewise: the OIDC plugin fetches discovery/token endpoints # server-side at the same host-facing URL the browser uses (same # pattern as temporal-ui and fedwiki). extra_hosts: - "member-console.localhost:host-gateway" - "${KC_HOSTNAME:-keycloak.localhost}:host-gateway" ports: - "${DISCOURSE_PORT:-9292}:80" healthcheck: test: "curl -fsS http://localhost/srv/status || exit 1" interval: 15s timeout: 10s retries: 10 # First boot after migrations still does site-setting warmup; the # recipe allows 25m (covers upgrade migrations) — fresh test boots # are far faster but keep generous headroom. start_period: 10m