# Cameleer SaaS Install Script Design ## Overview A professional installer for the Cameleer SaaS platform, distributed as two native scripts (`install.sh` for Linux, `install.ps1` for Windows). The installer downloads nothing — it embeds compose templates and generates all configuration from user input. All service initialization logic is baked into Docker images, configured via environment variables. Distribution model: `curl -sfL https://install.cameleer.io | bash` (Linux), `irm https://install.cameleer.io/windows | iex` (Windows). ## Platform Simplification (Prerequisites) The current architecture uses 7 services with 10+ bind-mounted config files. This design consolidates everything into 5 services with zero bind mounts (except Docker socket and optional user-supplied TLS certs). ### Image Consolidation | Image | Base | Bakes in | |---|---|---| | `cameleer-traefik` | `traefik:v3` | Static/dynamic Traefik config (uses Traefik env var substitution for dynamic values like ports), cert generation entrypoint (`openssl`), self-signed cert logic | | `cameleer-postgres` | `postgres:16-alpine` | `init-databases.sh` (creates `cameleer_saas`, `logto` databases) | | `cameleer-clickhouse` | `clickhouse/clickhouse-server` | Init SQL (`CREATE DATABASE cameleer`), `clickhouse-users.xml`, `clickhouse-config.xml` (Prometheus metrics) | | `cameleer-logto` | `ghcr.io/logto-io/logto` | Custom sign-in UI, bootstrap logic (app/user/role/scope creation), vendor seed (env-var gated). Replaces the separate `logto-bootstrap` init container. | | `cameleer-saas` | `eclipse-temurin:21-jre-alpine` | Spring Boot app + React SPA (already exists, no changes) | All images published to `gitea.siegeln.net/cameleer/`. ### Service Reduction | Before | After | |---|---| | traefik-certs (init container) | Merged into `cameleer-traefik` entrypoint | | traefik | `cameleer-traefik` | | postgres + bind-mounted init script | `cameleer-postgres` | | clickhouse + 3 bind-mounted config files | `cameleer-clickhouse` | | logto | `cameleer-logto` (with bootstrap) | | logto-bootstrap (init container) | Merged into `cameleer-logto` entrypoint | | cameleer-saas + bind-mounted UI | `cameleer-saas` | **Result: 7 services → 5 services. 10+ bind-mounted files → 0.** ### Bootstrap Merge The `logto-bootstrap` init container logic moves into `cameleer-logto`'s entrypoint as an idempotent startup step: 1. Logto starts and seeds its own database (`npm run cli db seed -- --swe`) 2. Entrypoint runs bootstrap logic (create apps, users, roles, scopes, branding) 3. Bootstrap checks for cached results in a Docker volume — skips if already done 4. Writes `logto-bootstrap.json` to shared volume 5. If `VENDOR_SEED_ENABLED=true`, creates vendor user and global role 6. Logto server starts normally The `cameleer-saas` service uses `depends_on: logto (healthy)` and reads bootstrap results from the shared volume on startup — same as today. ## Installer Architecture ### Distribution - Linux: `curl -sfL https://install.cameleer.io | bash` - Windows: `irm https://install.cameleer.io/windows | iex` The scripts are self-contained. They embed docker-compose templates and generate all files locally. No secondary downloads. ### Scripts - `install.sh` — Bash, targets Linux with Docker Engine - `install.ps1` — PowerShell, targets Windows with Docker Desktop (WSL2 backend) Both implement identical logic and produce identical output. They share a config file format (`cameleer.conf`) so configurations are portable between platforms. ### Prerequisites The installer checks (does not install) these prerequisites: - Docker Engine 24+ (Linux) or Docker Desktop 4.25+ (Windows) - Docker Compose v2 (`docker compose` subcommand) - `openssl` (Linux, for password generation) — PowerShell uses `[System.Security.Cryptography.RandomNumberGenerator]` - Ports 80, 443, 3002 are free (or custom ports if specified) - Docker socket accessible If any prerequisite is missing, the script prints a clear error message with a link to installation instructions and exits. ## Installation Modes ### Simple Mode (default) Asks 6 essential questions: 1. Install directory (default: `./cameleer`) 2. Public hostname (auto-detected, default: `localhost`) 3. Admin username (default: `admin`) 4. Admin password (default: auto-generated) 5. Use custom TLS certificates? (default: no → self-signed) - If yes: paths to cert.pem, key.pem, optional ca.pem 6. Connect to a monitoring network? (default: none) Everything else uses secure defaults. All passwords auto-generated. ### Expert Mode (`--expert` or chosen at interactive prompt) Adds these options, grouped by category: **Credentials:** - PostgreSQL password (default: generated) - ClickHouse password (default: generated) - Vendor account enable + username + password **Networking:** - HTTP port (default: 80) - HTTPS port (default: 443) - Logto admin console port (default: 3002) **Docker:** - Image version/tag (default: `latest`) - Compose project name (default: `cameleer-saas`) - Docker socket path (auto-detected) **TLS:** - CA bundle path - `NODE_TLS_REJECT_UNAUTHORIZED` setting **Logto:** - Admin console external exposure (default: yes) ### Silent Mode (`--silent`) No interactive prompts. Uses defaults plus overrides. **Config precedence:** CLI flags > environment variables > config file (`--config`) > defaults. ## Configuration Reference | Config key | CLI flag | Env var | Default | Simple | Expert | |---|---|---|---|---|---| | `install_dir` | `--install-dir` | `CAMELEER_INSTALL_DIR` | `./cameleer` | yes | yes | | `public_host` | `--public-host` | `PUBLIC_HOST` | auto-detect | yes | yes | | `public_protocol` | `--public-protocol` | `PUBLIC_PROTOCOL` | `https` | no | yes | | `admin_user` | `--admin-user` | `SAAS_ADMIN_USER` | `admin` | yes | yes | | `admin_password` | `--admin-password` | `SAAS_ADMIN_PASS` | generated | yes | yes | | `tls_mode` | `--tls-mode` | `TLS_MODE` | `self-signed` | yes | yes | | `cert_file` | `--cert-file` | `CERT_FILE` | none | yes* | yes | | `key_file` | `--key-file` | `KEY_FILE` | none | yes* | yes | | `ca_file` | `--ca-file` | `CA_FILE` | none | no | yes | | `monitoring_network` | `--monitoring-network` | `MONITORING_NETWORK` | none | yes | yes | | `postgres_password` | `--postgres-password` | `POSTGRES_PASSWORD` | generated | no | yes | | `clickhouse_password` | `--clickhouse-password` | `CLICKHOUSE_PASSWORD` | generated | no | yes | | `http_port` | `--http-port` | `HTTP_PORT` | `80` | no | yes | | `https_port` | `--https-port` | `HTTPS_PORT` | `443` | no | yes | | `logto_console_port` | `--logto-console-port` | `LOGTO_CONSOLE_PORT` | `3002` | no | yes | | `logto_console_exposed` | `--logto-console-exposed` | `LOGTO_CONSOLE_EXPOSED` | `true` | no | yes | | `vendor_enabled` | `--vendor-enabled` | `VENDOR_ENABLED` | `false` | no | yes | | `vendor_user` | `--vendor-user` | `VENDOR_USER` | `vendor` | no | yes | | `vendor_password` | `--vendor-password` | `VENDOR_PASS` | generated | no | yes | | `version` | `--version` | `CAMELEER_VERSION` | `latest` | no | yes | | `compose_project` | `--compose-project` | `COMPOSE_PROJECT` | `cameleer-saas` | no | yes | | `docker_socket` | `--docker-socket` | `DOCKER_SOCKET` | auto-detect | no | yes | | `node_tls_reject` | `--node-tls-reject` | `NODE_TLS_REJECT` | `0` (self-signed) / `1` (custom) | no | yes | *\* Only asked in simple mode if the user chooses custom TLS.* ### Config File Format (`cameleer.conf`) ```ini # Cameleer installation config # Generated by installer v1.0.0 on 2026-04-13 install_dir=./cameleer public_host=cameleer.example.com public_protocol=https admin_user=my-admin version=1.0.0 tls_mode=custom https_port=443 monitoring_network=prometheus ``` Plain `key=value`, `#` comments. Portable between Linux and Windows. ## Auto-Detection The installer auto-detects sensible defaults: | Value | Linux | Windows | |---|---|---| | Public hostname | `hostname -f`, reverse DNS of primary IP, fallback `localhost` | `[System.Net.Dns]::GetHostEntry`, fallback `localhost` | | Docker socket | `/var/run/docker.sock` | `//./pipe/docker_engine` | | Port availability | `ss -tlnp` or `netstat` check on 80, 443, 3002 | `Test-NetConnection` on 80, 443, 3002 | | Existing install | Check for `cameleer.conf` in install directory | Same | ## Output Files The installer generates the following in the install directory: ``` ./cameleer/ docker-compose.yml # Generated from embedded template .env # All service configuration .env.bak # Snapshot of .env at install time cameleer.conf # Installer config (for re-runs, cloning) credentials.txt # All generated passwords in plain text INSTALL.md # Tailored documentation certs/ # Only if user supplies custom TLS certs cert.pem key.pem ca.pem ``` ### docker-compose.yml (generated) The compose file is generated from a template embedded in the script, with values substituted from the user's configuration. Key characteristics: - All services use `${VARIABLE}` references to `.env` - No bind mounts except Docker socket and optional `certs/` directory - Shared volumes: `pgdata`, `chdata`, `bootstrapdata`, `certs` - Networks: `cameleer` (internal), `cameleer-traefik` (for dynamic tenant routing) - Optional external `monitoring_network` with Prometheus labels on services - Health checks on all services - `depends_on` with health conditions for startup ordering ### credentials.txt ``` =========================================== CAMELEER PLATFORM CREDENTIALS Generated: 2026-04-13 14:32:00 UTC SECURE THIS FILE AND DELETE AFTER NOTING THESE CREDENTIALS CANNOT BE RECOVERED =========================================== Admin Console: https://cameleer.example.com/platform/ Admin User: my-admin Admin Password: aB3x...generated...9Zq PostgreSQL: cameleer / Kx8m...generated...Wp2 ClickHouse: default / Rm4n...generated...Ht7 Vendor User: acme-admin (not enabled) Logto Console: https://cameleer.example.com:3002 ``` Printed to terminal once at the end of installation. Never displayed again on re-runs. ### INSTALL.md (generated) Tailored to the actual installation values. Sections: 1. **Installation Summary** — version, date, mode, install directory 2. **Service URLs** — platform UI, Logto admin console, API endpoint 3. **First Steps** — log in as admin, create first tenant 4. **Architecture Overview** — containers running, purpose of each 5. **Networking** — ports, monitoring network, Docker networks 6. **TLS** — self-signed or custom, cert location, how to replace via vendor UI 7. **Data & Backups** — Docker volume names, backup commands (pg_dump, clickhouse-backup) 8. **Upgrading** — re-run installer with `--version`, what gets preserved 9. **Troubleshooting** — common issues with `docker compose logs` commands 10. **Uninstalling** — clean removal steps ## Password Generation When no password is provided, the script generates cryptographically secure random passwords: - Linux: `openssl rand -base64 24` (32 characters) - Windows: `[System.Security.Cryptography.RandomNumberGenerator]` → Base64 ### Passwords Generated | Credential | Config key | Consumers | |---|---|---| | PostgreSQL password | `postgres_password` | postgres, logto, cameleer-saas | | ClickHouse password | `clickhouse_password` | clickhouse, cameleer-saas (tenant provisioning) | | Admin password | `admin_password` | Logto admin user | | Vendor password | `vendor_password` | Logto vendor user (only if enabled) | ### Credential Lifecycle 1. Generated (or user-provided) during install 2. Written to `.env` (consumed by Docker Compose) 3. Written to `credentials.txt` in plain text 4. Printed to terminal once at end of installation 5. Never shown again — re-runs preserve existing credentials without displaying them ## Monitoring Network Integration When a monitoring network is configured (simple or expert mode): 1. The script verifies the network exists via `docker network inspect` - If missing in interactive mode: asks whether to create it or skip - If missing in silent mode: creates it automatically 2. The network is added as an external network in the generated `docker-compose.yml` 3. Services are attached to it and labeled for Prometheus Docker SD: ```yaml cameleer-saas: labels: prometheus.io/scrape: "true" prometheus.io/port: "8080" prometheus.io/path: "/platform/actuator/prometheus" cameleer-traefik: labels: prometheus.io/scrape: "true" prometheus.io/port: "8082" prometheus.io/path: "/metrics" cameleer-clickhouse: labels: prometheus.io/scrape: "true" prometheus.io/port: "9363" prometheus.io/path: "/metrics" ``` No Prometheus configuration needed on the customer's side — Docker service discovery picks up the labels automatically. ## Idempotent Re-run & Upgrade ### Detection The script checks for `cameleer.conf` in the install directory. If found, it's a re-run. ### Interactive Re-run Menu ``` Existing Cameleer installation detected (v1.0.0) Install directory: ./cameleer Public host: cameleer.example.com [1] Upgrade to v1.1.0 (pull new images, update compose) [2] Reconfigure (re-run interactive setup, preserve data) [3] Reinstall (fresh install, WARNING: destroys data volumes) [4] Cancel ``` ### Re-run Behavior | Action | Preserve | Regenerate | Pull images | |---|---|---|---| | Upgrade | `.env`, `cameleer.conf`, `credentials.txt`, `certs/`, volumes | `docker-compose.yml`, `INSTALL.md` | yes (new version) | | Reconfigure | Data volumes, `credentials.txt` (unless passwords changed) | `.env`, `docker-compose.yml`, `cameleer.conf`, `INSTALL.md` | optional | | Reinstall | Nothing | Everything | yes | ### Silent Re-run Defaults to upgrade. Override with `--reconfigure` or `--reinstall`. ### Safety - Data volumes (`pgdata`, `chdata`, `bootstrapdata`) are never removed unless `--reinstall` is explicitly chosen - `--reinstall` requires double opt-in: `--reinstall --confirm-destroy` - The script never runs `docker volume rm` without this confirmation ## Health Verification After `docker compose up -d`, the script polls services in dependency order: | Step | Service | Check | Timeout | |---|---|---|---| | 1 | PostgreSQL | `pg_isready` via `docker compose exec` | 120s | | 2 | ClickHouse | `clickhouse-client` query via `docker compose exec` | 120s | | 3 | Logto | GET `/oidc/.well-known/openid-configuration` | 120s | | 4 | Bootstrap | Check `logto-bootstrap.json` exists in volume | 120s | | 5 | Cameleer SaaS | GET `/platform/api/config` | 120s | | 6 | Traefik | GET `https://{PUBLIC_HOST}/` (expect redirect) | 120s | **Polling interval:** 5 seconds. **Total timeout:** 5 minutes. ### Output ``` Verifying installation... [ok] PostgreSQL ready (3s) [ok] ClickHouse ready (5s) [ok] Logto ready (18s) [ok] Bootstrap complete (0s) [ok] Cameleer SaaS ready (8s) [ok] Traefik routing ready (1s) Installation complete! ``` ### Failure - Failing service marked with `[FAIL]` and a hint (e.g., "check `docker compose logs logto`") - Remaining checks skipped - Stack left running for inspection - Script exits with code 1 ## Script Structure (both platforms) ``` main() parse_args() detect_existing_install() if existing → show_rerun_menu() check_prerequisites() auto_detect_defaults() select_mode() # simple / expert / silent if interactive → run_prompts() merge_config() # CLI > env > config file > defaults validate_config() generate_passwords() # for any not provided if custom_certs → copy_certs() generate_env_file() generate_compose_file() write_config_file() # cameleer.conf docker_compose_pull() docker_compose_up() verify_health() generate_credentials_file() generate_install_doc() print_credentials() print_summary() ``` Each function has a direct equivalent in both bash and PowerShell. The logic, prompts, and output are identical across platforms. ## TLS Certificate Flow (Simplified) With the `traefik-certs` init container merged into `cameleer-traefik`, the certificate flow works as follows: **Shared `certs` Docker volume** remains the mechanism for sharing TLS state between `cameleer-traefik` and `cameleer-saas` (which mounts it read-only for per-tenant server provisioning). **Self-signed mode (default):** 1. `cameleer-traefik` entrypoint checks if `/certs/cert.pem` exists in the volume 2. If not, generates a self-signed cert for `${PUBLIC_HOST}` with wildcard SAN using `openssl` 3. Writes `cert.pem`, `key.pem`, `meta.json` to the `certs` volume 4. Starts Traefik normally **Custom cert mode:** 1. The installer copies user-supplied cert files to `./cameleer/certs/` on the host 2. The generated `docker-compose.yml` bind-mounts `./certs/:/user-certs:ro` on the `cameleer-traefik` service 3. `cameleer-traefik` entrypoint detects `CERT_FILE=/user-certs/cert.pem` and `KEY_FILE=/user-certs/key.pem` 4. Validates and copies them to the shared `certs` Docker volume 5. Writes `meta.json` with certificate metadata 6. Starts Traefik normally **Runtime cert replacement** (via vendor UI) continues to work unchanged — `cameleer-saas` writes to the `certs` volume's `staged/` directory and performs atomic swaps. ## Docker Socket Path The generated `docker-compose.yml` uses the platform-appropriate Docker socket path: - Linux: `/var/run/docker.sock:/var/run/docker.sock` - Windows (Docker Desktop): `//./pipe/docker_engine://./pipe/docker_engine` The installer detects the platform and generates the correct bind mount. The `docker_socket` config key allows overriding this in expert mode.