Files
member-console/test/compose.yaml
T
cgalo5758 d45a51d2e8 Profile-gate FedWiki and Discourse in the test stack
Put the fedwiki chain (init, render, farm, caddy) behind a fedwiki
compose profile symmetric with discourse's; the default composition is
neither, selected via COMPOSE_PROFILES in test/.env, so a default stack
no longer binds host 443.

Guard every script and walkthrough on service presence: shared
skipUnlessIntegrationEndpointReachable helper, seed-stack presence
checks (also repairing its unsourced .env and container-native render
invocation), generic root-owned testdata reclaim in teardown, discourse
coverage in verify-stack-isolation, and fedwiki's 8090 base in the port
probe.

Update stack docs and finalize status bookkeeping for all three
changes; archives the test-stack-integration-profiles change.
2026-08-01 04:14:05 -05:00

439 lines
17 KiB
YAML

# 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://<site>.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