Files
cameleer-saas/docker-compose.yml
hsiegeln df220bc5f3
All checks were successful
CI / build (push) Successful in 40s
CI / docker (push) Successful in 50s
feat: custom Logto sign-in UI with Cameleer branding
Replace Logto's default sign-in page with a custom React SPA that
matches the cameleer3-server login page using @cameleer/design-system.

- New Vite+React app at ui/sign-in/ with Experience API integration
- 4-step auth flow: init → verify password → identify → submit
- Design-system components: Card, Input, Button, FormField, Alert
- Same witty random subtitles as cameleer3-server LoginPage
- Dockerfile: add sign-in-frontend build stage, copy dist to image
- docker-compose: CUSTOM_UI_PATH on Logto, shared signinui volume
- SaaS entrypoint copies sign-in dist to shared volume on startup
- Add .gitattributes for LF line endings

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 11:43:22 +02:00

246 lines
9.0 KiB
YAML

services:
traefik-certs:
image: alpine:latest
restart: "no"
entrypoint: ["sh", "-c"]
command:
- |
if [ ! -f /certs/cert.pem ]; then
apk add --no-cache openssl >/dev/null 2>&1
openssl req -x509 -newkey rsa:4096 \
-keyout /certs/key.pem -out /certs/cert.pem \
-days 365 -nodes \
-subj "/CN=$$PUBLIC_HOST" \
-addext "subjectAltName=DNS:$$PUBLIC_HOST,DNS:*.$$PUBLIC_HOST"
echo "Generated self-signed cert for $$PUBLIC_HOST"
else
echo "Certs already exist, skipping"
fi
environment:
PUBLIC_HOST: ${PUBLIC_HOST:-localhost}
volumes:
- certs:/certs
traefik:
image: traefik:v3
restart: unless-stopped
depends_on:
traefik-certs:
condition: service_completed_successfully
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik.yml:/etc/traefik/traefik.yml:ro
- ./docker/traefik-dynamic.yml:/etc/traefik/dynamic.yml:ro
- acme:/etc/traefik/acme
- certs:/etc/traefik/certs:ro
networks:
- cameleer
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB:-cameleer_saas}
POSTGRES_USER: ${POSTGRES_USER:-cameleer}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-cameleer_dev}
volumes:
- pgdata:/var/lib/postgresql/data
- ./docker/init-databases.sh:/docker-entrypoint-initdb.d/init-databases.sh:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-cameleer} -d ${POSTGRES_DB:-cameleer_saas}"]
interval: 5s
timeout: 5s
retries: 5
networks:
- cameleer
logto:
image: ghcr.io/logto-io/logto:latest
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
entrypoint: ["sh", "-c", "npm run cli db seed -- --swe && npm start"]
volumes:
- signinui:/etc/logto/custom-ui
environment:
DB_URL: postgres://${POSTGRES_USER:-cameleer}:${POSTGRES_PASSWORD:-cameleer_dev}@postgres:5432/logto
ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}
ADMIN_ENDPOINT: http://${PUBLIC_HOST:-localhost}:3002
TRUST_PROXY_HEADER: 1
CUSTOM_UI_PATH: /etc/logto/custom-ui
healthcheck:
test: ["CMD-SHELL", "node -e \"require('http').get('http://localhost:3001/oidc/.well-known/openid-configuration', r => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))\""]
interval: 5s
timeout: 5s
retries: 30
start_period: 15s
labels:
- traefik.enable=true
- traefik.http.routers.logto.rule=PathPrefix(`/`)
- traefik.http.routers.logto.priority=1
- traefik.http.routers.logto.entrypoints=websecure
- traefik.http.routers.logto.tls=true
- traefik.http.services.logto.loadbalancer.server.port=3001
networks:
- cameleer
logto-bootstrap:
image: postgres:16-alpine
depends_on:
logto:
condition: service_healthy
cameleer3-server:
condition: service_healthy
restart: "no"
entrypoint: ["sh", "/scripts/logto-bootstrap.sh"]
environment:
LOGTO_ENDPOINT: http://logto:3001
LOGTO_ADMIN_ENDPOINT: http://logto:3002
LOGTO_PUBLIC_ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}
PUBLIC_HOST: ${PUBLIC_HOST:-localhost}
PUBLIC_PROTOCOL: ${PUBLIC_PROTOCOL:-https}
PG_HOST: postgres
PG_USER: ${POSTGRES_USER:-cameleer}
PG_PASSWORD: ${POSTGRES_PASSWORD:-cameleer_dev}
PG_DB_SAAS: ${POSTGRES_DB:-cameleer_saas}
SAAS_ADMIN_USER: ${SAAS_ADMIN_USER:-admin}
SAAS_ADMIN_PASS: ${SAAS_ADMIN_PASS:-admin}
TENANT_ADMIN_USER: ${TENANT_ADMIN_USER:-camel}
TENANT_ADMIN_PASS: ${TENANT_ADMIN_PASS:-camel}
CAMELEER_AUTH_TOKEN: ${CAMELEER_AUTH_TOKEN:-default-bootstrap-token}
SERVER_ENDPOINT: http://cameleer3-server:8081
SERVER_UI_USER: ${CAMELEER_UI_USER:-admin}
SERVER_UI_PASS: ${CAMELEER_UI_PASSWORD:-admin}
volumes:
- ./docker/logto-bootstrap.sh:/scripts/logto-bootstrap.sh:ro
- bootstrapdata:/data
networks:
- cameleer
cameleer-saas:
image: ${CAMELEER_IMAGE:-gitea.siegeln.net/cameleer/cameleer-saas}:${VERSION:-latest}
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
logto-bootstrap:
condition: service_completed_successfully
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- jardata:/data/jars
- bootstrapdata:/data/bootstrap:ro
- signinui:/data/sign-in-ui
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/${POSTGRES_DB:-cameleer_saas}
SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER:-cameleer}
SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD:-cameleer_dev}
LOGTO_ENDPOINT: ${LOGTO_ENDPOINT:-http://logto:3001}
LOGTO_PUBLIC_ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}
LOGTO_ISSUER_URI: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}/oidc
LOGTO_JWK_SET_URI: ${LOGTO_ENDPOINT:-http://logto:3001}/oidc/jwks
LOGTO_M2M_CLIENT_ID: ${LOGTO_M2M_CLIENT_ID:-}
LOGTO_M2M_CLIENT_SECRET: ${LOGTO_M2M_CLIENT_SECRET:-}
CAMELEER3_SERVER_ENDPOINT: http://cameleer3-server:8081
CLICKHOUSE_URL: jdbc:clickhouse://clickhouse:8123/cameleer
labels:
- traefik.enable=true
- traefik.http.routers.saas.rule=PathPrefix(`/platform`)
- traefik.http.routers.saas.entrypoints=websecure
- traefik.http.routers.saas.tls=true
- traefik.http.services.saas.loadbalancer.server.port=8080
networks:
- cameleer
cameleer3-server:
image: ${CAMELEER3_SERVER_IMAGE:-gitea.siegeln.net/cameleer/cameleer3-server}:${VERSION:-latest}
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
clickhouse:
condition: service_started
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/cameleer3
SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER:-cameleer}
SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD:-cameleer_dev}
CLICKHOUSE_URL: jdbc:clickhouse://clickhouse:8123/cameleer
CAMELEER_AUTH_TOKEN: ${CAMELEER_AUTH_TOKEN:-default-bootstrap-token}
CAMELEER_JWT_SECRET: ${CAMELEER_JWT_SECRET:-cameleer-dev-jwt-secret-change-in-production}
CAMELEER_TENANT_ID: ${CAMELEER_TENANT_SLUG:-default}
CAMELEER_OIDC_ISSUER_URI: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}/oidc
CAMELEER_OIDC_JWK_SET_URI: ${LOGTO_ENDPOINT:-http://logto:3001}/oidc/jwks
CAMELEER_OIDC_TLS_SKIP_VERIFY: "true"
CAMELEER_OIDC_AUDIENCE: ${CAMELEER_OIDC_AUDIENCE:-https://api.cameleer.local}
CAMELEER_CORS_ALLOWED_ORIGINS: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}
healthcheck:
test: ["CMD-SHELL", "curl -sf http://localhost:8081/api/v1/health || exit 1"]
interval: 5s
timeout: 5s
retries: 30
start_period: 15s
labels:
- traefik.enable=true
- traefik.http.routers.observe.rule=PathPrefix(`/observe`)
- traefik.http.routers.observe.service=observe
- traefik.http.services.observe.loadbalancer.server.port=8080
- traefik.http.routers.dashboard.rule=PathPrefix(`/dashboard`)
- traefik.http.routers.dashboard.service=dashboard
- traefik.http.routers.dashboard.middlewares=dashboard-strip
- traefik.http.middlewares.dashboard-strip.stripprefix.prefixes=/dashboard
- traefik.http.services.dashboard.loadbalancer.server.port=8080
networks:
- cameleer
cameleer3-server-ui:
image: ${CAMELEER3_SERVER_UI_IMAGE:-gitea.siegeln.net/cameleer/cameleer3-server-ui}:${VERSION:-latest}
restart: unless-stopped
depends_on:
cameleer3-server:
condition: service_healthy
environment:
CAMELEER_API_URL: http://cameleer3-server:8081
BASE_PATH: /server
labels:
- traefik.enable=true
- traefik.http.routers.server-ui.rule=PathPrefix(`/server`)
- traefik.http.routers.server-ui.entrypoints=websecure
- traefik.http.routers.server-ui.tls=true
- traefik.http.routers.server-ui.middlewares=server-ui-strip
- traefik.http.middlewares.server-ui-strip.stripprefix.prefixes=/server
- traefik.http.routers.server-ui.service=server-ui
- traefik.http.services.server-ui.loadbalancer.server.port=80
networks:
- cameleer
clickhouse:
image: clickhouse/clickhouse-server:latest
restart: unless-stopped
volumes:
- chdata:/var/lib/clickhouse
- ./docker/clickhouse-init.sql:/docker-entrypoint-initdb.d/init.sql:ro
- ./docker/clickhouse-users.xml:/etc/clickhouse-server/users.d/default-user.xml:ro
healthcheck:
test: ["CMD-SHELL", "clickhouse-client --query 'SELECT 1'"]
interval: 10s
timeout: 5s
retries: 3
networks:
- cameleer
networks:
cameleer:
driver: bridge
volumes:
pgdata:
chdata:
acme:
certs:
jardata:
bootstrapdata:
signinui: