12 Commits

Author SHA1 Message Date
hsiegeln
dc4ea33c9b feat: externalize docker-compose templates from installer scripts
All checks were successful
CI / build (push) Successful in 1m16s
CI / docker (push) Successful in 20s
Replace inline heredoc compose generation with static template files.
Templates are copied to the install dir and composed via COMPOSE_FILE in .env.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 21:53:26 +02:00
hsiegeln
186f7639ad docs: update CLAUDE.md with template-based installer architecture
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 21:11:04 +02:00
hsiegeln
6c7895b0d6 chore(installer): remove generated install output, add to gitignore
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 21:09:30 +02:00
hsiegeln
6170f61eeb refactor(installer): replace ps1 compose generation with template copying
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 21:08:34 +02:00
hsiegeln
2ed527ac74 refactor(installer): replace sh compose generation with template copying
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 21:03:01 +02:00
hsiegeln
cb1f6b8ccf feat(installer): add .env.example with documented variables
Reference .env file documenting all configuration variables across both
deployment modes, with section headers for compose assembly, public access,
credentials, TLS, Docker, provisioning, and monitoring.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 20:59:15 +02:00
hsiegeln
758585cc9a feat(installer): add TLS and monitoring overlay templates
Optional compose overlays: TLS overlay mounts user-supplied certs into
traefik, monitoring overlay replaces the noop bridge with an external
Docker network for Prometheus scraping.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 20:59:10 +02:00
hsiegeln
141b44048c feat(installer): add standalone docker-compose and traefik templates
Standalone mode: server + server-ui services with postgres image override
to stock postgres:16-alpine. Includes traefik-dynamic.yml for default TLS
certificate store configuration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 20:59:05 +02:00
hsiegeln
3c343f9441 feat(installer): add SaaS docker-compose template
Logto identity provider and cameleer-saas management plane services.
Includes Traefik labels, CORS config, bootstrap healthcheck, and all
provisioning env vars parameterized from .env.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 20:59:00 +02:00
hsiegeln
bdb24f8de6 feat(installer): add infra base docker-compose template
Shared infrastructure base (traefik, postgres, clickhouse) always loaded
regardless of deployment mode. Uses parameterized images, fail-if-unset
password variables, and a noop monitoring network bridge.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 20:58:54 +02:00
hsiegeln
933b56f68f docs: add implementation plan for externalizing compose templates
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 20:54:31 +02:00
hsiegeln
19c463051a docs: add design spec for externalizing docker compose templates
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 20:47:14 +02:00
17 changed files with 1530 additions and 1127 deletions

3
.gitignore vendored
View File

@@ -28,6 +28,9 @@ Thumbs.db
.playwright-mcp/
.gitnexus
# Installer output (generated by install.sh / install.ps1)
installer/cameleer/
# Generated by postinstall from @cameleer/design-system
ui/public/favicon.svg
docker/runtime-base/agent.jar

View File

@@ -288,6 +288,13 @@ The installer (`installer/install.sh`) supports two deployment modes:
Standalone mode generates a simpler compose with the server running directly. No Logto, no SaaS management plane, no bootstrap. The admin logs in with local credentials at `/`.
The installer uses static docker-compose templates in `installer/templates/`. Templates are copied to the install directory and composed via `COMPOSE_FILE` in `.env`:
- `docker-compose.yml` — shared infrastructure (traefik, postgres, clickhouse)
- `docker-compose.saas.yml` — SaaS mode (logto, cameleer-saas)
- `docker-compose.server.yml` — standalone mode (server, server-ui)
- `docker-compose.tls.yml` — overlay: custom TLS cert volume
- `docker-compose.monitoring.yml` — overlay: external monitoring network
### Tenant Provisioning Flow
When SaaS admin creates a tenant via `VendorTenantService`:

View File

