Complete sign-up pipeline: email registration via Logto Experience API, SMTP email verification, and self-service trial tenant creation. Layer 1 — Logto config: - Bootstrap Phase 8b: SMTP email connector with branded HTML templates - Bootstrap Phase 8c: enable SignInAndRegister (email+password sign-up) - Dockerfile installs official Logto connectors (ensures SMTP available) - SMTP env vars in docker-compose, installer templates, .env.example Layer 2 — Experience API (ui/sign-in/experience-api.ts): - Registration flow: initRegistration → sendVerificationCode → verifyCode → addProfile (password) → identifyUser → submit - Sign-in auto-detects email vs username identifier Layer 3 — Custom sign-in UI (ui/sign-in/SignInPage.tsx): - Three-mode state machine: signIn / register / verifyCode - Reads first_screen=register from URL query params - Toggle links between sign-in and register views Layer 4 — Post-registration onboarding: - OnboardingService: reuses VendorTenantService.createAndProvision(), adds calling user to Logto org as owner, enforces one trial per user - OnboardingController: POST /api/onboarding/tenant (authenticated only) - OnboardingPage.tsx: org name + auto-slug form - LandingRedirect: detects zero orgs → redirects to /onboarding - RegisterPage.tsx: /platform/register initiates OIDC with firstScreen Installers (install.sh + install.ps1): - Both prompt for SMTP config in SaaS mode - CLI args, env var capture, cameleer.conf persistence Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
8.6 KiB
Docker & Infrastructure
Routing (single-domain, path-based via Traefik)
All services on one hostname. Infrastructure containers (Traefik, Logto) use PUBLIC_HOST + PUBLIC_PROTOCOL env vars directly. The SaaS app reads these via CAMELEER_SAAS_PROVISIONING_PUBLICHOST / CAMELEER_SAAS_PROVISIONING_PUBLICPROTOCOL (Spring Boot properties cameleer.saas.provisioning.publichost / cameleer.saas.provisioning.publicprotocol).
| Path | Target | Notes |
|---|---|---|
/platform/* |
cameleer-saas:8080 | SPA + API (server.servlet.context-path: /platform) |
/platform/vendor/* |
(SPA routes) | Vendor console (platform:admin) |
/platform/tenant/* |
(SPA routes) | Tenant admin portal (org-scoped) |
/t/{slug}/* |
per-tenant server-ui | Provisioned tenant UI containers (Traefik labels) |
/ |
redirect -> /platform/ |
Via docker/traefik-dynamic.yml |
/* (catch-all) |
cameleer-logto:3001 (priority=1) | Custom sign-in UI, OIDC, interaction |
- SPA assets at
/_app/(ViteassetsDir: '_app') to avoid conflict with Logto's/assets/ - Logto
ENDPOINT=${PUBLIC_PROTOCOL}://${PUBLIC_HOST}(same domain, same origin) - TLS:
traefik-certsinit container generates self-signed cert (dev) or copies user-supplied cert viaCERT_FILE/KEY_FILE/CA_FILEenv vars. Default cert configured indocker/traefik-dynamic.yml(NOT statictraefik.yml— Traefik v3 ignorestls.stores.defaultin static config). Runtime cert replacement via vendor UI (stage/activate/restore). ACME for production (future). Server containers import/certs/ca.peminto JVM truststore at startup viadocker-entrypoint.shfor OIDC trust. - Root
/->/platform/redirect via Traefik file provider (docker/traefik-dynamic.yml) - LoginPage auto-redirects to Logto OIDC (no intermediate button)
- Per-tenant server containers get Traefik labels for
/t/{slug}/*routing at provisioning time
Docker Networks
Compose-defined networks:
| Network | Name on Host | Purpose |
|---|---|---|
cameleer |
cameleer-saas_cameleer |
Compose default — shared services (DB, Logto, SaaS) |
cameleer-traefik |
cameleer-traefik (fixed name:) |
Traefik + provisioned tenant containers |
Per-tenant networks (created dynamically by DockerTenantProvisioner):
| Network | Name Pattern | Purpose |
|---|---|---|
| Tenant network | cameleer-tenant-{slug} |
Internal bridge, no internet — isolates tenant server + apps |
| Environment network | cameleer-env-{tenantId}-{envSlug} |
Tenant-scoped (includes tenantId to prevent slug collision across tenants) |
Server containers join three networks: tenant network (primary), shared services network (cameleer), and traefik network. Apps deployed by the server use the tenant network as primary.
Backend IP resolution: Traefik's Docker provider is configured with network: cameleer-traefik (static traefik.yml). Every cameleer-managed container — saas-provisioned tenant containers (via DockerTenantProvisioner) and cameleer-server's per-app containers (via DockerNetworkManager) — is attached to cameleer-traefik at creation, so Traefik always resolves a reachable backend IP. Provisioned tenant containers additionally emit a traefik.docker.network=cameleer-traefik label as per-service defense-in-depth. (Pre-2026-04-23 the static config pointed at network: cameleer, a name that never matched any real network — that produced 504 Gateway Timeout on every managed app until the Traefik image was rebuilt.)
Custom sign-in UI (ui/sign-in/)
Separate Vite+React SPA replacing Logto's default sign-in page. Supports both sign-in and self-service registration.
- Built as custom Logto Docker image (
cameleer-logto):ui/sign-in/Dockerfile= node build stage +FROM ghcr.io/logto-io/logto:latest+ install official connectors (SMTP) + COPY dist over/etc/logto/packages/experience/dist/ - Uses
@cameleer/design-systemcomponents (Card, Input, Button, FormField, Alert) - Sign-in: Logto Experience API (4-step: init -> verify password -> identify -> submit -> redirect). Auto-detects email vs username identifier.
- Registration: 2-phase flow. Phase 1: init Register -> send verification code to email. Phase 2: verify code -> set password -> identify (creates user) -> submit -> redirect.
- Reads
first_screen=registerfrom URL query params to show register form initially (set by@logto/reactSDK'sfirstScreenoption) CUSTOM_UI_PATHenv var does NOT work for Logto OSS — must volume-mount or replace the experience dist directory- Favicon bundled in
ui/sign-in/public/favicon.svg(served by Logto, not SaaS)
Deployment pipeline
App deployment is handled by the cameleer-server's DeploymentExecutor (7-stage async flow):
- PRE_FLIGHT — validate config, check JAR exists
- PULL_IMAGE — pull base image if missing
- CREATE_NETWORK — ensure cameleer-traefik and cameleer-env-{slug} networks
- START_REPLICAS — create N containers with Traefik labels
- HEALTH_CHECK — poll
/cameleer/healthon agent port 9464 - SWAP_TRAFFIC — stop old deployment (blue/green)
- COMPLETE — mark RUNNING or DEGRADED
Key files:
DeploymentExecutor.java(in cameleer-server) — async staged deployment, runtime type auto-detectionDockerRuntimeOrchestrator.java(in cameleer-server) — Docker client, container lifecycle, builds runtime-type-specific entrypoints (spring-boot uses-cp+PropertiesLauncherwith-Dloader.pathfor log appender; quarkus uses-jar; plain-java uses-cp+ detected main class; native exec directly). Overrides the Dockerfile ENTRYPOINT.docker/runtime-base/Dockerfile— base image with agent JAR +cameleer-log-appender.jar+ JRE. The Dockerfile ENTRYPOINT (-jar /app/app.jar) is a fallback —DockerRuntimeOrchestratoroverrides it at container creation.RuntimeDetector.java(in cameleer-server) — detects runtime type from JAR manifestMain-Class; derives correctPropertiesLauncherpackage (Spring Boot 3.2+ vs pre-3.2)ServerApiClient.java— M2M token acquisition for SaaS->server API calls (agent status). UsesX-Cameleer-Protocol-Version: 1header- Docker socket access:
group_add: ["0"]in docker-compose.dev.yml (not root group membership in Dockerfile) - Network: deployed containers join
cameleer-tenant-{slug}(primary, isolation) +cameleer-traefik(routing) +cameleer-env-{tenantId}-{envSlug}(environment isolation)
Bootstrap (docker/logto-bootstrap.sh)
Idempotent script run inside the Logto container entrypoint. Clean slate — no example tenant, no viewer user, no server configuration. Phases:
- Wait for Logto health (no server to wait for — servers are provisioned per-tenant)
- Get Management API token (reads
m-defaultsecret from DB) - Create Logto apps (SPA, Traditional Web App with
skipConsent, M2M with Management API role + server API role) 3b. Create API resource scopes (1 platform + 9 tenant + 3 server scopes) - Create org roles (owner, operator, viewer with API resource scope assignments) + M2M server role (
cameleer-m2m-serverwithserver:adminscope) - Create admin user (SaaS admin with Logto console access)
7b. Configure Logto Custom JWT for access tokens (maps org roles ->
rolesclaim: owner->server:admin, operator->server:operator, viewer->server:viewer; saas-vendor global role -> server:admin) - Configure Logto sign-in branding (Cameleer colors
#C6820E/#D4941E, logo from/platform/logo.svg) 8b. Configure SMTP email connector (ifSMTP_HOST/SMTP_USERenv vars set) — discovers factory via/api/connector-factories, creates connector with Cameleer-branded HTML email templates for Register/SignIn/ForgotPassword/Generic. Skips gracefully if SMTP not configured. 8c. Enable self-service registration — setssignInMode: "SignInAndRegister",signUp: { identifiers: ["email"], password: true, verify: true }, sign-in methods: email+password and username+password (backwards-compatible with admin user). - Cleanup seeded Logto apps
- Write bootstrap results to
/data/logto-bootstrap.json - Create
saas-vendorglobal role with all API scopes and assign to admin user (always runs — admin IS the platform admin).
SMTP env vars for email verification: SMTP_HOST, SMTP_PORT (default 587), SMTP_USER, SMTP_PASS, SMTP_FROM_EMAIL (default noreply@cameleer.io). Passed to cameleer-logto container via docker-compose. Both installers prompt for these in SaaS mode.
The multi-tenant compose stack is: Traefik + PostgreSQL + ClickHouse + Logto (with bootstrap entrypoint) + cameleer-saas. No cameleer-server or cameleer-server-ui in compose — those are provisioned per-tenant by DockerTenantProvisioner.