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.
439 lines
17 KiB
YAML
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
|