@@ -0,0 +1,961 @@
# Externalize Docker Compose Templates — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Replace inline docker-compose generation in installer scripts with static template files, reducing duplication and enabling user customization.
**Architecture:** Static YAML templates in `installer/templates/` are copied to the install directory. The installer writes `.env` (including `COMPOSE_FILE` to select which templates are active) and runs `docker compose up -d`. Conditional features (TLS, monitoring) are handled via compose file layering and `.env` variables instead of heredoc injection.
**Tech Stack:** Docker Compose v2, YAML, Bash, PowerShell
**Spec:** `docs/superpowers/specs/2026-04-15-externalize-compose-templates-design.md`
---
### Task 1: Create `docker-compose.yml` (infra base template)
**Files:**
- Create: `installer/templates/docker-compose.yml`
This is the shared infrastructure base — always loaded regardless of deployment mode.
- [ ] **Step 1: Create the infra base template**
```yaml
# Cameleer Infrastructure
# Shared base — always loaded. Mode-specific services in separate compose files.
services:
cameleer-traefik:
image: ${TRAEFIK_IMAGE:-gitea.siegeln.net/cameleer/cameleer-traefik}:${VERSION:-latest}
restart: unless-stopped
ports:
- "${HTTP_PORT:-80}:80"
- "${HTTPS_PORT:-443}:443"
- "${LOGTO_CONSOLE_BIND:-127.0.0.1}:${LOGTO_CONSOLE_PORT:-3002}:3002"
environment:
PUBLIC_HOST: ${PUBLIC_HOST:-localhost}
CERT_FILE: ${CERT_FILE:-}
KEY_FILE: ${KEY_FILE:-}
CA_FILE: ${CA_FILE:-}
volumes:
- cameleer-certs:/certs
- ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock:ro
labels:
- "prometheus.io/scrape=true"
- "prometheus.io/port=8082"
- "prometheus.io/path=/metrics"
networks:
- cameleer
- cameleer-traefik
- monitoring
cameleer-postgres:
image: ${POSTGRES_IMAGE:-gitea.siegeln.net/cameleer/cameleer-postgres}:${VERSION:-latest}
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB:-cameleer_saas}
POSTGRES_USER: ${POSTGRES_USER:-cameleer}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set in .env}
volumes:
- cameleer-pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER:-cameleer} -d $${POSTGRES_DB:-cameleer_saas}"]
interval: 5s
timeout: 5s
retries: 5
networks:
- cameleer
- monitoring
cameleer-clickhouse:
image: ${CLICKHOUSE_IMAGE:-gitea.siegeln.net/cameleer/cameleer-clickhouse}:${VERSION:-latest}
restart: unless-stopped
environment:
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:?CLICKHOUSE_PASSWORD must be set in .env}
volumes:
- cameleer-chdata:/var/lib/clickhouse
healthcheck:
test: ["CMD-SHELL", "clickhouse-client --password $${CLICKHOUSE_PASSWORD} --query 'SELECT 1'"]
interval: 10s
timeout: 5s
retries: 3
labels:
- "prometheus.io/scrape=true"
- "prometheus.io/port=9363"
- "prometheus.io/path=/metrics"
networks:
- cameleer
- monitoring
volumes:
cameleer-pgdata:
cameleer-chdata:
cameleer-certs:
networks:
cameleer:
driver: bridge
cameleer-traefik:
name: cameleer-traefik
driver: bridge
monitoring:
name: cameleer-monitoring-noop
```
Key changes from the generated version:
- Logto console port always present with `LOGTO_CONSOLE_BIND` controlling exposure
- Prometheus labels unconditional on traefik and clickhouse
- `monitoring` network defined as local noop bridge
- All services join `monitoring` network
- `POSTGRES_DB` uses `${POSTGRES_DB:-cameleer_saas}` (parameterized — standalone overrides via `.env`)
- Password variables use `:?` fail-if-unset
Note: The SaaS mode uses `cameleer-postgres` (custom multi-DB image) while standalone uses `postgres:16-alpine`. The `POSTGRES_IMAGE` variable already handles this — the infra base uses `${POSTGRES_IMAGE:-...}` and standalone `.env` sets `POSTGRES_IMAGE=postgres:16-alpine`.
- [ ] **Step 2: Verify YAML is valid**
Run: `python -c "import yaml; yaml.safe_load(open('installer/templates/docker-compose.yml'))"`
Expected: No output (valid YAML). If python/yaml not available, use `docker compose -f installer/templates/docker-compose.yml config --quiet` (will fail on unset vars, but validates structure).
- [ ] **Step 3: Commit**
```bash
git add installer/templates/docker-compose.yml
git commit -m "feat(installer): add infra base docker-compose template"
```
---
### Task 2: Create `docker-compose.saas.yml` (SaaS mode template)
**Files:**
- Create: `installer/templates/docker-compose.saas.yml`
SaaS-specific services: Logto identity provider and cameleer-saas management plane.
- [ ] **Step 1: Create the SaaS template**
```yaml
# Cameleer SaaS — Logto + management plane
# Loaded in SaaS deployment mode
services:
cameleer-logto:
image: ${LOGTO_IMAGE:-gitea.siegeln.net/cameleer/cameleer-logto}:${VERSION:-latest}
restart: unless-stopped
depends_on:
cameleer-postgres:
condition: service_healthy
environment:
DB_URL: postgres://${POSTGRES_USER:-cameleer}:${POSTGRES_PASSWORD}@cameleer-postgres:5432/logto
ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}
ADMIN_ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}:${LOGTO_CONSOLE_PORT:-3002}
TRUST_PROXY_HEADER: 1
NODE_TLS_REJECT_UNAUTHORIZED: "${NODE_TLS_REJECT:-0}"
LOGTO_ENDPOINT: http://cameleer-logto:3001
LOGTO_ADMIN_ENDPOINT: http://cameleer-logto:3002
LOGTO_PUBLIC_ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}
PUBLIC_HOST: ${PUBLIC_HOST:-localhost}
PUBLIC_PROTOCOL: ${PUBLIC_PROTOCOL:-https}
PG_HOST: cameleer-postgres
PG_USER: ${POSTGRES_USER:-cameleer}
PG_PASSWORD: ${POSTGRES_PASSWORD}
PG_DB_SAAS: cameleer_saas
SAAS_ADMIN_USER: ${SAAS_ADMIN_USER:-admin}
SAAS_ADMIN_PASS: ${SAAS_ADMIN_PASS:?SAAS_ADMIN_PASS must be set in .env}
healthcheck:
test: ["CMD-SHELL", "node -e \"require('http').get('http://localhost:3001/oidc/.well-known/openid-configuration', r => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))\" && test -f /data/logto-bootstrap.json"]
interval: 10s
timeout: 5s
retries: 60
start_period: 30s
labels:
- traefik.enable=true
- traefik.http.routers.cameleer-logto.rule=PathPrefix(`/`)
- traefik.http.routers.cameleer-logto.priority=1
- traefik.http.routers.cameleer-logto.entrypoints=websecure
- traefik.http.routers.cameleer-logto.tls=true
- traefik.http.routers.cameleer-logto.service=cameleer-logto
- traefik.http.routers.cameleer-logto.middlewares=cameleer-logto-cors
- "traefik.http.middlewares.cameleer-logto-cors.headers.accessControlAllowOriginList=${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}:${LOGTO_CONSOLE_PORT:-3002}"
- traefik.http.middlewares.cameleer-logto-cors.headers.accessControlAllowMethods=GET,POST,PUT,PATCH,DELETE,OPTIONS
- traefik.http.middlewares.cameleer-logto-cors.headers.accessControlAllowHeaders=Authorization,Content-Type
- traefik.http.middlewares.cameleer-logto-cors.headers.accessControlAllowCredentials=true
- traefik.http.services.cameleer-logto.loadbalancer.server.port=3001
- traefik.http.routers.cameleer-logto-console.rule=PathPrefix(`/`)
- traefik.http.routers.cameleer-logto-console.entrypoints=admin-console
- traefik.http.routers.cameleer-logto-console.tls=true
- traefik.http.routers.cameleer-logto-console.service=cameleer-logto-console
- traefik.http.services.cameleer-logto-console.loadbalancer.server.port=3002
volumes:
- cameleer-bootstrapdata:/data
networks:
- cameleer
- monitoring
cameleer-saas:
image: ${CAMELEER_IMAGE:-gitea.siegeln.net/cameleer/cameleer-saas}:${VERSION:-latest}
restart: unless-stopped
depends_on:
cameleer-logto:
condition: service_healthy
environment:
# SaaS database
SPRING_DATASOURCE_URL: jdbc:postgresql://cameleer-postgres:5432/cameleer_saas
SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER:-cameleer}
SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD}
# Identity (Logto)
CAMELEER_SAAS_IDENTITY_LOGTOENDPOINT: http://cameleer-logto:3001
CAMELEER_SAAS_IDENTITY_LOGTOPUBLICENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}
# Provisioning — passed to per-tenant server containers
CAMELEER_SAAS_PROVISIONING_PUBLICHOST: ${PUBLIC_HOST:-localhost}
CAMELEER_SAAS_PROVISIONING_PUBLICPROTOCOL: ${PUBLIC_PROTOCOL:-https}
CAMELEER_SAAS_PROVISIONING_NETWORKNAME: ${COMPOSE_PROJECT_NAME:-cameleer-saas}_cameleer
CAMELEER_SAAS_PROVISIONING_TRAEFIKNETWORK: cameleer-traefik
CAMELEER_SAAS_PROVISIONING_DATASOURCEUSERNAME: ${POSTGRES_USER:-cameleer}
CAMELEER_SAAS_PROVISIONING_DATASOURCEPASSWORD: ${POSTGRES_PASSWORD}
CAMELEER_SAAS_PROVISIONING_CLICKHOUSEPASSWORD: ${CLICKHOUSE_PASSWORD}
CAMELEER_SAAS_PROVISIONING_SERVERIMAGE: ${CAMELEER_SAAS_PROVISIONING_SERVERIMAGE:-gitea.siegeln.net/cameleer/cameleer-server:latest}
CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE: ${CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE:-gitea.siegeln.net/cameleer/cameleer-server-ui:latest}
labels:
- traefik.enable=true
- traefik.http.routers.saas.rule=PathPrefix(`/platform`)
- traefik.http.routers.saas.entrypoints=websecure
- traefik.http.routers.saas.tls=true
- traefik.http.services.saas.loadbalancer.server.port=8080
- "prometheus.io/scrape=true"
- "prometheus.io/port=8080"
- "prometheus.io/path=/platform/actuator/prometheus"
volumes:
- cameleer-bootstrapdata:/data/bootstrap:ro
- cameleer-certs:/certs
- ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock
group_add:
- "${DOCKER_GID:-0}"
networks:
- cameleer
- monitoring
volumes:
cameleer-bootstrapdata:
networks:
monitoring:
name: cameleer-monitoring-noop
```
Key changes:
- Logto console traefik labels always included (harmless when port is localhost-only)
- Prometheus labels on cameleer-saas always included
- `DOCKER_GID` read from `.env` via `${DOCKER_GID:-0}` instead of inline `stat`
- Both services join `monitoring` network
- `monitoring` network redefined as noop bridge (compose merges with base definition)
- [ ] **Step 2: Commit**
```bash
git add installer/templates/docker-compose.saas.yml
git commit -m "feat(installer): add SaaS docker-compose template"
```
---
### Task 3: Create `docker-compose.server.yml` (standalone mode template)
**Files:**
- Create: `installer/templates/docker-compose.server.yml`
- Create: `installer/templates/traefik-dynamic.yml`
Standalone-specific services: cameleer-server + server-ui. Also includes the traefik dynamic config that standalone mode needs (overrides the baked-in SaaS redirect).
- [ ] **Step 1: Create the standalone template**
```yaml
# Cameleer Server (standalone)
# Loaded in standalone deployment mode
services:
cameleer-traefik:
volumes:
- ./traefik-dynamic.yml:/etc/traefik/dynamic.yml:ro
cameleer-postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: ${POSTGRES_DB:-cameleer}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER:-cameleer} -d $${POSTGRES_DB:-cameleer}"]
cameleer-server:
image: ${SERVER_IMAGE:-gitea.siegeln.net/cameleer/cameleer-server}:${VERSION:-latest}
container_name: cameleer-server
restart: unless-stopped
depends_on:
cameleer-postgres:
condition: service_healthy
environment:
CAMELEER_SERVER_TENANT_ID: default
SPRING_DATASOURCE_URL: jdbc:postgresql://cameleer-postgres:5432/${POSTGRES_DB:-cameleer}?currentSchema=tenant_default
SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER:-cameleer}
SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD}
CAMELEER_SERVER_CLICKHOUSE_URL: jdbc:clickhouse://cameleer-clickhouse:8123/cameleer
CAMELEER_SERVER_CLICKHOUSE_USERNAME: default
CAMELEER_SERVER_CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD}
CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN: ${BOOTSTRAP_TOKEN:?BOOTSTRAP_TOKEN must be set in .env}
CAMELEER_SERVER_SECURITY_UIUSER: ${SERVER_ADMIN_USER:-admin}
CAMELEER_SERVER_SECURITY_UIPASSWORD: ${SERVER_ADMIN_PASS:?SERVER_ADMIN_PASS must be set in .env}
CAMELEER_SERVER_SECURITY_CORSALLOWEDORIGINS: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}
CAMELEER_SERVER_RUNTIME_ENABLED: "true"
CAMELEER_SERVER_RUNTIME_SERVERURL: http://cameleer-server:8081
CAMELEER_SERVER_RUNTIME_ROUTINGDOMAIN: ${PUBLIC_HOST:-localhost}
CAMELEER_SERVER_RUNTIME_ROUTINGMODE: path
CAMELEER_SERVER_RUNTIME_JARSTORAGEPATH: /data/jars
CAMELEER_SERVER_RUNTIME_DOCKERNETWORK: cameleer-apps
CAMELEER_SERVER_RUNTIME_JARDOCKERVOLUME: cameleer-jars
CAMELEER_SERVER_RUNTIME_BASEIMAGE: gitea.siegeln.net/cameleer/cameleer-runtime-base:${VERSION:-latest}
labels:
- traefik.enable=true
- traefik.http.routers.server-api.rule=PathPrefix(`/api`)
- traefik.http.routers.server-api.entrypoints=websecure
- traefik.http.routers.server-api.tls=true
- traefik.http.services.server-api.loadbalancer.server.port=8081
- traefik.docker.network=cameleer-traefik
healthcheck:
test: ["CMD-SHELL", "curl -sf http://localhost:8081/api/v1/health || exit 1"]
interval: 10s
timeout: 5s
retries: 30
start_period: 30s
volumes:
- jars:/data/jars
- cameleer-certs:/certs:ro
- ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock
group_add:
- "${DOCKER_GID:-0}"
networks:
- cameleer
- cameleer-traefik
- cameleer-apps
- monitoring
cameleer-server-ui:
image: ${SERVER_UI_IMAGE:-gitea.siegeln.net/cameleer/cameleer-server-ui}:${VERSION:-latest}
restart: unless-stopped
depends_on:
cameleer-server:
condition: service_healthy
environment:
CAMELEER_API_URL: http://cameleer-server:8081
BASE_PATH: ""
labels:
- traefik.enable=true
- traefik.http.routers.ui.rule=PathPrefix(`/`)
- traefik.http.routers.ui.priority=1
- traefik.http.routers.ui.entrypoints=websecure
- traefik.http.routers.ui.tls=true
- traefik.http.services.ui.loadbalancer.server.port=80
- traefik.docker.network=cameleer-traefik
networks:
- cameleer-traefik
- monitoring
volumes:
jars:
networks:
cameleer-apps:
name: cameleer-apps
driver: bridge
monitoring:
name: cameleer-monitoring-noop
```
Key design decisions:
- `cameleer-traefik` and `cameleer-postgres` entries are **overrides** — compose merges them with the base. The postgres image switches to `postgres:16-alpine` and the healthcheck uses `${POSTGRES_DB:-cameleer}` instead of hardcoded `cameleer_saas`. Traefik gets the `traefik-dynamic.yml` volume mount.
- `DOCKER_GID` from `.env` via `${DOCKER_GID:-0}`
- `BOOTSTRAP_TOKEN` uses `:?` fail-if-unset
- Both server and server-ui join `monitoring` network
- [ ] **Step 2: Create the traefik dynamic config template**
```yaml
tls:
stores:
default:
defaultCertificate:
certFile: /certs/cert.pem
keyFile: /certs/key.pem
```
This file is only relevant in standalone mode (overrides the baked-in SaaS `/` -> `/platform/` redirect in the traefik image).
- [ ] **Step 3: Commit**
```bash
git add installer/templates/docker-compose.server.yml installer/templates/traefik-dynamic.yml
git commit -m "feat(installer): add standalone docker-compose and traefik templates"
```
---
### Task 4: Create overlay templates (TLS + monitoring)
**Files:**
- Create: `installer/templates/docker-compose.tls.yml`
- Create: `installer/templates/docker-compose.monitoring.yml`
- [ ] **Step 1: Create the TLS overlay**
```yaml
# Custom TLS certificates overlay
# Adds user-supplied certificate volume to traefik
services:
cameleer-traefik:
volumes:
- ./certs:/user-certs:ro
```
- [ ] **Step 2: Create the monitoring overlay**
```yaml
# External monitoring network overlay
# Overrides the noop monitoring bridge with a real external network
networks:
monitoring:
external: true
name: ${MONITORING_NETWORK:?MONITORING_NETWORK must be set in .env}
```
This is the key to the monitoring pattern: the base compose files define `monitoring` as a local noop bridge and all services join it. When this overlay is included in `COMPOSE_FILE`, compose merges the network definition — overriding it to point at the real external monitoring network. No per-service entries needed.
- [ ] **Step 3: Commit**
```bash
git add installer/templates/docker-compose.tls.yml installer/templates/docker-compose.monitoring.yml
git commit -m "feat(installer): add TLS and monitoring overlay templates"
```
---
### Task 5: Create `.env.example`
**Files:**
- Create: `installer/templates/.env.example`
- [ ] **Step 1: Create the documented variable reference**
```bash
# Cameleer Configuration
# Copy this file to .env and fill in the values.
# The installer generates .env automatically — this file is for reference.
# ============================================================
# Compose file assembly (set by installer)
# ============================================================
# SaaS: docker-compose.yml:docker-compose.saas.yml
# Standalone: docker-compose.yml:docker-compose.server.yml
# Add :docker-compose.tls.yml for custom TLS certificates
# Add :docker-compose.monitoring.yml for external monitoring network
COMPOSE_FILE=docker-compose.yml:docker-compose.saas.yml
# ============================================================
# Image version
# ============================================================
VERSION=latest
# ============================================================
# Public access
# ============================================================
PUBLIC_HOST=localhost
PUBLIC_PROTOCOL=https
# ============================================================
# Ports
# ============================================================
HTTP_PORT=80
HTTPS_PORT=443
# Set to 0.0.0.0 to expose Logto admin console externally (default: localhost only)
# LOGTO_CONSOLE_BIND=0.0.0.0
LOGTO_CONSOLE_PORT=3002
# ============================================================
# PostgreSQL
# ============================================================
POSTGRES_USER=cameleer
POSTGRES_PASSWORD=CHANGE_ME
# SaaS: cameleer_saas, Standalone: cameleer
POSTGRES_DB=cameleer_saas
# ============================================================
# ClickHouse
# ============================================================
CLICKHOUSE_PASSWORD=CHANGE_ME
# ============================================================
# Admin credentials (SaaS mode)
# ============================================================
SAAS_ADMIN_USER=admin
SAAS_ADMIN_PASS=CHANGE_ME
# ============================================================
# Admin credentials (standalone mode)
# ============================================================
# SERVER_ADMIN_USER=admin
# SERVER_ADMIN_PASS=CHANGE_ME
# BOOTSTRAP_TOKEN=CHANGE_ME
# ============================================================
# TLS
# ============================================================
# Set to 1 to reject unauthorized TLS certificates (production)
NODE_TLS_REJECT=0
# Custom TLS certificate paths (inside container, set by installer)
# CERT_FILE=/user-certs/cert.pem
# KEY_FILE=/user-certs/key.pem
# CA_FILE=/user-certs/ca.pem
# ============================================================
# Docker
# ============================================================
DOCKER_SOCKET=/var/run/docker.sock
# GID of the docker socket — detected by installer, used for container group_add
DOCKER_GID=0
# ============================================================
# Provisioning images (SaaS mode only)
# ============================================================
# CAMELEER_SAAS_PROVISIONING_SERVERIMAGE=gitea.siegeln.net/cameleer/cameleer-server:latest
# CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE=gitea.siegeln.net/cameleer/cameleer-server-ui:latest
# ============================================================
# Monitoring (optional)
# ============================================================
# External Docker network name for Prometheus scraping.
# Only needed when docker-compose.monitoring.yml is in COMPOSE_FILE.
# MONITORING_NETWORK=prometheus
```
- [ ] **Step 2: Commit**
```bash
git add installer/templates/.env.example
git commit -m "feat(installer): add .env.example with documented variables"
```
---
### Task 6: Update `install.sh` — replace compose generation with template copying
**Files:**
- Modify: `installer/install.sh:574-672` (generate_env_file — add COMPOSE_FILE and LOGTO_CONSOLE_BIND)
- Modify: `installer/install.sh:674-1135` (replace generate_compose_file + generate_compose_file_standalone with copy_templates)
- Modify: `installer/install.sh:1728-1731` (reinstall cleanup — delete template files)
- Modify: `installer/install.sh:1696-1710` (upgrade path — copy templates instead of generate)
- Modify: `installer/install.sh:1790-1791` (main — call copy_templates instead of generate_compose_file)
- [ ] **Step 1: Replace `generate_compose_file` and `generate_compose_file_standalone` with `copy_templates`**
Delete both functions (`generate_compose_file` at line 674 and `generate_compose_file_standalone` at line 934) and replace with:
```bash
copy_templates() {
local src
src="$(cd "$(dirname "$0")" && pwd)/templates"
# Base infra — always copied
cp "$src/docker-compose.yml" "$INSTALL_DIR/docker-compose.yml"
cp "$src/.env.example" "$INSTALL_DIR/.env.example"
# Mode-specific
if [ "$DEPLOYMENT_MODE" = "standalone" ]; then
cp "$src/docker-compose.server.yml" "$INSTALL_DIR/docker-compose.server.yml"
cp "$src/traefik-dynamic.yml" "$INSTALL_DIR/traefik-dynamic.yml"
else
cp "$src/docker-compose.saas.yml" "$INSTALL_DIR/docker-compose.saas.yml"
fi
# Optional overlays
if [ "$TLS_MODE" = "custom" ]; then
cp "$src/docker-compose.tls.yml" "$INSTALL_DIR/docker-compose.tls.yml"
fi
if [ -n "$MONITORING_NETWORK" ]; then
cp "$src/docker-compose.monitoring.yml" "$INSTALL_DIR/docker-compose.monitoring.yml"
fi
log_info "Copied docker-compose templates to $INSTALL_DIR"
}
```
- [ ] **Step 2: Update `generate_env_file` to include `COMPOSE_FILE`, `LOGTO_CONSOLE_BIND`, and `DOCKER_GID`**
In the standalone `.env` block (line 577-614), add after the `DOCKER_GID` line:
```bash
# Compose file assembly
COMPOSE_FILE=docker-compose.yml:docker-compose.server.yml$([ "$TLS_MODE" = "custom" ] && echo ":docker-compose.tls.yml")$([ -n "$MONITORING_NETWORK" ] && echo ":docker-compose.monitoring.yml")
EOF
```
In the SaaS `.env` block (line 617-668), add `LOGTO_CONSOLE_BIND` and `COMPOSE_FILE`. After the `LOGTO_CONSOLE_PORT` line:
```bash
LOGTO_CONSOLE_BIND=$([ "$LOGTO_CONSOLE_EXPOSED" = "true" ] && echo "0.0.0.0" || echo "127.0.0.1")
```
And at the end of the SaaS block, add the `COMPOSE_FILE` line:
```bash
# Compose file assembly
COMPOSE_FILE=docker-compose.yml:docker-compose.saas.yml$([ "$TLS_MODE" = "custom" ] && echo ":docker-compose.tls.yml")$([ -n "$MONITORING_NETWORK" ] && echo ":docker-compose.monitoring.yml")
```
Also add the `MONITORING_NETWORK` variable to `.env` when set:
```bash
if [ -n "$MONITORING_NETWORK" ]; then
echo "" >> "$f"
echo "# Monitoring" >> "$f"
echo "MONITORING_NETWORK=${MONITORING_NETWORK}" >> "$f"
fi
```
- [ ] **Step 3: Update `main()` — replace `generate_compose_file` call with `copy_templates`**
At line 1791, change:
```bash
generate_compose_file
```
to:
```bash
copy_templates
```
- [ ] **Step 4: Update `handle_rerun` upgrade path**
At line 1703, change:
```bash
generate_compose_file
```
to:
```bash
copy_templates
```
- [ ] **Step 5: Update reinstall cleanup to remove template files**
At lines 1728-1731, update the `rm -f` list to include all possible template files:
```bash
rm -f "$INSTALL_DIR/.env" "$INSTALL_DIR/.env.bak" "$INSTALL_DIR/.env.example" \
"$INSTALL_DIR/docker-compose.yml" "$INSTALL_DIR/docker-compose.saas.yml" \
"$INSTALL_DIR/docker-compose.server.yml" "$INSTALL_DIR/docker-compose.tls.yml" \
"$INSTALL_DIR/docker-compose.monitoring.yml" "$INSTALL_DIR/traefik-dynamic.yml" \
"$INSTALL_DIR/cameleer.conf" "$INSTALL_DIR/credentials.txt" \
"$INSTALL_DIR/INSTALL.md"
```
- [ ] **Step 6: Commit**
```bash
git add installer/install.sh
git commit -m "refactor(installer): replace sh compose generation with template copying"
```
---
### Task 7: Update `install.ps1` — replace compose generation with template copying
**Files:**
- Modify: `installer/install.ps1:574-666` (Generate-EnvFile — add COMPOSE_FILE and LOGTO_CONSOLE_BIND)
- Modify: `installer/install.ps1:671-1105` (replace Generate-ComposeFile + Generate-ComposeFileStandalone with Copy-Templates)
- Modify: `installer/install.ps1:1706-1723` (upgrade path)
- Modify: `installer/install.ps1:1746` (reinstall cleanup)
- Modify: `installer/install.ps1:1797-1798` (Main — call Copy-Templates)
- [ ] **Step 1: Replace `Generate-ComposeFile` and `Generate-ComposeFileStandalone` with `Copy-Templates`**
Delete both functions and replace with:
```powershell
function Copy-Templates {
$c = $script:cfg
$src = Join-Path $PSScriptRoot 'templates'
# Base infra — always copied
Copy-Item (Join-Path $src 'docker-compose.yml') (Join-Path $c.InstallDir 'docker-compose.yml') -Force
Copy-Item (Join-Path $src '.env.example') (Join-Path $c.InstallDir '.env.example') -Force
# Mode-specific
if ($c.DeploymentMode -eq 'standalone') {
Copy-Item (Join-Path $src 'docker-compose.server.yml') (Join-Path $c.InstallDir 'docker-compose.server.yml') -Force
Copy-Item (Join-Path $src 'traefik-dynamic.yml') (Join-Path $c.InstallDir 'traefik-dynamic.yml') -Force
} else {
Copy-Item (Join-Path $src 'docker-compose.saas.yml') (Join-Path $c.InstallDir 'docker-compose.saas.yml') -Force
}
# Optional overlays
if ($c.TlsMode -eq 'custom') {
Copy-Item (Join-Path $src 'docker-compose.tls.yml') (Join-Path $c.InstallDir 'docker-compose.tls.yml') -Force
}
if ($c.MonitoringNetwork) {
Copy-Item (Join-Path $src 'docker-compose.monitoring.yml') (Join-Path $c.InstallDir 'docker-compose.monitoring.yml') -Force
}
Log-Info "Copied docker-compose templates to $($c.InstallDir)"
}
```
- [ ] **Step 2: Update `Generate-EnvFile` to include `COMPOSE_FILE`, `LOGTO_CONSOLE_BIND`, and `MONITORING_NETWORK`**
In the standalone `.env` content block, add after `DOCKER_GID`:
```powershell
$composeFile = 'docker-compose.yml:docker-compose.server.yml'
if ($c.TlsMode -eq 'custom') { $composeFile += ':docker-compose.tls.yml' }
if ($c.MonitoringNetwork) { $composeFile += ':docker-compose.monitoring.yml' }
```
Then append to `$content`:
```powershell
$content += "`n`n# Compose file assembly`nCOMPOSE_FILE=$composeFile"
if ($c.MonitoringNetwork) {
$content += "`n`n# Monitoring`nMONITORING_NETWORK=$($c.MonitoringNetwork)"
}
```
In the SaaS `.env` content block, add `LOGTO_CONSOLE_BIND` after `LOGTO_CONSOLE_PORT`:
```powershell
$consoleBind = if ($c.LogtoConsoleExposed -eq 'true') { '0.0.0.0' } else { '127.0.0.1' }
```
Add to the content string: `LOGTO_CONSOLE_BIND=$consoleBind`
Build `COMPOSE_FILE`:
```powershell
$composeFile = 'docker-compose.yml:docker-compose.saas.yml'
if ($c.TlsMode -eq 'custom') { $composeFile += ':docker-compose.tls.yml' }
if ($c.MonitoringNetwork) { $composeFile += ':docker-compose.monitoring.yml' }
```
And append to `$content`:
```powershell
$content += "`n`n# Compose file assembly`nCOMPOSE_FILE=$composeFile"
if ($c.MonitoringNetwork) {
$content += "`n`n# Monitoring`nMONITORING_NETWORK=$($c.MonitoringNetwork)"
}
```
- [ ] **Step 3: Update `Main` — replace `Generate-ComposeFile` call with `Copy-Templates`**
At line 1798, change:
```powershell
Generate-ComposeFile
```
to:
```powershell
Copy-Templates
```
- [ ] **Step 4: Update `Handle-Rerun` upgrade path**
At line 1716, change:
```powershell
Generate-ComposeFile
```
to:
```powershell
Copy-Templates
```
- [ ] **Step 5: Update reinstall cleanup to remove template files**
At line 1746, update the filename list:
```powershell
foreach ($fname in @('.env','.env.bak','.env.example','docker-compose.yml','docker-compose.saas.yml','docker-compose.server.yml','docker-compose.tls.yml','docker-compose.monitoring.yml','traefik-dynamic.yml','cameleer.conf','credentials.txt','INSTALL.md')) {
```
- [ ] **Step 6: Commit**
```bash
git add installer/install.ps1
git commit -m "refactor(installer): replace ps1 compose generation with template copying"
```
---
### Task 8: Update existing generated install and clean up
**Files:**
- Modify: `installer/cameleer/docker-compose.yml` (replace with template copy for dev environment)
- [ ] **Step 1: Remove the old generated docker-compose.yml from the cameleer/ directory**
The `installer/cameleer/` directory contains a previously generated install. The `docker-compose.yml` there is now stale — it was generated by the old inline method. Since this is a dev environment output, remove it (it will be recreated by running the installer with the new template approach).
```bash
git rm installer/cameleer/docker-compose.yml
```
- [ ] **Step 2: Add `installer/cameleer/` to `.gitignore` if not already there**
The install output directory should not be tracked. Check if `.gitignore` already covers it. If not, add:
```
installer/cameleer/
```
This prevents generated `.env`, `credentials.txt`, and compose files from being committed.
- [ ] **Step 3: Commit**
```bash
git add -A installer/cameleer/ .gitignore
git commit -m "chore(installer): remove generated install output, add to gitignore"
```
---
### Task 9: Verify the templates produce equivalent output
**Files:** (no changes — verification only)
- [ ] **Step 1: Compare template output against the old generated compose**
Create a temporary `.env` file and run `docker compose config` to render the resolved compose. Compare against the old generated output:
```bash
cd installer/cameleer
# Back up old generated file for comparison
cp docker-compose.yml docker-compose.old.yml 2>/dev/null || true
# Create a test .env that exercises the SaaS path
cat > /tmp/test-saas.env << 'EOF'
COMPOSE_FILE=docker-compose.yml:docker-compose.saas.yml
VERSION=latest
PUBLIC_HOST=test.example.com
PUBLIC_PROTOCOL=https
HTTP_PORT=80
HTTPS_PORT=443
LOGTO_CONSOLE_PORT=3002
LOGTO_CONSOLE_BIND=0.0.0.0
POSTGRES_USER=cameleer
POSTGRES_PASSWORD=testpass
POSTGRES_DB=cameleer_saas
CLICKHOUSE_PASSWORD=testpass
SAAS_ADMIN_USER=admin
SAAS_ADMIN_PASS=testpass
NODE_TLS_REJECT=0
DOCKER_SOCKET=/var/run/docker.sock
DOCKER_GID=0
CAMELEER_SAAS_PROVISIONING_SERVERIMAGE=gitea.siegeln.net/cameleer/cameleer-server:latest
CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE=gitea.siegeln.net/cameleer/cameleer-server-ui:latest
EOF
# Render the new templates
cd ../templates
docker compose --env-file /tmp/test-saas.env config
```
Expected: A fully resolved compose with all 5 services (traefik, postgres, clickhouse, logto, saas), correct environment variables, and the monitoring noop network.
- [ ] **Step 2: Test standalone mode rendering**
```bash
cat > /tmp/test-standalone.env << 'EOF'
COMPOSE_FILE=docker-compose.yml:docker-compose.server.yml
VERSION=latest
PUBLIC_HOST=test.example.com
PUBLIC_PROTOCOL=https
HTTP_PORT=80
HTTPS_PORT=443
POSTGRES_IMAGE=postgres:16-alpine
POSTGRES_USER=cameleer
POSTGRES_PASSWORD=testpass
POSTGRES_DB=cameleer
CLICKHOUSE_PASSWORD=testpass
SERVER_ADMIN_USER=admin
SERVER_ADMIN_PASS=testpass
BOOTSTRAP_TOKEN=testtoken
DOCKER_SOCKET=/var/run/docker.sock
DOCKER_GID=0
EOF
cd ../templates
docker compose --env-file /tmp/test-standalone.env config
```
Expected: 5 services (traefik, postgres with `postgres:16-alpine` image, clickhouse, server, server-ui). Postgres `POSTGRES_DB` should be `cameleer`. Server should have all env vars resolved.
- [ ] **Step 3: Test with TLS + monitoring overlays**
```bash
cat > /tmp/test-full.env << 'EOF'
COMPOSE_FILE=docker-compose.yml:docker-compose.saas.yml:docker-compose.tls.yml:docker-compose.monitoring.yml
VERSION=latest
PUBLIC_HOST=test.example.com
PUBLIC_PROTOCOL=https
HTTP_PORT=80
HTTPS_PORT=443
LOGTO_CONSOLE_PORT=3002
LOGTO_CONSOLE_BIND=0.0.0.0
POSTGRES_USER=cameleer
POSTGRES_PASSWORD=testpass
POSTGRES_DB=cameleer_saas
CLICKHOUSE_PASSWORD=testpass
SAAS_ADMIN_USER=admin
SAAS_ADMIN_PASS=testpass
NODE_TLS_REJECT=0
DOCKER_SOCKET=/var/run/docker.sock
DOCKER_GID=0
MONITORING_NETWORK=prometheus
CAMELEER_SAAS_PROVISIONING_SERVERIMAGE=gitea.siegeln.net/cameleer/cameleer-server:latest
CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE=gitea.siegeln.net/cameleer/cameleer-server-ui:latest
EOF
cd ../templates
docker compose --env-file /tmp/test-full.env config
```
Expected: Same as SaaS mode but with `./certs:/user-certs:ro` volume on traefik and the `monitoring` network declared as `external: true` with name `prometheus`.
- [ ] **Step 4: Clean up temp files**
```bash
rm -f /tmp/test-saas.env /tmp/test-standalone.env /tmp/test-full.env
```
- [ ] **Step 5: Commit verification results as a note (optional)**
No code changes — this task is verification only. If all checks pass, proceed to the final commit.
---
### Task 10: Final commit — update CLAUDE.md deployment modes table
**Files:**
- Modify: `CLAUDE.md` (update Deployment Modes section to reference template files)
- [ ] **Step 1: Update the deployment modes documentation**
In the "Deployment Modes (installer)" section of CLAUDE.md, add a note about the template-based approach:
After the deployment modes table, add:
```markdown
The installer uses static docker-compose templates in `installer/templates/`. Templates are copied to the install directory and composed via `COMPOSE_FILE` in `.env`:
- `docker-compose.yml` — shared infrastructure (traefik, postgres, clickhouse)
- `docker-compose.saas.yml` — SaaS mode (logto, cameleer-saas)
- `docker-compose.server.yml` — standalone mode (server, server-ui)
- `docker-compose.tls.yml` — overlay: custom TLS cert volume
- `docker-compose.monitoring.yml` — overlay: external monitoring network
```
- [ ] **Step 2: Commit**
```bash
git add CLAUDE.md
git commit -m "docs: update CLAUDE.md with template-based installer architecture"
```

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.

View File

@@ -1,33 +0,0 @@
# Cameleer SaaS Configuration
# Generated by installer v1.0.0 on 2026-04-15 08:55:30 UTC
VERSION=latest
PUBLIC_HOST=desktop-fb5vgj9.siegeln.internal
PUBLIC_PROTOCOL=https
HTTP_PORT=80
HTTPS_PORT=443
LOGTO_CONSOLE_PORT=3002
# PostgreSQL
POSTGRES_USER=cameleer
POSTGRES_PASSWORD=dwnyYXj3bVe6kFcOHERr57SkrkD9476a
POSTGRES_DB=cameleer_saas
# ClickHouse
CLICKHOUSE_PASSWORD=SshXE61qZqB1kVoZpQLbr2mDYokw1ZgJ
# Admin user
SAAS_ADMIN_USER=admin
SAAS_ADMIN_PASS=1J3TrbgIYZbxjav1K14uy5DX8nil6Bdi
# TLS
NODE_TLS_REJECT=0
# Docker
DOCKER_SOCKET=/var/run/docker.sock
DOCKER_GID=0
# Provisioning images
CAMELEER_SAAS_PROVISIONING_SERVERIMAGE=gitea.siegeln.net/cameleer/cameleer-server:latest
CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE=gitea.siegeln.net/cameleer/cameleer-server-ui:latest

View File

@@ -1,95 +0,0 @@
# Cameleer SaaS -- Installation Documentation
## Installation Summary
| | |
|---|---|
| **Version** | latest |
| **Date** | 2026-04-15 08:55:55 UTC |
| **Installer** | v1.0.0 |
| **Install Directory** | C:\Users\Hendrik\Documents\projects\cameleer-saas\installer\cameleer |
| **Hostname** | desktop-fb5vgj9.siegeln.internal |
| **TLS** | Self-signed (auto-generated) |
## Service URLs
- **Platform UI:** https://desktop-fb5vgj9.siegeln.internal/platform/
- **API Endpoint:** https://desktop-fb5vgj9.siegeln.internal/platform/api/
- **Logto Admin Console:** https://desktop-fb5vgj9.siegeln.internal:3002
## First Steps
1. Open the Platform UI in your browser
2. Log in as admin with the credentials from `credentials.txt`
3. Create tenants from the admin console
4. The platform will provision a dedicated server instance for each tenant
## Architecture
| Container | Purpose |
|---|---|
| `traefik` | Reverse proxy, TLS termination, routing |
| `postgres` | PostgreSQL database (SaaS + Logto + tenant schemas) |
| `clickhouse` | Time-series storage (traces, metrics, logs) |
| `logto` | OIDC identity provider + bootstrap |
| `cameleer-saas` | SaaS platform (Spring Boot + React) |
Per-tenant `cameleer-server` and `cameleer-server-ui` containers are provisioned dynamically.
## Networking
| Port | Service |
|---|---|
| 80 | HTTP (redirects to HTTPS) |
| 443 | HTTPS (main entry point) |
| 3002 | Logto Admin Console |
## TLS
**Mode:** Self-signed (auto-generated)
The platform generated a self-signed certificate on first boot. To replace it:
1. Log in as admin and navigate to **Certificates** in the admin console
2. Upload your certificate and key via the UI
3. Activate the new certificate (zero-downtime swap)
## Data & Backups
| Docker Volume | Contains |
|---|---|
| `cameleer-pgdata` | PostgreSQL data (tenants, licenses, audit) |
| `cameleer-chdata` | ClickHouse data (traces, metrics, logs) |
| `cameleer-certs` | TLS certificates |
| `cameleer-bootstrapdata` | Logto bootstrap results |
### Backup Commands
```bash
docker compose -p cameleer-saas exec cameleer-postgres pg_dump -U cameleer cameleer_saas > backup.sql
docker compose -p cameleer-saas exec cameleer-clickhouse clickhouse-client --query "SELECT * FROM cameleer.traces FORMAT Native" > traces.native
```
## Upgrading
```powershell
.\install.ps1 -InstallDir C:\Users\Hendrik\Documents\projects\cameleer-saas\installer\cameleer -Version NEW_VERSION
```
## Troubleshooting
| Issue | Command |
|---|---|
| Service not starting | `docker compose -p cameleer-saas logs SERVICE_NAME` |
| Bootstrap failed | `docker compose -p cameleer-saas logs cameleer-logto` |
| Routing issues | `docker compose -p cameleer-saas logs cameleer-traefik` |
| Database issues | `docker compose -p cameleer-saas exec cameleer-postgres psql -U cameleer -d cameleer_saas` |
## Uninstalling
```powershell
Set-Location C:\Users\Hendrik\Documents\projects\cameleer-saas\installer\cameleer
docker compose -p cameleer-saas down
docker compose -p cameleer-saas down -v
Remove-Item -Recurse -Force C:\Users\Hendrik\Documents\projects\cameleer-saas\installer\cameleer
```

View File

@@ -1,18 +0,0 @@
# Cameleer installation config
# Generated by installer v1.0.0 on 2026-04-15 08:55:30 UTC
install_dir=C:\Users\Hendrik\Documents\projects\cameleer-saas\installer\cameleer
public_host=desktop-fb5vgj9.siegeln.internal
public_protocol=https
admin_user=admin
tls_mode=self-signed
http_port=80
https_port=443
logto_console_port=3002
logto_console_exposed=true
monitoring_network=
version=latest
compose_project=cameleer-saas
docker_socket=/var/run/docker.sock
node_tls_reject=0
deployment_mode=saas

View File

@@ -1,16 +0,0 @@
===========================================
CAMELEER PLATFORM CREDENTIALS
Generated: 2026-04-15 08:55:55 UTC
SECURE THIS FILE AND DELETE AFTER NOTING
THESE CREDENTIALS CANNOT BE RECOVERED
===========================================
Admin Console: https://desktop-fb5vgj9.siegeln.internal/platform/
Admin User: admin
Admin Password: 1J3TrbgIYZbxjav1K14uy5DX8nil6Bdi
PostgreSQL: cameleer / dwnyYXj3bVe6kFcOHERr57SkrkD9476a
ClickHouse: default / SshXE61qZqB1kVoZpQLbr2mDYokw1ZgJ
Logto Console: https://desktop-fb5vgj9.siegeln.internal:3002

View File

@@ -607,26 +607,37 @@ BOOTSTRAP_TOKEN=$bt
# Docker
DOCKER_SOCKET=$($c.DockerSocket)
DOCKER_GID=$gid
POSTGRES_IMAGE=postgres:16-alpine
"@
if ($c.TlsMode -eq 'custom') {
$content += "`nCERT_FILE=/user-certs/cert.pem"
$content += "`nKEY_FILE=/user-certs/key.pem"
if ($c.CaFile) { $content += "`nCA_FILE=/user-certs/ca.pem" }
}
$composeFile = 'docker-compose.yml:docker-compose.server.yml'
if ($c.TlsMode -eq 'custom') { $composeFile += ':docker-compose.tls.yml' }
if ($c.MonitoringNetwork) { $composeFile += ':docker-compose.monitoring.yml' }
$content += "`n`n# Compose file assembly`nCOMPOSE_FILE=$composeFile"
if ($c.MonitoringNetwork) {
$content += "`n`n# Monitoring`nMONITORING_NETWORK=$($c.MonitoringNetwork)"
}
} else {
$consoleBind = if ($c.LogtoConsoleExposed -eq 'true') { '0.0.0.0' } else { '127.0.0.1' }
$content = @"
# Cameleer SaaS Configuration
# Generated by installer v${CAMELEER_INSTALLER_VERSION} on $ts
VERSION=$($c.Version)
PUBLIC_HOST=$($c.PublicHost)
PUBLIC_PROTOCOL=$($c.PublicProtocol)
HTTP_PORT=$($c.HttpPort)
HTTPS_PORT=$($c.HttpsPort)
LOGTO_CONSOLE_PORT=$($c.LogtoConsolePort)
LOGTO_CONSOLE_BIND=$consoleBind
# PostgreSQL
POSTGRES_USER=cameleer
POSTGRES_PASSWORD=$($c.PostgresPassword)
@@ -658,6 +669,13 @@ CAMELEER_SAAS_PROVISIONING_SERVERIMAGE=${REGISTRY}/cameleer-server:$($c.Version)
CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE=${REGISTRY}/cameleer-server-ui:$($c.Version)
"@
$content += $provisioningBlock
$composeFile = 'docker-compose.yml:docker-compose.saas.yml'
if ($c.TlsMode -eq 'custom') { $composeFile += ':docker-compose.tls.yml' }
if ($c.MonitoringNetwork) { $composeFile += ':docker-compose.monitoring.yml' }
$content += "`n`n# Compose file assembly`nCOMPOSE_FILE=$composeFile"
if ($c.MonitoringNetwork) {
$content += "`n`n# Monitoring`nMONITORING_NETWORK=$($c.MonitoringNetwork)"
}
}
Write-Utf8File $f $content
@@ -665,443 +683,33 @@ CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE=${REGISTRY}/cameleer-server-ui:$($c.Ver
Log-Info 'Generated .env'
}
# --- Docker Compose generation ---
# Rule: '@ and "@ closing delimiters must ALWAYS be alone at column 0 on their own line.
# --- Copy docker-compose templates ---
function Generate-ComposeFile {
$c = $script:cfg
if ($c.DeploymentMode -eq 'standalone') { Generate-ComposeFileStandalone; return }
$f = Join-Path $c.InstallDir 'docker-compose.yml'
$gid = Get-DockerGid $c.DockerSocket
$out = New-Object System.Collections.Generic.List[string]
$out.Add(@'
# Cameleer SaaS Platform
# Generated by Cameleer installer -- do not edit manually
services:
cameleer-traefik:
image: ${TRAEFIK_IMAGE:-gitea.siegeln.net/cameleer/cameleer-traefik}:${VERSION:-latest}
restart: unless-stopped
ports:
- "${HTTP_PORT:-80}:80"
- "${HTTPS_PORT:-443}:443"
'@
)
if ($c.LogtoConsoleExposed -eq 'true') {
$out.Add(' - "${LOGTO_CONSOLE_PORT:-3002}:3002"')
}
$out.Add(@'
environment:
PUBLIC_HOST: ${PUBLIC_HOST:-localhost}
CERT_FILE: ${CERT_FILE:-}
KEY_FILE: ${KEY_FILE:-}
CA_FILE: ${CA_FILE:-}
volumes:
- cameleer-certs:/certs
- ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock:ro
'@
)
if ($c.TlsMode -eq 'custom') { $out.Add(' - ./certs:/user-certs:ro') }
$out.Add(@'
networks:
- cameleer
- cameleer-traefik
'@
)
if ($c.MonitoringNetwork) {
$out.Add(" - $($c.MonitoringNetwork)")
$out.Add(@'
labels:
- "prometheus.io/scrape=true"
- "prometheus.io/port=8082"
- "prometheus.io/path=/metrics"
'@
)
}
$out.Add(@'
cameleer-postgres:
image: ${POSTGRES_IMAGE:-gitea.siegeln.net/cameleer/cameleer-postgres}:${VERSION:-latest}
restart: unless-stopped
environment:
POSTGRES_DB: cameleer_saas
POSTGRES_USER: ${POSTGRES_USER:-cameleer}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- cameleer-pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER:-cameleer} -d cameleer_saas"]
interval: 5s
timeout: 5s
retries: 5
networks:
- cameleer
'@
)
if ($c.MonitoringNetwork) { $out.Add(" - $($c.MonitoringNetwork)") }
$out.Add(@'
cameleer-clickhouse:
image: ${CLICKHOUSE_IMAGE:-gitea.siegeln.net/cameleer/cameleer-clickhouse}:${VERSION:-latest}
restart: unless-stopped
environment:
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD}
volumes:
- cameleer-chdata:/var/lib/clickhouse
healthcheck:
test: ["CMD-SHELL", "clickhouse-client --password $${CLICKHOUSE_PASSWORD} --query 'SELECT 1'"]
interval: 10s
timeout: 5s
retries: 3
networks:
- cameleer
'@
)
if ($c.MonitoringNetwork) {
$out.Add(" - $($c.MonitoringNetwork)")
$out.Add(@'
labels:
- "prometheus.io/scrape=true"
- "prometheus.io/port=9363"
- "prometheus.io/path=/metrics"
'@
)
}
$out.Add(@'
cameleer-logto:
image: ${LOGTO_IMAGE:-gitea.siegeln.net/cameleer/cameleer-logto}:${VERSION:-latest}
restart: unless-stopped
depends_on:
cameleer-postgres:
condition: service_healthy
environment:
DB_URL: postgres://${POSTGRES_USER:-cameleer}:${POSTGRES_PASSWORD}@cameleer-postgres:5432/logto
ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}
ADMIN_ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}:${LOGTO_CONSOLE_PORT:-3002}
TRUST_PROXY_HEADER: 1
NODE_TLS_REJECT_UNAUTHORIZED: "${NODE_TLS_REJECT:-0}"
LOGTO_ENDPOINT: http://cameleer-logto:3001
LOGTO_ADMIN_ENDPOINT: http://cameleer-logto:3002
LOGTO_PUBLIC_ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}
PUBLIC_HOST: ${PUBLIC_HOST:-localhost}
PUBLIC_PROTOCOL: ${PUBLIC_PROTOCOL:-https}
PG_HOST: cameleer-postgres
PG_USER: ${POSTGRES_USER:-cameleer}
PG_PASSWORD: ${POSTGRES_PASSWORD}
PG_DB_SAAS: cameleer_saas
SAAS_ADMIN_USER: ${SAAS_ADMIN_USER:-admin}
SAAS_ADMIN_PASS: ${SAAS_ADMIN_PASS:?SAAS_ADMIN_PASS must be set in .env}
healthcheck:
test: ["CMD-SHELL", "node -e \"require('http').get('http://localhost:3001/oidc/.well-known/openid-configuration', r => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))\" && test -f /data/logto-bootstrap.json"]
interval: 10s
timeout: 5s
retries: 60
start_period: 30s
labels:
- traefik.enable=true
- traefik.http.routers.cameleer-logto.rule=PathPrefix(`/`)
- traefik.http.routers.cameleer-logto.priority=1
- traefik.http.routers.cameleer-logto.entrypoints=websecure
- traefik.http.routers.cameleer-logto.tls=true
- traefik.http.routers.cameleer-logto.service=cameleer-logto
- traefik.http.routers.cameleer-logto.middlewares=cameleer-logto-cors
- "traefik.http.middlewares.cameleer-logto-cors.headers.accessControlAllowOriginList=${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}:${LOGTO_CONSOLE_PORT:-3002}"
- traefik.http.middlewares.cameleer-logto-cors.headers.accessControlAllowMethods=GET,POST,PUT,PATCH,DELETE,OPTIONS
- traefik.http.middlewares.cameleer-logto-cors.headers.accessControlAllowHeaders=Authorization,Content-Type
- traefik.http.middlewares.cameleer-logto-cors.headers.accessControlAllowCredentials=true
- traefik.http.services.cameleer-logto.loadbalancer.server.port=3001
'@
)
if ($c.LogtoConsoleExposed -eq 'true') {
$out.Add(@'
- traefik.http.routers.cameleer-logto-console.rule=PathPrefix(`/`)
- traefik.http.routers.cameleer-logto-console.entrypoints=admin-console
- traefik.http.routers.cameleer-logto-console.tls=true
- traefik.http.routers.cameleer-logto-console.service=cameleer-logto-console
- traefik.http.services.cameleer-logto-console.loadbalancer.server.port=3002
'@
)
}
$out.Add(@'
volumes:
- cameleer-bootstrapdata:/data
networks:
- cameleer
cameleer-saas:
image: ${CAMELEER_IMAGE:-gitea.siegeln.net/cameleer/cameleer-saas}:${VERSION:-latest}
restart: unless-stopped
depends_on:
cameleer-logto:
condition: service_healthy
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://cameleer-postgres:5432/cameleer_saas
SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER:-cameleer}
SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD}
CAMELEER_SAAS_IDENTITY_LOGTOENDPOINT: http://cameleer-logto:3001
CAMELEER_SAAS_IDENTITY_LOGTOPUBLICENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}
CAMELEER_SAAS_PROVISIONING_PUBLICHOST: ${PUBLIC_HOST:-localhost}
CAMELEER_SAAS_PROVISIONING_PUBLICPROTOCOL: ${PUBLIC_PROTOCOL:-https}
CAMELEER_SAAS_PROVISIONING_NETWORKNAME: ${COMPOSE_PROJECT_NAME:-cameleer-saas}_cameleer
CAMELEER_SAAS_PROVISIONING_TRAEFIKNETWORK: cameleer-traefik
CAMELEER_SAAS_PROVISIONING_DATASOURCEUSERNAME: ${POSTGRES_USER:-cameleer}
CAMELEER_SAAS_PROVISIONING_DATASOURCEPASSWORD: ${POSTGRES_PASSWORD}
CAMELEER_SAAS_PROVISIONING_CLICKHOUSEPASSWORD: ${CLICKHOUSE_PASSWORD}
CAMELEER_SAAS_PROVISIONING_SERVERIMAGE: ${CAMELEER_SAAS_PROVISIONING_SERVERIMAGE:-gitea.siegeln.net/cameleer/cameleer-server:latest}
CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE: ${CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE:-gitea.siegeln.net/cameleer/cameleer-server-ui:latest}
labels:
- traefik.enable=true
- traefik.http.routers.saas.rule=PathPrefix(`/platform`)
- traefik.http.routers.saas.entrypoints=websecure
- traefik.http.routers.saas.tls=true
- traefik.http.services.saas.loadbalancer.server.port=8080
'@
)
if ($c.MonitoringNetwork) {
$out.Add(@'
- "prometheus.io/scrape=true"
- "prometheus.io/port=8080"
- "prometheus.io/path=/platform/actuator/prometheus"
'@
)
}
$out.Add(@'
volumes:
- cameleer-bootstrapdata:/data/bootstrap:ro
- cameleer-certs:/certs
- ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock
networks:
- cameleer
'@
)
if ($c.MonitoringNetwork) { $out.Add(" - $($c.MonitoringNetwork)") }
$out.Add(" group_add:")
$out.Add(" - `"$gid`"")
$out.Add(@'
volumes:
cameleer-pgdata:
cameleer-chdata:
cameleer-certs:
cameleer-bootstrapdata:
networks:
cameleer:
driver: bridge
cameleer-traefik:
name: cameleer-traefik
driver: bridge
'@
)
if ($c.MonitoringNetwork) {
$out.Add(" $($c.MonitoringNetwork):")
$out.Add(' external: true')
}
Write-Utf8File $f ($out -join "`n")
Log-Info 'Generated docker-compose.yml'
}
function Generate-ComposeFileStandalone {
function Copy-Templates {
$c = $script:cfg
$f = Join-Path $c.InstallDir 'docker-compose.yml'
$gid = Get-DockerGid $c.DockerSocket
$out = New-Object System.Collections.Generic.List[string]
$out.Add(@'
# Cameleer Server (standalone)
# Generated by Cameleer installer -- do not edit manually
services:
cameleer-traefik:
image: ${TRAEFIK_IMAGE:-gitea.siegeln.net/cameleer/cameleer-traefik}:${VERSION:-latest}
restart: unless-stopped
ports:
- "${HTTP_PORT:-80}:80"
- "${HTTPS_PORT:-443}:443"
environment:
PUBLIC_HOST: ${PUBLIC_HOST:-localhost}
CERT_FILE: ${CERT_FILE:-}
KEY_FILE: ${KEY_FILE:-}
CA_FILE: ${CA_FILE:-}
volumes:
- cameleer-certs:/certs
- ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock:ro
- ./traefik-dynamic.yml:/etc/traefik/dynamic.yml:ro
'@
)
if ($c.TlsMode -eq 'custom') { $out.Add(' - ./certs:/user-certs:ro') }
$out.Add(@'
networks:
- cameleer
- cameleer-traefik
'@
)
if ($c.MonitoringNetwork) { $out.Add(" - $($c.MonitoringNetwork)") }
$out.Add(@'
cameleer-postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB:-cameleer}
POSTGRES_USER: ${POSTGRES_USER:-cameleer}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- cameleer-pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER:-cameleer} -d $${POSTGRES_DB:-cameleer}"]
interval: 5s
timeout: 5s
retries: 5
networks:
- cameleer
'@
)
if ($c.MonitoringNetwork) { $out.Add(" - $($c.MonitoringNetwork)") }
$out.Add(@'
cameleer-clickhouse:
image: ${CLICKHOUSE_IMAGE:-gitea.siegeln.net/cameleer/cameleer-clickhouse}:${VERSION:-latest}
restart: unless-stopped
environment:
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD}
volumes:
- cameleer-chdata:/var/lib/clickhouse
healthcheck:
test: ["CMD-SHELL", "clickhouse-client --password $${CLICKHOUSE_PASSWORD} --query 'SELECT 1'"]
interval: 10s
timeout: 5s
retries: 3
networks:
- cameleer
'@
)
if ($c.MonitoringNetwork) { $out.Add(" - $($c.MonitoringNetwork)") }
# Server block: double-quoted so $gid expands; compose ${VAR} uses backtick-dollar
$serverBlock = @"
cameleer-server:
image: `${SERVER_IMAGE:-gitea.siegeln.net/cameleer/cameleer-server}:`${VERSION:-latest}
container_name: cameleer-server
restart: unless-stopped
depends_on:
cameleer-postgres:
condition: service_healthy
environment:
CAMELEER_SERVER_TENANT_ID: default
SPRING_DATASOURCE_URL: jdbc:postgresql://cameleer-postgres:5432/`${POSTGRES_DB:-cameleer}?currentSchema=tenant_default
SPRING_DATASOURCE_USERNAME: `${POSTGRES_USER:-cameleer}
SPRING_DATASOURCE_PASSWORD: `${POSTGRES_PASSWORD}
CAMELEER_SERVER_CLICKHOUSE_URL: jdbc:clickhouse://cameleer-clickhouse:8123/cameleer
CAMELEER_SERVER_CLICKHOUSE_USERNAME: default
CAMELEER_SERVER_CLICKHOUSE_PASSWORD: `${CLICKHOUSE_PASSWORD}
CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN: `${BOOTSTRAP_TOKEN}
CAMELEER_SERVER_SECURITY_UIUSER: `${SERVER_ADMIN_USER:-admin}
CAMELEER_SERVER_SECURITY_UIPASSWORD: `${SERVER_ADMIN_PASS:?SERVER_ADMIN_PASS must be set in .env}
CAMELEER_SERVER_SECURITY_CORSALLOWEDORIGINS: `${PUBLIC_PROTOCOL:-https}://`${PUBLIC_HOST:-localhost}
CAMELEER_SERVER_RUNTIME_ENABLED: "true"
CAMELEER_SERVER_RUNTIME_SERVERURL: http://cameleer-server:8081
CAMELEER_SERVER_RUNTIME_ROUTINGDOMAIN: `${PUBLIC_HOST:-localhost}
CAMELEER_SERVER_RUNTIME_ROUTINGMODE: path
CAMELEER_SERVER_RUNTIME_JARSTORAGEPATH: /data/jars
CAMELEER_SERVER_RUNTIME_DOCKERNETWORK: cameleer-apps
CAMELEER_SERVER_RUNTIME_JARDOCKERVOLUME: cameleer-jars
CAMELEER_SERVER_RUNTIME_BASEIMAGE: gitea.siegeln.net/cameleer/cameleer-runtime-base:`${VERSION:-latest}
labels:
- traefik.enable=true
- traefik.http.routers.server-api.rule=PathPrefix(``/api``)
- traefik.http.routers.server-api.entrypoints=websecure
- traefik.http.routers.server-api.tls=true
- traefik.http.services.server-api.loadbalancer.server.port=8081
- traefik.docker.network=cameleer-traefik
healthcheck:
test: ["CMD-SHELL", "curl -sf http://localhost:8081/api/v1/health || exit 1"]
interval: 10s
timeout: 5s
retries: 30
start_period: 30s
volumes:
- jars:/data/jars
- cameleer-certs:/certs:ro
- `${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock
group_add:
- "$gid"
networks:
- cameleer
- cameleer-traefik
- cameleer-apps
cameleer-server-ui:
image: `${SERVER_UI_IMAGE:-gitea.siegeln.net/cameleer/cameleer-server-ui}:`${VERSION:-latest}
restart: unless-stopped
depends_on:
cameleer-server:
condition: service_healthy
environment:
CAMELEER_API_URL: http://cameleer-server:8081
BASE_PATH: ""
labels:
- traefik.enable=true
- traefik.http.routers.ui.rule=PathPrefix(``/``)
- traefik.http.routers.ui.priority=1
- traefik.http.routers.ui.entrypoints=websecure
- traefik.http.routers.ui.tls=true
- traefik.http.services.ui.loadbalancer.server.port=80
- traefik.docker.network=cameleer-traefik
networks:
- cameleer-traefik
"@
$out.Add($serverBlock)
$out.Add(@'
volumes:
cameleer-pgdata:
cameleer-chdata:
cameleer-certs:
jars:
networks:
cameleer:
driver: bridge
cameleer-traefik:
name: cameleer-traefik
driver: bridge
cameleer-apps:
name: cameleer-apps
driver: bridge
'@
)
if ($c.MonitoringNetwork) {
$out.Add(" $($c.MonitoringNetwork):")
$out.Add(' external: true')
$src = Join-Path $PSScriptRoot 'templates'
# Base infra -- always copied
Copy-Item (Join-Path $src 'docker-compose.yml') (Join-Path $c.InstallDir 'docker-compose.yml') -Force
Copy-Item (Join-Path $src '.env.example') (Join-Path $c.InstallDir '.env.example') -Force
# Mode-specific
if ($c.DeploymentMode -eq 'standalone') {
Copy-Item (Join-Path $src 'docker-compose.server.yml') (Join-Path $c.InstallDir 'docker-compose.server.yml') -Force
Copy-Item (Join-Path $src 'traefik-dynamic.yml') (Join-Path $c.InstallDir 'traefik-dynamic.yml') -Force
} else {
Copy-Item (Join-Path $src 'docker-compose.saas.yml') (Join-Path $c.InstallDir 'docker-compose.saas.yml') -Force
}
Write-Utf8File $f ($out -join "`n")
$traefikDyn = @'
tls:
stores:
default:
defaultCertificate:
certFile: /certs/cert.pem
keyFile: /certs/key.pem
'@
Write-Utf8File (Join-Path $c.InstallDir 'traefik-dynamic.yml') $traefikDyn
Log-Info 'Generated docker-compose.yml (standalone)'
# Optional overlays
if ($c.TlsMode -eq 'custom') {
Copy-Item (Join-Path $src 'docker-compose.tls.yml') (Join-Path $c.InstallDir 'docker-compose.tls.yml') -Force
}
if ($c.MonitoringNetwork) {
Copy-Item (Join-Path $src 'docker-compose.monitoring.yml') (Join-Path $c.InstallDir 'docker-compose.monitoring.yml') -Force
}
Log-Info "Copied docker-compose templates to $($c.InstallDir)"
}
# --- Docker operations ---
@@ -1713,7 +1321,7 @@ function Handle-Rerun {
Merge-Config
$script:cfg.InstallDir = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath(
$script:cfg.InstallDir)
Generate-ComposeFile
Copy-Templates
Invoke-ComposePull
Invoke-ComposeDown
Invoke-ComposeUp
@@ -1743,7 +1351,7 @@ function Handle-Rerun {
docker compose -p $proj down -v 2>$null
} catch {}
finally { Pop-Location }
foreach ($fname in @('.env','.env.bak','docker-compose.yml','cameleer.conf','credentials.txt','INSTALL.md','traefik-dynamic.yml')) {
foreach ($fname in @('.env','.env.bak','.env.example','docker-compose.yml','docker-compose.saas.yml','docker-compose.server.yml','docker-compose.tls.yml','docker-compose.monitoring.yml','traefik-dynamic.yml','cameleer.conf','credentials.txt','INSTALL.md')) {
$fp = Join-Path $c.InstallDir $fname
if (Test-Path $fp) { Remove-Item $fp -Force }
}
@@ -1795,7 +1403,7 @@ function Main {
if ($script:cfg.TlsMode -eq 'custom') { Copy-Certs }
Generate-EnvFile
Generate-ComposeFile
Copy-Templates
Write-ConfigFile
Invoke-ComposePull

View File

@@ -603,12 +603,22 @@ BOOTSTRAP_TOKEN=$(generate_password)
# Docker
DOCKER_SOCKET=${DOCKER_SOCKET}
DOCKER_GID=$(stat -c '%g' "${DOCKER_SOCKET}" 2>/dev/null || echo "0")
POSTGRES_IMAGE=postgres:16-alpine
# Compose file assembly
COMPOSE_FILE=docker-compose.yml:docker-compose.server.yml$([ "$TLS_MODE" = "custom" ] && echo ":docker-compose.tls.yml")$([ -n "$MONITORING_NETWORK" ] && echo ":docker-compose.monitoring.yml")
EOF
if [ "$TLS_MODE" = "custom" ]; then
echo "CERT_FILE=/user-certs/cert.pem" >> "$f"
echo "KEY_FILE=/user-certs/key.pem" >> "$f"
[ -n "$CA_FILE" ] && echo "CA_FILE=/user-certs/ca.pem" >> "$f"
fi
if [ -n "$MONITORING_NETWORK" ]; then
echo "" >> "$f"
echo "# Monitoring" >> "$f"
echo "MONITORING_NETWORK=${MONITORING_NETWORK}" >> "$f"
fi
log_info "Generated .env"
cp "$f" "$INSTALL_DIR/.env.bak"
return
@@ -629,6 +639,7 @@ PUBLIC_PROTOCOL=${PUBLIC_PROTOCOL}
HTTP_PORT=${HTTP_PORT}
HTTPS_PORT=${HTTPS_PORT}
LOGTO_CONSOLE_PORT=${LOGTO_CONSOLE_PORT}
LOGTO_CONSOLE_BIND=$([ "$LOGTO_CONSOLE_EXPOSED" = "true" ] && echo "0.0.0.0" || echo "127.0.0.1")
# PostgreSQL
POSTGRES_USER=cameleer
@@ -665,473 +676,46 @@ DOCKER_GID=$(stat -c '%g' "${DOCKER_SOCKET}" 2>/dev/null || echo "0")
# Provisioning images
CAMELEER_SAAS_PROVISIONING_SERVERIMAGE=${REGISTRY}/cameleer-server:${VERSION}
CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE=${REGISTRY}/cameleer-server-ui:${VERSION}
# Compose file assembly
COMPOSE_FILE=docker-compose.yml:docker-compose.saas.yml$([ "$TLS_MODE" = "custom" ] && echo ":docker-compose.tls.yml")$([ -n "$MONITORING_NETWORK" ] && echo ":docker-compose.monitoring.yml")
EOF
if [ -n "$MONITORING_NETWORK" ]; then
echo "" >> "$f"
echo "# Monitoring" >> "$f"
echo "MONITORING_NETWORK=${MONITORING_NETWORK}" >> "$f"
fi
log_info "Generated .env"
cp "$f" "$INSTALL_DIR/.env.bak"
}
generate_compose_file() {
copy_templates() {
local src
src="$(cd "$(dirname "$0")" && pwd)/templates"
# Base infra — always copied
cp "$src/docker-compose.yml" "$INSTALL_DIR/docker-compose.yml"
cp "$src/.env.example" "$INSTALL_DIR/.env.example"
# Mode-specific
if [ "$DEPLOYMENT_MODE" = "standalone" ]; then
generate_compose_file_standalone
return
fi
local f="$INSTALL_DIR/docker-compose.yml"
: > "$f"
cat >> "$f" << 'EOF'
# Cameleer SaaS Platform
# Generated by Cameleer installer <20> do not edit manually
services:
cameleer-traefik:
image: ${TRAEFIK_IMAGE:-gitea.siegeln.net/cameleer/cameleer-traefik}:${VERSION:-latest}
restart: unless-stopped
ports:
- "${HTTP_PORT:-80}:80"
- "${HTTPS_PORT:-443}:443"
EOF
if [ "$LOGTO_CONSOLE_EXPOSED" = "true" ]; then
cat >> "$f" << 'EOF'
- "${LOGTO_CONSOLE_PORT:-3002}:3002"
EOF
cp "$src/docker-compose.server.yml" "$INSTALL_DIR/docker-compose.server.yml"
cp "$src/traefik-dynamic.yml" "$INSTALL_DIR/traefik-dynamic.yml"
else
cp "$src/docker-compose.saas.yml" "$INSTALL_DIR/docker-compose.saas.yml"
fi
cat >> "$f" << 'EOF'
environment:
PUBLIC_HOST: ${PUBLIC_HOST:-localhost}
CERT_FILE: ${CERT_FILE:-}
KEY_FILE: ${KEY_FILE:-}
CA_FILE: ${CA_FILE:-}
volumes:
- cameleer-certs:/certs
- ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock:ro
EOF
# Optional overlays
if [ "$TLS_MODE" = "custom" ]; then
cat >> "$f" << 'EOF'
- ./certs:/user-certs:ro
EOF
cp "$src/docker-compose.tls.yml" "$INSTALL_DIR/docker-compose.tls.yml"
fi
cat >> "$f" << 'EOF'
networks:
- cameleer
- cameleer-traefik
EOF
if [ -n "$MONITORING_NETWORK" ]; then
echo " - ${MONITORING_NETWORK}" >> "$f"
cat >> "$f" << 'EOF'
labels:
- "prometheus.io/scrape=true"
- "prometheus.io/port=8082"
- "prometheus.io/path=/metrics"
EOF
cp "$src/docker-compose.monitoring.yml" "$INSTALL_DIR/docker-compose.monitoring.yml"
fi
cat >> "$f" << 'EOF'
cameleer-postgres:
image: ${POSTGRES_IMAGE:-gitea.siegeln.net/cameleer/cameleer-postgres}:${VERSION:-latest}
restart: unless-stopped
environment:
POSTGRES_DB: cameleer_saas
POSTGRES_USER: ${POSTGRES_USER:-cameleer}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- cameleer-pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER:-cameleer} -d cameleer_saas"]
interval: 5s
timeout: 5s
retries: 5
networks:
- cameleer
EOF
if [ -n "$MONITORING_NETWORK" ]; then
echo " - ${MONITORING_NETWORK}" >> "$f"
fi
cat >> "$f" << 'EOF'
cameleer-clickhouse:
image: ${CLICKHOUSE_IMAGE:-gitea.siegeln.net/cameleer/cameleer-clickhouse}:${VERSION:-latest}
restart: unless-stopped
environment:
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD}
volumes:
- cameleer-chdata:/var/lib/clickhouse
healthcheck:
test: ["CMD-SHELL", "clickhouse-client --password $${CLICKHOUSE_PASSWORD} --query 'SELECT 1'"]
interval: 10s
timeout: 5s
retries: 3
networks:
- cameleer
EOF
if [ -n "$MONITORING_NETWORK" ]; then
echo " - ${MONITORING_NETWORK}" >> "$f"
cat >> "$f" << 'EOF'
labels:
- "prometheus.io/scrape=true"
- "prometheus.io/port=9363"
- "prometheus.io/path=/metrics"
EOF
fi
cat >> "$f" << 'EOF'
cameleer-logto:
image: ${LOGTO_IMAGE:-gitea.siegeln.net/cameleer/cameleer-logto}:${VERSION:-latest}
restart: unless-stopped
depends_on:
cameleer-postgres:
condition: service_healthy
environment:
DB_URL: postgres://${POSTGRES_USER:-cameleer}:${POSTGRES_PASSWORD}@cameleer-postgres:5432/logto
ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}
ADMIN_ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}:${LOGTO_CONSOLE_PORT:-3002}
TRUST_PROXY_HEADER: 1
NODE_TLS_REJECT_UNAUTHORIZED: "${NODE_TLS_REJECT:-0}"
LOGTO_ENDPOINT: http://cameleer-logto:3001
LOGTO_ADMIN_ENDPOINT: http://cameleer-logto:3002
LOGTO_PUBLIC_ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}
PUBLIC_HOST: ${PUBLIC_HOST:-localhost}
PUBLIC_PROTOCOL: ${PUBLIC_PROTOCOL:-https}
PG_HOST: cameleer-postgres
PG_USER: ${POSTGRES_USER:-cameleer}
PG_PASSWORD: ${POSTGRES_PASSWORD}
PG_DB_SAAS: cameleer_saas
SAAS_ADMIN_USER: ${SAAS_ADMIN_USER:-admin}
SAAS_ADMIN_PASS: ${SAAS_ADMIN_PASS:?SAAS_ADMIN_PASS must be set in .env}
healthcheck:
test: ["CMD-SHELL", "node -e \"require('http').get('http://localhost:3001/oidc/.well-known/openid-configuration', r => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))\" && test -f /data/logto-bootstrap.json"]
interval: 10s
timeout: 5s
retries: 60
start_period: 30s
labels:
- traefik.enable=true
- traefik.http.routers.cameleer-logto.rule=PathPrefix(`/`)
- traefik.http.routers.cameleer-logto.priority=1
- traefik.http.routers.cameleer-logto.entrypoints=websecure
- traefik.http.routers.cameleer-logto.tls=true
- traefik.http.routers.cameleer-logto.service=cameleer-logto
- traefik.http.routers.cameleer-logto.middlewares=cameleer-logto-cors
- "traefik.http.middlewares.cameleer-logto-cors.headers.accessControlAllowOriginList=${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}:${LOGTO_CONSOLE_PORT:-3002}"
- traefik.http.middlewares.cameleer-logto-cors.headers.accessControlAllowMethods=GET,POST,PUT,PATCH,DELETE,OPTIONS
- traefik.http.middlewares.cameleer-logto-cors.headers.accessControlAllowHeaders=Authorization,Content-Type
- traefik.http.middlewares.cameleer-logto-cors.headers.accessControlAllowCredentials=true
- traefik.http.services.cameleer-logto.loadbalancer.server.port=3001
EOF
if [ "$LOGTO_CONSOLE_EXPOSED" = "true" ]; then
cat >> "$f" << 'EOF'
- traefik.http.routers.cameleer-logto-console.rule=PathPrefix(`/`)
- traefik.http.routers.cameleer-logto-console.entrypoints=admin-console
- traefik.http.routers.cameleer-logto-console.tls=true
- traefik.http.routers.cameleer-logto-console.service=cameleer-logto-console
- traefik.http.services.cameleer-logto-console.loadbalancer.server.port=3002
EOF
fi
cat >> "$f" << 'EOF'
volumes:
- cameleer-bootstrapdata:/data
networks:
- cameleer
cameleer-saas:
image: ${CAMELEER_IMAGE:-gitea.siegeln.net/cameleer/cameleer-saas}:${VERSION:-latest}
restart: unless-stopped
depends_on:
cameleer-logto:
condition: service_healthy
environment:
# SaaS database
SPRING_DATASOURCE_URL: jdbc:postgresql://cameleer-postgres:5432/cameleer_saas
SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER:-cameleer}
SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD}
# Identity (Logto)
CAMELEER_SAAS_IDENTITY_LOGTOENDPOINT: http://cameleer-logto:3001
CAMELEER_SAAS_IDENTITY_LOGTOPUBLICENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}
# Provisioning — passed to per-tenant server containers
CAMELEER_SAAS_PROVISIONING_PUBLICHOST: ${PUBLIC_HOST:-localhost}
CAMELEER_SAAS_PROVISIONING_PUBLICPROTOCOL: ${PUBLIC_PROTOCOL:-https}
CAMELEER_SAAS_PROVISIONING_NETWORKNAME: ${COMPOSE_PROJECT_NAME:-cameleer-saas}_cameleer
CAMELEER_SAAS_PROVISIONING_TRAEFIKNETWORK: cameleer-traefik
CAMELEER_SAAS_PROVISIONING_DATASOURCEUSERNAME: ${POSTGRES_USER:-cameleer}
CAMELEER_SAAS_PROVISIONING_DATASOURCEPASSWORD: ${POSTGRES_PASSWORD}
CAMELEER_SAAS_PROVISIONING_CLICKHOUSEPASSWORD: ${CLICKHOUSE_PASSWORD}
CAMELEER_SAAS_PROVISIONING_SERVERIMAGE: ${CAMELEER_SAAS_PROVISIONING_SERVERIMAGE:-gitea.siegeln.net/cameleer/cameleer-server:latest}
CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE: ${CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE:-gitea.siegeln.net/cameleer/cameleer-server-ui:latest}
labels:
- traefik.enable=true
- traefik.http.routers.saas.rule=PathPrefix(`/platform`)
- traefik.http.routers.saas.entrypoints=websecure
- traefik.http.routers.saas.tls=true
- traefik.http.services.saas.loadbalancer.server.port=8080
EOF
if [ -n "$MONITORING_NETWORK" ]; then
cat >> "$f" << 'EOF'
- "prometheus.io/scrape=true"
- "prometheus.io/port=8080"
- "prometheus.io/path=/platform/actuator/prometheus"
EOF
fi
cat >> "$f" << 'EOF'
volumes:
- cameleer-bootstrapdata:/data/bootstrap:ro
- cameleer-certs:/certs
- ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock
networks:
- cameleer
EOF
if [ -n "$MONITORING_NETWORK" ]; then
echo " - ${MONITORING_NETWORK}" >> "$f"
fi
# Detect Docker socket GID for container access
local docker_gid
docker_gid=$(stat -c '%g' "${DOCKER_SOCKET:-/var/run/docker.sock}" 2>/dev/null || echo "0")
cat >> "$f" << EOF
group_add:
- "${docker_gid}"
volumes:
EOF
cat >> "$f" << 'EOF'
cameleer-pgdata:
cameleer-chdata:
cameleer-certs:
cameleer-bootstrapdata:
networks:
cameleer:
driver: bridge
cameleer-traefik:
name: cameleer-traefik
driver: bridge
EOF
if [ -n "$MONITORING_NETWORK" ]; then
cat >> "$f" << EOF
${MONITORING_NETWORK}:
external: true
EOF
fi
log_info "Generated docker-compose.yml"
}
generate_compose_file_standalone() {
local f="$INSTALL_DIR/docker-compose.yml"
: > "$f"
cat >> "$f" << 'COMPOSEEOF'
# Cameleer Server (standalone)
# Generated by Cameleer installer — do not edit manually
services:
cameleer-traefik:
image: ${TRAEFIK_IMAGE:-gitea.siegeln.net/cameleer/cameleer-traefik}:${VERSION:-latest}
restart: unless-stopped
ports:
- "${HTTP_PORT:-80}:80"
- "${HTTPS_PORT:-443}:443"
environment:
PUBLIC_HOST: ${PUBLIC_HOST:-localhost}
CERT_FILE: ${CERT_FILE:-}
KEY_FILE: ${KEY_FILE:-}
CA_FILE: ${CA_FILE:-}
volumes:
- cameleer-certs:/certs
- ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock:ro
- ./traefik-dynamic.yml:/etc/traefik/dynamic.yml:ro
COMPOSEEOF
if [ "$TLS_MODE" = "custom" ]; then
echo " - ./certs:/user-certs:ro" >> "$f"
fi
cat >> "$f" << 'COMPOSEEOF'
networks:
- cameleer
- cameleer-traefik
COMPOSEEOF
if [ -n "$MONITORING_NETWORK" ]; then
echo " - ${MONITORING_NETWORK}" >> "$f"
fi
cat >> "$f" << 'COMPOSEEOF'
cameleer-postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB:-cameleer}
POSTGRES_USER: ${POSTGRES_USER:-cameleer}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- cameleer-pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER:-cameleer} -d $${POSTGRES_DB:-cameleer}"]
interval: 5s
timeout: 5s
retries: 5
networks:
- cameleer
COMPOSEEOF
if [ -n "$MONITORING_NETWORK" ]; then
echo " - ${MONITORING_NETWORK}" >> "$f"
fi
cat >> "$f" << 'COMPOSEEOF'
cameleer-clickhouse:
image: ${CLICKHOUSE_IMAGE:-gitea.siegeln.net/cameleer/cameleer-clickhouse}:${VERSION:-latest}
restart: unless-stopped
environment:
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD}
volumes:
- cameleer-chdata:/var/lib/clickhouse
healthcheck:
test: ["CMD-SHELL", "clickhouse-client --password $${CLICKHOUSE_PASSWORD} --query 'SELECT 1'"]
interval: 10s
timeout: 5s
retries: 3
networks:
- cameleer
COMPOSEEOF
if [ -n "$MONITORING_NETWORK" ]; then
echo " - ${MONITORING_NETWORK}" >> "$f"
fi
# Detect Docker socket GID
local docker_gid
docker_gid=$(stat -c '%g' "${DOCKER_SOCKET:-/var/run/docker.sock}" 2>/dev/null || echo "0")
cat >> "$f" << COMPOSEEOF
cameleer-server:
image: \${SERVER_IMAGE:-gitea.siegeln.net/cameleer/cameleer-server}:\${VERSION:-latest}
container_name: cameleer-server
restart: unless-stopped
depends_on:
cameleer-postgres:
condition: service_healthy
environment:
CAMELEER_SERVER_TENANT_ID: default
SPRING_DATASOURCE_URL: jdbc:postgresql://cameleer-postgres:5432/\${POSTGRES_DB:-cameleer}?currentSchema=tenant_default
SPRING_DATASOURCE_USERNAME: \${POSTGRES_USER:-cameleer}
SPRING_DATASOURCE_PASSWORD: \${POSTGRES_PASSWORD}
CAMELEER_SERVER_CLICKHOUSE_URL: jdbc:clickhouse://cameleer-clickhouse:8123/cameleer
CAMELEER_SERVER_CLICKHOUSE_USERNAME: default
CAMELEER_SERVER_CLICKHOUSE_PASSWORD: \${CLICKHOUSE_PASSWORD}
CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN: \${BOOTSTRAP_TOKEN}
CAMELEER_SERVER_SECURITY_UIUSER: \${SERVER_ADMIN_USER:-admin}
CAMELEER_SERVER_SECURITY_UIPASSWORD: \${SERVER_ADMIN_PASS:?SERVER_ADMIN_PASS must be set in .env}
CAMELEER_SERVER_SECURITY_CORSALLOWEDORIGINS: \${PUBLIC_PROTOCOL:-https}://\${PUBLIC_HOST:-localhost}
CAMELEER_SERVER_RUNTIME_ENABLED: "true"
CAMELEER_SERVER_RUNTIME_SERVERURL: http://cameleer-server:8081
CAMELEER_SERVER_RUNTIME_ROUTINGDOMAIN: \${PUBLIC_HOST:-localhost}
CAMELEER_SERVER_RUNTIME_ROUTINGMODE: path
CAMELEER_SERVER_RUNTIME_JARSTORAGEPATH: /data/jars
CAMELEER_SERVER_RUNTIME_DOCKERNETWORK: cameleer-apps
CAMELEER_SERVER_RUNTIME_JARDOCKERVOLUME: cameleer-jars
CAMELEER_SERVER_RUNTIME_BASEIMAGE: gitea.siegeln.net/cameleer/cameleer-runtime-base:\${VERSION:-latest}
labels:
- traefik.enable=true
- traefik.http.routers.server-api.rule=PathPrefix(\`/api\`)
- traefik.http.routers.server-api.entrypoints=websecure
- traefik.http.routers.server-api.tls=true
- traefik.http.services.server-api.loadbalancer.server.port=8081
- traefik.docker.network=cameleer-traefik
healthcheck:
test: ["CMD-SHELL", "curl -sf http://localhost:8081/api/v1/health || exit 1"]
interval: 10s
timeout: 5s
retries: 30
start_period: 30s
volumes:
- jars:/data/jars
- cameleer-certs:/certs:ro
- \${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock
group_add:
- "${docker_gid}"
networks:
- cameleer
- cameleer-traefik
- cameleer-apps
cameleer-server-ui:
image: \${SERVER_UI_IMAGE:-gitea.siegeln.net/cameleer/cameleer-server-ui}:\${VERSION:-latest}
restart: unless-stopped
depends_on:
cameleer-server:
condition: service_healthy
environment:
CAMELEER_API_URL: http://cameleer-server:8081
BASE_PATH: ""
labels:
- traefik.enable=true
- traefik.http.routers.ui.rule=PathPrefix(\`/\`)
- traefik.http.routers.ui.priority=1
- traefik.http.routers.ui.entrypoints=websecure
- traefik.http.routers.ui.tls=true
- traefik.http.services.ui.loadbalancer.server.port=80
- traefik.docker.network=cameleer-traefik
networks:
- cameleer-traefik
COMPOSEEOF
cat >> "$f" << 'COMPOSEEOF'
volumes:
cameleer-pgdata:
cameleer-chdata:
cameleer-certs:
jars:
networks:
cameleer:
driver: bridge
cameleer-traefik:
name: cameleer-traefik
driver: bridge
cameleer-apps:
name: cameleer-apps
driver: bridge
COMPOSEEOF
if [ -n "$MONITORING_NETWORK" ]; then
cat >> "$f" << EOF
${MONITORING_NETWORK}:
external: true
EOF
fi
# Generate standalone traefik dynamic config (overrides baked-in redirect)
cat > "$INSTALL_DIR/traefik-dynamic.yml" << 'TRAEFIKEOF'
tls:
stores:
default:
defaultCertificate:
certFile: /certs/cert.pem
keyFile: /certs/key.pem
TRAEFIKEOF
log_info "Generated docker-compose.yml (standalone)"
log_info "Copied docker-compose templates to $INSTALL_DIR"
}
# --- Docker operations ---
@@ -1700,7 +1284,7 @@ handle_rerun() {
load_config_file "$INSTALL_DIR/cameleer.conf"
load_env_overrides
merge_config
generate_compose_file
copy_templates
docker_compose_pull
docker_compose_down
docker_compose_up
@@ -1725,9 +1309,12 @@ handle_rerun() {
log_info "Reinstalling..."
docker_compose_down 2>/dev/null || true
(cd "$INSTALL_DIR" && docker compose -p "${COMPOSE_PROJECT:-cameleer-saas}" down -v 2>/dev/null || true)
rm -f "$INSTALL_DIR/.env" "$INSTALL_DIR/docker-compose.yml" \
rm -f "$INSTALL_DIR/.env" "$INSTALL_DIR/.env.bak" "$INSTALL_DIR/.env.example" \
"$INSTALL_DIR/docker-compose.yml" "$INSTALL_DIR/docker-compose.saas.yml" \
"$INSTALL_DIR/docker-compose.server.yml" "$INSTALL_DIR/docker-compose.tls.yml" \
"$INSTALL_DIR/docker-compose.monitoring.yml" "$INSTALL_DIR/traefik-dynamic.yml" \
"$INSTALL_DIR/cameleer.conf" "$INSTALL_DIR/credentials.txt" \
"$INSTALL_DIR/INSTALL.md" "$INSTALL_DIR/.env.bak"
"$INSTALL_DIR/INSTALL.md"
rm -rf "$INSTALL_DIR/certs"
IS_RERUN=false
return
@@ -1788,7 +1375,7 @@ main() {
# Generate configuration files
generate_env_file
generate_compose_file
copy_templates
write_config_file
# Pull and start

View File

@@ -0,0 +1,88 @@
# Cameleer Configuration
# Copy this file to .env and fill in the values.
# The installer generates .env automatically — this file is for reference.
# ============================================================
# Compose file assembly (set by installer)
# ============================================================
# SaaS: docker-compose.yml:docker-compose.saas.yml
# Standalone: docker-compose.yml:docker-compose.server.yml
# Add :docker-compose.tls.yml for custom TLS certificates
# Add :docker-compose.monitoring.yml for external monitoring network
COMPOSE_FILE=docker-compose.yml:docker-compose.saas.yml
# ============================================================
# Image version
# ============================================================
VERSION=latest
# ============================================================
# Public access
# ============================================================
PUBLIC_HOST=localhost
PUBLIC_PROTOCOL=https
# ============================================================
# Ports
# ============================================================
HTTP_PORT=80
HTTPS_PORT=443
# Set to 0.0.0.0 to expose Logto admin console externally (default: localhost only)
# LOGTO_CONSOLE_BIND=0.0.0.0
LOGTO_CONSOLE_PORT=3002
# ============================================================
# PostgreSQL
# ============================================================
POSTGRES_USER=cameleer
POSTGRES_PASSWORD=CHANGE_ME
# SaaS: cameleer_saas, Standalone: cameleer
POSTGRES_DB=cameleer_saas
# ============================================================
# ClickHouse
# ============================================================
CLICKHOUSE_PASSWORD=CHANGE_ME
# ============================================================
# Admin credentials (SaaS mode)
# ============================================================
SAAS_ADMIN_USER=admin
SAAS_ADMIN_PASS=CHANGE_ME
# ============================================================
# Admin credentials (standalone mode)
# ============================================================
# SERVER_ADMIN_USER=admin
# SERVER_ADMIN_PASS=CHANGE_ME
# BOOTSTRAP_TOKEN=CHANGE_ME
# ============================================================
# TLS
# ============================================================
# Set to 1 to reject unauthorized TLS certificates (production)
NODE_TLS_REJECT=0
# Custom TLS certificate paths (inside container, set by installer)
# CERT_FILE=/user-certs/cert.pem
# KEY_FILE=/user-certs/key.pem
# CA_FILE=/user-certs/ca.pem
# ============================================================
# Docker
# ============================================================
DOCKER_SOCKET=/var/run/docker.sock
# GID of the docker socket — detected by installer, used for container group_add
DOCKER_GID=0
# ============================================================
# Provisioning images (SaaS mode only)
# ============================================================
# CAMELEER_SAAS_PROVISIONING_SERVERIMAGE=gitea.siegeln.net/cameleer/cameleer-server:latest
# CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE=gitea.siegeln.net/cameleer/cameleer-server-ui:latest
# ============================================================
# Monitoring (optional)
# ============================================================
# External Docker network name for Prometheus scraping.
# Only needed when docker-compose.monitoring.yml is in COMPOSE_FILE.
# MONITORING_NETWORK=prometheus

View File

@@ -0,0 +1,7 @@
# External monitoring network overlay
# Overrides the noop monitoring bridge with a real external network
networks:
monitoring:
external: true
name: ${MONITORING_NETWORK:?MONITORING_NETWORK must be set in .env}

View File

@@ -1,58 +1,7 @@
# Cameleer SaaS Platform
# Generated by Cameleer installer -- do not edit manually
# Cameleer SaaS — Logto + management plane
# Loaded in SaaS deployment mode
services:
cameleer-traefik:
image: ${TRAEFIK_IMAGE:-gitea.siegeln.net/cameleer/cameleer-traefik}:${VERSION:-latest}
restart: unless-stopped
ports:
- "${HTTP_PORT:-80}:80"
- "${HTTPS_PORT:-443}:443"
- "${LOGTO_CONSOLE_PORT:-3002}:3002"
environment:
PUBLIC_HOST: ${PUBLIC_HOST:-localhost}
CERT_FILE: ${CERT_FILE:-}
KEY_FILE: ${KEY_FILE:-}
CA_FILE: ${CA_FILE:-}
volumes:
- cameleer-certs:/certs
- ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock:ro
networks:
- cameleer
- cameleer-traefik
cameleer-postgres:
image: ${POSTGRES_IMAGE:-gitea.siegeln.net/cameleer/cameleer-postgres}:${VERSION:-latest}
restart: unless-stopped
environment:
POSTGRES_DB: cameleer_saas
POSTGRES_USER: ${POSTGRES_USER:-cameleer}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- cameleer-pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER:-cameleer} -d cameleer_saas"]
interval: 5s
timeout: 5s
retries: 5
networks:
- cameleer
cameleer-clickhouse:
image: ${CLICKHOUSE_IMAGE:-gitea.siegeln.net/cameleer/cameleer-clickhouse}:${VERSION:-latest}
restart: unless-stopped
environment:
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD}
volumes:
- cameleer-chdata:/var/lib/clickhouse
healthcheck:
test: ["CMD-SHELL", "clickhouse-client --password $${CLICKHOUSE_PASSWORD} --query 'SELECT 1'"]
interval: 10s
timeout: 5s
retries: 3
networks:
- cameleer
cameleer-logto:
image: ${LOGTO_IMAGE:-gitea.siegeln.net/cameleer/cameleer-logto}:${VERSION:-latest}
restart: unless-stopped
@@ -104,7 +53,8 @@ services:
- cameleer-bootstrapdata:/data
networks:
- cameleer
- monitoring
cameleer-saas:
image: ${CAMELEER_IMAGE:-gitea.siegeln.net/cameleer/cameleer-saas}:${VERSION:-latest}
restart: unless-stopped
@@ -112,11 +62,14 @@ services:
cameleer-logto:
condition: service_healthy
environment:
# SaaS database
SPRING_DATASOURCE_URL: jdbc:postgresql://cameleer-postgres:5432/cameleer_saas
SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER:-cameleer}
SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD}
# Identity (Logto)
CAMELEER_SAAS_IDENTITY_LOGTOENDPOINT: http://cameleer-logto:3001
CAMELEER_SAAS_IDENTITY_LOGTOPUBLICENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}
# Provisioning — passed to per-tenant server containers
CAMELEER_SAAS_PROVISIONING_PUBLICHOST: ${PUBLIC_HOST:-localhost}
CAMELEER_SAAS_PROVISIONING_PUBLICPROTOCOL: ${PUBLIC_PROTOCOL:-https}
CAMELEER_SAAS_PROVISIONING_NETWORKNAME: ${COMPOSE_PROJECT_NAME:-cameleer-saas}_cameleer
@@ -132,24 +85,22 @@ services:
- traefik.http.routers.saas.entrypoints=websecure
- traefik.http.routers.saas.tls=true
- traefik.http.services.saas.loadbalancer.server.port=8080
- "prometheus.io/scrape=true"
- "prometheus.io/port=8080"
- "prometheus.io/path=/platform/actuator/prometheus"
volumes:
- cameleer-bootstrapdata:/data/bootstrap:ro
- cameleer-certs:/certs
- ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock
group_add:
- "${DOCKER_GID:-0}"
networks:
- cameleer
group_add:
- "0"
- monitoring
volumes:
cameleer-pgdata:
cameleer-chdata:
cameleer-certs:
cameleer-bootstrapdata:
networks:
cameleer:
driver: bridge
cameleer-traefik:
name: cameleer-traefik
driver: bridge
monitoring:
name: cameleer-monitoring-noop

View File

@@ -0,0 +1,97 @@
# Cameleer Server (standalone)
# Loaded in standalone deployment mode
services:
cameleer-traefik:
volumes:
- ./traefik-dynamic.yml:/etc/traefik/dynamic.yml:ro
cameleer-postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: ${POSTGRES_DB:-cameleer}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER:-cameleer} -d $${POSTGRES_DB:-cameleer}"]
cameleer-server:
image: ${SERVER_IMAGE:-gitea.siegeln.net/cameleer/cameleer-server}:${VERSION:-latest}
container_name: cameleer-server
restart: unless-stopped
depends_on:
cameleer-postgres:
condition: service_healthy
environment:
CAMELEER_SERVER_TENANT_ID: default
SPRING_DATASOURCE_URL: jdbc:postgresql://cameleer-postgres:5432/${POSTGRES_DB:-cameleer}?currentSchema=tenant_default
SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER:-cameleer}
SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD}
CAMELEER_SERVER_CLICKHOUSE_URL: jdbc:clickhouse://cameleer-clickhouse:8123/cameleer
CAMELEER_SERVER_CLICKHOUSE_USERNAME: default
CAMELEER_SERVER_CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD}
CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN: ${BOOTSTRAP_TOKEN:?BOOTSTRAP_TOKEN must be set in .env}
CAMELEER_SERVER_SECURITY_UIUSER: ${SERVER_ADMIN_USER:-admin}
CAMELEER_SERVER_SECURITY_UIPASSWORD: ${SERVER_ADMIN_PASS:?SERVER_ADMIN_PASS must be set in .env}
CAMELEER_SERVER_SECURITY_CORSALLOWEDORIGINS: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}
CAMELEER_SERVER_RUNTIME_ENABLED: "true"
CAMELEER_SERVER_RUNTIME_SERVERURL: http://cameleer-server:8081
CAMELEER_SERVER_RUNTIME_ROUTINGDOMAIN: ${PUBLIC_HOST:-localhost}
CAMELEER_SERVER_RUNTIME_ROUTINGMODE: path
CAMELEER_SERVER_RUNTIME_JARSTORAGEPATH: /data/jars
CAMELEER_SERVER_RUNTIME_DOCKERNETWORK: cameleer-apps
CAMELEER_SERVER_RUNTIME_JARDOCKERVOLUME: cameleer-jars
CAMELEER_SERVER_RUNTIME_BASEIMAGE: gitea.siegeln.net/cameleer/cameleer-runtime-base:${VERSION:-latest}
labels:
- traefik.enable=true
- traefik.http.routers.server-api.rule=PathPrefix(`/api`)
- traefik.http.routers.server-api.entrypoints=websecure
- traefik.http.routers.server-api.tls=true
- traefik.http.services.server-api.loadbalancer.server.port=8081
- traefik.docker.network=cameleer-traefik
healthcheck:
test: ["CMD-SHELL", "curl -sf http://localhost:8081/api/v1/health || exit 1"]
interval: 10s
timeout: 5s
retries: 30
start_period: 30s
volumes:
- jars:/data/jars
- cameleer-certs:/certs:ro
- ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock
group_add:
- "${DOCKER_GID:-0}"
networks:
- cameleer
- cameleer-traefik
- cameleer-apps
- monitoring
cameleer-server-ui:
image: ${SERVER_UI_IMAGE:-gitea.siegeln.net/cameleer/cameleer-server-ui}:${VERSION:-latest}
restart: unless-stopped
depends_on:
cameleer-server:
condition: service_healthy
environment:
CAMELEER_API_URL: http://cameleer-server:8081
BASE_PATH: ""
labels:
- traefik.enable=true
- traefik.http.routers.ui.rule=PathPrefix(`/`)
- traefik.http.routers.ui.priority=1
- traefik.http.routers.ui.entrypoints=websecure
- traefik.http.routers.ui.tls=true
- traefik.http.services.ui.loadbalancer.server.port=80
- traefik.docker.network=cameleer-traefik
networks:
- cameleer-traefik
- monitoring
volumes:
jars:
networks:
cameleer-apps:
name: cameleer-apps
driver: bridge
monitoring:
name: cameleer-monitoring-noop

View File

@@ -0,0 +1,7 @@
# Custom TLS certificates overlay
# Adds user-supplied certificate volume to traefik
services:
cameleer-traefik:
volumes:
- ./certs:/user-certs:ro

View File

@@ -0,0 +1,79 @@
# Cameleer Infrastructure
# Shared base — always loaded. Mode-specific services in separate compose files.
services:
cameleer-traefik:
image: ${TRAEFIK_IMAGE:-gitea.siegeln.net/cameleer/cameleer-traefik}:${VERSION:-latest}
restart: unless-stopped
ports:
- "${HTTP_PORT:-80}:80"
- "${HTTPS_PORT:-443}:443"
- "${LOGTO_CONSOLE_BIND:-127.0.0.1}:${LOGTO_CONSOLE_PORT:-3002}:3002"
environment:
PUBLIC_HOST: ${PUBLIC_HOST:-localhost}
CERT_FILE: ${CERT_FILE:-}
KEY_FILE: ${KEY_FILE:-}
CA_FILE: ${CA_FILE:-}
volumes:
- cameleer-certs:/certs
- ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock:ro
labels:
- "prometheus.io/scrape=true"
- "prometheus.io/port=8082"
- "prometheus.io/path=/metrics"
networks:
- cameleer
- cameleer-traefik
- monitoring
cameleer-postgres:
image: ${POSTGRES_IMAGE:-gitea.siegeln.net/cameleer/cameleer-postgres}:${VERSION:-latest}
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB:-cameleer_saas}
POSTGRES_USER: ${POSTGRES_USER:-cameleer}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set in .env}
volumes:
- cameleer-pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER:-cameleer} -d $${POSTGRES_DB:-cameleer_saas}"]
interval: 5s
timeout: 5s
retries: 5
networks:
- cameleer
- monitoring
cameleer-clickhouse:
image: ${CLICKHOUSE_IMAGE:-gitea.siegeln.net/cameleer/cameleer-clickhouse}:${VERSION:-latest}
restart: unless-stopped
environment:
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:?CLICKHOUSE_PASSWORD must be set in .env}
volumes:
- cameleer-chdata:/var/lib/clickhouse
healthcheck:
test: ["CMD-SHELL", "clickhouse-client --password $${CLICKHOUSE_PASSWORD} --query 'SELECT 1'"]
interval: 10s
timeout: 5s
retries: 3
labels:
- "prometheus.io/scrape=true"
- "prometheus.io/port=9363"
- "prometheus.io/path=/metrics"
networks:
- cameleer
- monitoring
volumes:
cameleer-pgdata:
cameleer-chdata:
cameleer-certs:
networks:
cameleer:
driver: bridge
cameleer-traefik:
name: cameleer-traefik
driver: bridge
monitoring:
name: cameleer-monitoring-noop

View File

@@ -0,0 +1,6 @@
tls:
stores:
default:
defaultCertificate:
certFile: /certs/cert.pem
keyFile: /certs/key.pem