# 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/` (Vite `assetsDir: '_app'`) to avoid conflict with Logto's `/assets/` - Logto `ENDPOINT` = `${PUBLIC_PROTOCOL}://${PUBLIC_HOST}` (same domain, same origin) - TLS: `traefik-certs` init container generates self-signed cert (dev) or copies user-supplied cert via `CERT_FILE`/`KEY_FILE`/`CA_FILE` env vars. Default cert configured in `docker/traefik-dynamic.yml` (NOT static `traefik.yml` — Traefik v3 ignores `tls.stores.default` in static config). Runtime cert replacement via vendor UI (stage/activate/restore). ACME for production (future). Server containers import `/certs/ca.pem` into JVM truststore at startup via `docker-entrypoint.sh` for 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-system` components (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=register` from URL query params to show register form initially (set by `@logto/react` SDK's `firstScreen` option) - `CUSTOM_UI_PATH` env 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): 1. PRE_FLIGHT — validate config, check JAR exists 2. PULL_IMAGE — pull base image if missing 3. CREATE_NETWORK — ensure cameleer-traefik and cameleer-env-{slug} networks 4. START_REPLICAS — create N containers with Traefik labels 5. HEALTH_CHECK — poll `/cameleer/health` on agent port 9464 6. SWAP_TRAFFIC — stop old deployment (blue/green) 7. COMPLETE — mark RUNNING or DEGRADED Key files: - `DeploymentExecutor.java` (in cameleer-server) — async staged deployment, runtime type auto-detection - `DockerRuntimeOrchestrator.java` (in cameleer-server) — Docker client, container lifecycle, builds runtime-type-specific entrypoints (spring-boot uses `-cp` + `PropertiesLauncher` with `-Dloader.path` for 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 — `DockerRuntimeOrchestrator` overrides it at container creation. - `RuntimeDetector.java` (in cameleer-server) — detects runtime type from JAR manifest `Main-Class`; derives correct `PropertiesLauncher` package (Spring Boot 3.2+ vs pre-3.2) - `ServerApiClient.java` — M2M token acquisition for SaaS->server API calls (agent status). Uses `X-Cameleer-Protocol-Version: 1` header - 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: 1. Wait for Logto health (no server to wait for — servers are provisioned per-tenant) 2. Get Management API token (reads `m-default` secret from DB) 3. 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) 4. Create org roles (owner, operator, viewer with API resource scope assignments) + M2M server role (`cameleer-m2m-server` with `server:admin` scope) 5. Create admin user (SaaS admin with Logto console access) 7b. Configure Logto Custom JWT for access tokens (maps org roles -> `roles` claim: owner->server:admin, operator->server:operator, viewer->server:viewer; saas-vendor global role -> server:admin) 8. Configure Logto sign-in branding (Cameleer colors `#C6820E`/`#D4941E`, logo from `/platform/logo.svg`) 8b. Configure SMTP email connector (if `SMTP_HOST`/`SMTP_USER` env 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 — sets `signInMode: "SignInAndRegister"`, `signUp: { identifiers: ["email"], password: true, verify: true }`, sign-in methods: email+password and username+password (backwards-compatible with admin user). 9. Cleanup seeded Logto apps 10. Write bootstrap results to `/data/logto-bootstrap.json` 12. Create `saas-vendor` global 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`.