Pre-fix the paragraph claimed every dynamically-created container MUST
carry `traefik.docker.network=cameleer-traefik` to avoid a 504, because
Traefik's Docker provider pointed at `network: cameleer` (a literal
name that never matched any real network). After the one-line static
config fix (df64573), Traefik's provider targets `cameleer-traefik`
directly — the network every managed container already joins — so the
per-container label is just defense-in-depth, not required.
Rewritten to describe current behaviour and keep a short note about the
pre-fix 504 for operators who roll back to an old image.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7.5 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. Visually matches cameleer-server LoginPage.
- Built as custom Logto Docker image (
cameleer-logto):ui/sign-in/Dockerfile= node build stage +FROM ghcr.io/logto-io/logto:latest+ COPY dist over/etc/logto/packages/experience/dist/ - Uses
@cameleer/design-systemcomponents (Card, Input, Button, FormField, Alert) - Authenticates via Logto Experience API (4-step: init -> verify password -> identify -> submit -> redirect)
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) - 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).
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.