docs: add design spec for externalizing docker compose templates

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-15 20:47:14 +02:00
parent 41052d01e8
commit 19c463051a

View File

@@ -0,0 +1,164 @@
# 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.