# Externalize Docker Compose Templates **Date:** 2026-04-15 **Status:** Approved ## Problem The installer scripts (`install.sh` and `install.ps1`) generate docker-compose YAML inline via heredocs and string interpolation. This causes three problems: 1. **Maintainability** -- ~450 lines of compose generation are duplicated across two scripts in two languages. Every compose change must be made twice. 2. **Readability** -- The compose structure is buried inside 1800-line scripts and difficult to review or edit. 3. **User customization** -- Users cannot tweak the compose after installation without re-running the installer or editing generated output that gets overwritten on next run. ## Design Replace inline compose generation with static template files. The installer collects user input, writes `.env`, copies templates, and runs `docker compose up -d`. ### File Layout ``` installer/ templates/ docker-compose.yml # Infra: traefik, postgres, clickhouse docker-compose.server.yml # Standalone: server + server-ui docker-compose.saas.yml # SaaS: logto + cameleer-saas docker-compose.tls.yml # Overlay: custom cert volume on traefik docker-compose.monitoring.yml # Overlay: override monitoring network to external .env.example # Documented variable reference install.sh install.ps1 ``` ### Compose File Responsibilities #### `docker-compose.yml` (infra base, always loaded) - **Services:** `cameleer-traefik`, `cameleer-postgres`, `cameleer-clickhouse` - **Prometheus labels** on all services unconditionally (harmless when no scraper connects) - **Logto console port** always mapped; bind address controlled by `${LOGTO_CONSOLE_BIND:-127.0.0.1}` (exposed = `0.0.0.0`, unexposed = localhost-only) - **Networks:** `cameleer`, `cameleer-traefik`, `monitoring` (local noop bridge by default) - **Volumes:** `cameleer-pgdata`, `cameleer-chdata`, `cameleer-certs` - All services reference the `monitoring` network #### `docker-compose.server.yml` (standalone mode) - **Services:** `cameleer-server`, `cameleer-server-ui` - **Additional volumes:** `jars` - **Additional network:** `cameleer-apps` - **Monitoring network:** defines local noop bridge, both services reference it #### `docker-compose.saas.yml` (SaaS mode) - **Services:** `cameleer-logto`, `cameleer-saas` - **Additional volume:** `cameleer-bootstrapdata` - **Monitoring network:** defines local noop bridge, both services reference it #### `docker-compose.tls.yml` (overlay) - Adds `./certs:/user-certs:ro` volume mount to `cameleer-traefik` #### `docker-compose.monitoring.yml` (overlay) - Overrides the `monitoring` network definition from local noop bridge to `external: true` with `name: ${MONITORING_NETWORK}` - No per-service entries needed -- services already reference the `monitoring` network in their base files ### Monitoring Network Pattern Each compose file defines the `monitoring` network as a local bridge with a throwaway name: ```yaml # In docker-compose.yml, docker-compose.server.yml, docker-compose.saas.yml networks: monitoring: name: cameleer-monitoring-noop ``` Services reference this network unconditionally. Without the monitoring overlay, it is an inert local bridge. When `docker-compose.monitoring.yml` is included, Docker Compose merges the network definition, overriding it to external: ```yaml # docker-compose.monitoring.yml networks: monitoring: external: true name: ${MONITORING_NETWORK} ``` This avoids conditional YAML injection and keeps a single monitoring overlay file regardless of deployment mode. ### Logto Console Port Handling Instead of conditionally injecting the port mapping, always include it with a bind address variable: ```yaml # In docker-compose.yml (traefik ports) - "${LOGTO_CONSOLE_BIND:-127.0.0.1}:${LOGTO_CONSOLE_PORT:-3002}:3002" ``` The installer writes `LOGTO_CONSOLE_BIND=0.0.0.0` when the user chooses to expose the console, and omits it (defaulting to `127.0.0.1`) when not. ### `.env.example` Documented reference of all variables, grouped by section: - **Version/images:** `VERSION`, `CAMELEER_SAAS_PROVISIONING_SERVERIMAGE`, `CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE` - **Public access:** `PUBLIC_HOST`, `PUBLIC_PROTOCOL` - **Ports:** `HTTP_PORT`, `HTTPS_PORT`, `LOGTO_CONSOLE_BIND`, `LOGTO_CONSOLE_PORT` - **PostgreSQL:** `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB` - **ClickHouse:** `CLICKHOUSE_PASSWORD` - **Admin credentials (SaaS):** `SAAS_ADMIN_USER`, `SAAS_ADMIN_PASS` - **Admin credentials (standalone):** `SERVER_ADMIN_USER`, `SERVER_ADMIN_PASS`, `BOOTSTRAP_TOKEN` - **Docker:** `DOCKER_SOCKET`, `DOCKER_GID` - **TLS (optional):** `CERT_FILE`, `KEY_FILE`, `CA_FILE` - **Monitoring (optional):** `MONITORING_NETWORK` - **Compose file assembly:** `COMPOSE_FILE` - **TLS validation:** `NODE_TLS_REJECT` ### `COMPOSE_FILE` Assembly The installer builds the `COMPOSE_FILE` value in `.env` based on user choices: | Mode | COMPOSE_FILE | |---|---| | Standalone | `docker-compose.yml:docker-compose.server.yml` | | Standalone + TLS | `docker-compose.yml:docker-compose.server.yml:docker-compose.tls.yml` | | Standalone + monitoring | `docker-compose.yml:docker-compose.server.yml:docker-compose.monitoring.yml` | | SaaS | `docker-compose.yml:docker-compose.saas.yml` | | SaaS + TLS | `docker-compose.yml:docker-compose.saas.yml:docker-compose.tls.yml` | | SaaS + TLS + monitoring | `docker-compose.yml:docker-compose.saas.yml:docker-compose.tls.yml:docker-compose.monitoring.yml` | ### Installer Script Changes **Removed:** - `generate_compose_file()` / `Generate-ComposeFile` (~250 lines each, SaaS mode) - `generate_compose_file_standalone()` / `Generate-ComposeFileStandalone` (~200 lines each, standalone mode) - All conditional YAML injection logic (logto console, TLS, monitoring, GID) **Added:** - Copy template files from `templates/` to install directory - `COMPOSE_FILE` assembly logic in `.env` generation - `LOGTO_CONSOLE_BIND` variable (replaces conditional port injection) - `DOCKER_GID` written to `.env` (replaces inline `stat` in heredoc) **Unchanged:** - User prompting, argument parsing, config file reading - Password generation, Docker GID detection - `credentials.txt`, `INSTALL.md`, `cameleer.conf` generation - `docker compose up -d` invocation ### Password Fallback Safety All password variables in templates use `:?` (fail-if-unset) instead of `:-` (default) to prevent silent fallback to weak credentials: ```yaml SAAS_ADMIN_PASS: ${SAAS_ADMIN_PASS:?SAAS_ADMIN_PASS must be set in .env} ``` Username defaults (`:-admin`) are acceptable and retained. ### Migration for Existing Installs Existing `installer/cameleer/` has a single generated `docker-compose.yml`. On re-install, the installer copies the template files and regenerates `.env`. The old monolithic compose file is replaced by the multi-file set. `cameleer.conf` preserves user choices, so re-running the installer reproduces the same configuration with the new file structure.