21 Commits

Author SHA1 Message Date
hsiegeln
11dd6a354f feat(installer): add PowerShell installer for Windows
Some checks failed
CI / build (push) Successful in 1m24s
CI / docker (push) Failing after 25s
Mirrors install.sh structure and produces identical output files.
Uses native PowerShell idioms for parameters, prompts, and crypto.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 16:39:24 +02:00
hsiegeln
7f15177310 feat(installer): add main function and complete install.sh
Appends the main() entry point that wires together all installer phases:
arg parsing, config loading, rerun detection, prerequisites, auto-detect,
interactive prompts, config merge/validate, password generation, file
generation, docker pull/up, health verification, and output printing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 16:33:15 +02:00
hsiegeln
b01f6e5109 feat(installer): add re-run, upgrade, and reinstall logic 2026-04-13 16:32:02 +02:00
hsiegeln
8146f072df feat(installer): add output file generation (credentials, INSTALL.md, config) 2026-04-13 16:31:38 +02:00
hsiegeln
f13fd3faf0 feat(installer): add docker operations and health verification 2026-04-13 16:30:53 +02:00
hsiegeln
5e5bc97bf5 feat(installer): add .env and docker-compose.yml generation 2026-04-13 16:30:32 +02:00
hsiegeln
7fc80cad58 feat(installer): add config merge, validation, and password generation 2026-04-13 16:25:34 +02:00
hsiegeln
6eabd0cf2e feat(installer): add interactive prompts for simple and expert modes 2026-04-13 16:25:16 +02:00
hsiegeln
4debee966a feat(installer): add prerequisite checks and auto-detection 2026-04-13 16:24:55 +02:00
hsiegeln
1e348eb8ca feat(installer): add argument parsing and config file handling 2026-04-13 16:24:35 +02:00
hsiegeln
f136502a35 feat(installer): scaffold install.sh with constants and utilities
Creates the installer skeleton (Phase 2, Task 8) with version/registry
constants, color codes, default values, _ENV_* variable capture pattern,
config/state variable declarations, and utility functions (log_*, print_banner,
prompt, prompt_password, prompt_yesno, generate_password).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 16:22:21 +02:00
hsiegeln
bf367b1db7 ci: add builds for cameleer-postgres, cameleer-clickhouse, cameleer-traefik
Update Logto build to use repo root context for bootstrap script access.
2026-04-13 16:20:37 +02:00
hsiegeln
f5165add13 feat: consolidate docker-compose.yml for baked-in images
Remove all bind-mounted config files and init containers. Services
reduced from 7 to 5. All configuration via environment variables.
2026-04-13 16:19:29 +02:00
hsiegeln
ec38d0b1c2 feat: merge bootstrap into cameleer-logto image
Adds logto-entrypoint.sh that seeds DB, starts Logto, waits for health,
runs bootstrap, then keeps Logto running. Eliminates the separate
logto-bootstrap init container.
2026-04-13 16:17:13 +02:00
hsiegeln
6cd82de5f9 fix: update traefik-dynamic.yml cert paths to /certs/
The entrypoint writes certs to /certs/ but the dynamic config
referenced /etc/traefik/certs/. Since both are baked into the image,
align the paths so only one volume mount is needed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 16:15:39 +02:00
hsiegeln
0a0898b2f7 feat: create cameleer-traefik image with cert generation and config baked in
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 16:14:47 +02:00
hsiegeln
6864081550 feat: create cameleer-clickhouse image with init and config baked in
Bakes init.sql, users.xml (with from_env password), and prometheus.xml
into a custom ClickHouse image to eliminate 3 bind-mounted config files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 16:13:06 +02:00
hsiegeln
fe5838b40f feat: create cameleer-postgres image with init script baked in
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 16:12:02 +02:00
hsiegeln
1b57f03973 Add install script implementation plan
18 tasks across 3 phases:
- Phase 1 (Tasks 1-7): Platform image consolidation — bake init
  scripts into cameleer-postgres, cameleer-clickhouse, cameleer-traefik,
  merge bootstrap into cameleer-logto, update compose and CI
- Phase 2 (Tasks 8-17): Bash installer with simple/expert/silent modes,
  config precedence, health verification, idempotent re-run
- Phase 3 (Task 18): PowerShell port for Windows

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 16:08:50 +02:00
hsiegeln
0a06615ae2 Fix spec self-review issues in install script design
Resolve TBD placeholder (Docker minimum versions), clarify TLS cert
flow after traefik-certs init container merge, note Traefik env var
substitution for dynamic config, and document Docker socket path
differences between Linux and Windows.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 15:38:59 +02:00
hsiegeln
16a2ff3174 Add install script design spec
Defines a professional installer for the Cameleer SaaS platform with
dual native scripts (bash + PowerShell), three installation modes
(simple/expert/silent), and a platform simplification that consolidates
7 services into 5 by baking all init logic into Docker images.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 15:37:23 +02:00
21 changed files with 6375 additions and 295 deletions

View File

@@ -1,9 +1,18 @@
# Cameleer SaaS Environment Variables
# Copy to .env and fill in values
# Cameleer SaaS Environment Configuration
# Copy to .env and fill in values for production
# Application version
# Image version
VERSION=latest
# Public access
PUBLIC_HOST=localhost
PUBLIC_PROTOCOL=https
# Ports
HTTP_PORT=80
HTTPS_PORT=443
LOGTO_CONSOLE_PORT=3002
# PostgreSQL
POSTGRES_USER=cameleer
POSTGRES_PASSWORD=change_me_in_production
@@ -12,19 +21,24 @@ POSTGRES_DB=cameleer_saas
# ClickHouse
CLICKHOUSE_PASSWORD=change_me_in_production
# Public domain (used by Traefik, Logto, and SaaS provisioning)
PUBLIC_HOST=localhost
PUBLIC_PROTOCOL=https
# Admin user (created by bootstrap)
SAAS_ADMIN_USER=admin
SAAS_ADMIN_PASS=change_me_in_production
# Logto Identity Provider (infrastructure — used by logto-bootstrap init container)
LOGTO_ENDPOINT=http://logto:3001
LOGTO_DB_PASSWORD=change_me_in_production
# TLS (leave empty for self-signed)
# NODE_TLS_REJECT=0 # Set to 1 when using real certificates
# CERT_FILE=
# KEY_FILE=
# CA_FILE=
# SaaS Identity (Logto M2M credentials — usually auto-provisioned by bootstrap)
CAMELEER_SAAS_IDENTITY_M2MCLIENTID=
CAMELEER_SAAS_IDENTITY_M2MCLIENTSECRET=
CAMELEER_SAAS_IDENTITY_SPACLIENTID=
# Vendor account (optional)
VENDOR_SEED_ENABLED=false
# VENDOR_USER=vendor
# VENDOR_PASS=change_me
# SaaS Provisioning
CAMELEER_SAAS_PROVISIONING_SERVERIMAGE=gitea.siegeln.net/cameleer/cameleer3-server:latest
CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE=gitea.siegeln.net/cameleer/cameleer3-server-ui:latest
# Docker images (override for custom registries)
# TRAEFIK_IMAGE=gitea.siegeln.net/cameleer/cameleer-traefik
# POSTGRES_IMAGE=gitea.siegeln.net/cameleer/cameleer-postgres
# CLICKHOUSE_IMAGE=gitea.siegeln.net/cameleer/cameleer-clickhouse
# LOGTO_IMAGE=gitea.siegeln.net/cameleer/cameleer-logto
# CAMELEER_IMAGE=gitea.siegeln.net/cameleer/cameleer-saas

View File

@@ -139,6 +139,39 @@ jobs:
--cache-from type=registry,ref=gitea.siegeln.net/cameleer/cameleer-logto:buildcache \
--cache-to type=registry,ref=gitea.siegeln.net/cameleer/cameleer-logto:buildcache,mode=max \
--provenance=false \
--push ui/sign-in/
--push .
env:
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
- name: Build and push PostgreSQL image
run: |
TAGS="-t gitea.siegeln.net/cameleer/cameleer-postgres:${{ github.sha }}"
for TAG in $IMAGE_TAGS; do
TAGS="$TAGS -t gitea.siegeln.net/cameleer/cameleer-postgres:$TAG"
done
docker buildx build --platform linux/amd64 \
$TAGS \
--provenance=false \
--push docker/cameleer-postgres/
- name: Build and push ClickHouse image
run: |
TAGS="-t gitea.siegeln.net/cameleer/cameleer-clickhouse:${{ github.sha }}"
for TAG in $IMAGE_TAGS; do
TAGS="$TAGS -t gitea.siegeln.net/cameleer/cameleer-clickhouse:$TAG"
done
docker buildx build --platform linux/amd64 \
$TAGS \
--provenance=false \
--push docker/cameleer-clickhouse/
- name: Build and push Traefik image
run: |
TAGS="-t gitea.siegeln.net/cameleer/cameleer-traefik:${{ github.sha }}"
for TAG in $IMAGE_TAGS; do
TAGS="$TAGS -t gitea.siegeln.net/cameleer/cameleer-traefik:$TAG"
done
docker buildx build --platform linux/amd64 \
$TAGS \
--provenance=false \
--push docker/cameleer-traefik/

View File

@@ -1,58 +1,11 @@
services:
traefik-certs:
image: alpine:latest
restart: "no"
entrypoint: ["sh", "-c"]
command:
- |
if [ -f /certs/cert.pem ]; then
echo "Certs already exist, skipping"
exit 0
fi
# Option 1: User-supplied certificate
if [ -n "$$CERT_FILE" ] && [ -n "$$KEY_FILE" ]; then
apk add --no-cache openssl >/dev/null 2>&1
cp "$$CERT_FILE" /certs/cert.pem
cp "$$KEY_FILE" /certs/key.pem
if [ -n "$$CA_FILE" ]; then
cp "$$CA_FILE" /certs/ca.pem
fi
# Validate: key matches cert
CERT_MOD=$$(openssl x509 -noout -modulus -in /certs/cert.pem 2>/dev/null | md5sum)
KEY_MOD=$$(openssl rsa -noout -modulus -in /certs/key.pem 2>/dev/null | md5sum)
if [ "$$CERT_MOD" != "$$KEY_MOD" ]; then
echo "ERROR: Certificate and key do not match!"
rm -f /certs/cert.pem /certs/key.pem /certs/ca.pem
exit 1
fi
SELF_SIGNED=false
echo "Installed user-supplied certificate"
else
# Option 2: Generate self-signed
apk add --no-cache openssl >/dev/null 2>&1
openssl req -x509 -newkey rsa:4096 \
-keyout /certs/key.pem -out /certs/cert.pem \
-days 365 -nodes \
-subj "/CN=$$PUBLIC_HOST" \
-addext "subjectAltName=DNS:$$PUBLIC_HOST,DNS:*.$$PUBLIC_HOST"
SELF_SIGNED=true
echo "Generated self-signed cert for $$PUBLIC_HOST"
fi
# Write metadata for SaaS app to seed DB
SUBJECT=$$(openssl x509 -noout -subject -in /certs/cert.pem 2>/dev/null | sed 's/subject=//')
FINGERPRINT=$$(openssl x509 -noout -fingerprint -sha256 -in /certs/cert.pem 2>/dev/null | sed 's/.*=//')
NOT_BEFORE=$$(openssl x509 -noout -startdate -in /certs/cert.pem 2>/dev/null | sed 's/notBefore=//')
NOT_AFTER=$$(openssl x509 -noout -enddate -in /certs/cert.pem 2>/dev/null | sed 's/notAfter=//')
HAS_CA=false
[ -f /certs/ca.pem ] && HAS_CA=true
cat > /certs/meta.json <<METAEOF
{"subject":"$$SUBJECT","fingerprint":"$$FINGERPRINT","selfSigned":$$SELF_SIGNED,"hasCa":$$HAS_CA,"notBefore":"$$NOT_BEFORE","notAfter":"$$NOT_AFTER"}
METAEOF
mkdir -p /certs/staged /certs/prev
chmod 775 /certs /certs/staged /certs/prev
chmod 660 /certs/*.pem 2>/dev/null || true
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:-}
@@ -60,28 +13,13 @@ services:
CA_FILE: ${CA_FILE:-}
volumes:
- certs:/certs
traefik:
image: traefik:v3
restart: unless-stopped
depends_on:
traefik-certs:
condition: service_completed_successfully
ports:
- "80:80"
- "443:443"
- "3002:3002"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik.yml:/etc/traefik/traefik.yml:ro
- ./docker/traefik-dynamic.yml:/etc/traefik/dynamic.yml:ro
- certs:/etc/traefik/certs:ro
networks:
- cameleer
- cameleer-traefik
postgres:
image: postgres:16-alpine
image: ${POSTGRES_IMAGE:-gitea.siegeln.net/cameleer/cameleer-postgres}:${VERSION:-latest}
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB:-cameleer_saas}
@@ -89,7 +27,6 @@ services:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-cameleer_dev}
volumes:
- pgdata:/var/lib/postgresql/data
- ./docker/init-databases.sh:/docker-entrypoint-initdb.d/init-databases.sh:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-cameleer} -d ${POSTGRES_DB:-cameleer_saas}"]
interval: 5s
@@ -98,54 +35,37 @@ services:
networks:
- cameleer
clickhouse:
image: ${CLICKHOUSE_IMAGE:-gitea.siegeln.net/cameleer/cameleer-clickhouse}:${VERSION:-latest}
restart: unless-stopped
environment:
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-cameleer_ch}
volumes:
- chdata:/var/lib/clickhouse
healthcheck:
test: ["CMD-SHELL", "clickhouse-client --password ${CLICKHOUSE_PASSWORD:-cameleer_ch} --query 'SELECT 1'"]
interval: 10s
timeout: 5s
retries: 3
labels:
- prometheus.scrape=true
- prometheus.path=/metrics
- prometheus.port=9363
networks:
- cameleer
logto:
image: ${LOGTO_IMAGE:-gitea.siegeln.net/cameleer/cameleer-logto}:${VERSION:-latest}
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
entrypoint: ["sh", "-c", "npm run cli db seed -- --swe && npm start"]
environment:
DB_URL: postgres://${POSTGRES_USER:-cameleer}:${POSTGRES_PASSWORD:-cameleer_dev}@postgres:5432/logto
ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}
ADMIN_ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}:3002
ADMIN_ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}:${LOGTO_CONSOLE_PORT:-3002}
TRUST_PROXY_HEADER: 1
NODE_TLS_REJECT_UNAUTHORIZED: "0" # dev only — accept self-signed cert for internal OIDC discovery
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))\""]
interval: 5s
timeout: 5s
retries: 30
start_period: 15s
labels:
- traefik.enable=true
- traefik.http.routers.logto.rule=PathPrefix(`/`)
- traefik.http.routers.logto.priority=1
- traefik.http.routers.logto.entrypoints=websecure
- traefik.http.routers.logto.tls=true
- traefik.http.routers.logto.service=logto
- traefik.http.routers.logto.middlewares=logto-cors
- traefik.http.middlewares.logto-cors.headers.accessControlAllowOriginList=${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}:3002
- traefik.http.middlewares.logto-cors.headers.accessControlAllowMethods=GET,POST,PUT,PATCH,DELETE,OPTIONS
- traefik.http.middlewares.logto-cors.headers.accessControlAllowHeaders=Authorization,Content-Type
- traefik.http.middlewares.logto-cors.headers.accessControlAllowCredentials=true
- traefik.http.services.logto.loadbalancer.server.port=3001
- traefik.http.routers.logto-console.rule=PathPrefix(`/`)
- traefik.http.routers.logto-console.entrypoints=admin-console
- traefik.http.routers.logto-console.tls=true
- traefik.http.routers.logto-console.service=logto-console
- traefik.http.services.logto-console.loadbalancer.server.port=3002
networks:
- cameleer
logto-bootstrap:
image: postgres:16-alpine
depends_on:
logto:
condition: service_healthy
restart: "no"
entrypoint: ["sh", "/scripts/logto-bootstrap.sh"]
environment:
NODE_TLS_REJECT_UNAUTHORIZED: "${NODE_TLS_REJECT:-0}"
LOGTO_ENDPOINT: http://logto:3001
LOGTO_ADMIN_ENDPOINT: http://logto:3002
LOGTO_PUBLIC_ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}
@@ -157,8 +77,34 @@ services:
PG_DB_SAAS: ${POSTGRES_DB:-cameleer_saas}
SAAS_ADMIN_USER: ${SAAS_ADMIN_USER:-admin}
SAAS_ADMIN_PASS: ${SAAS_ADMIN_PASS:-admin}
VENDOR_SEED_ENABLED: "${VENDOR_SEED_ENABLED:-false}"
VENDOR_USER: ${VENDOR_USER:-vendor}
VENDOR_PASS: ${VENDOR_PASS:-vendor}
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.logto.rule=PathPrefix(`/`)
- traefik.http.routers.logto.priority=1
- traefik.http.routers.logto.entrypoints=websecure
- traefik.http.routers.logto.tls=true
- traefik.http.routers.logto.service=logto
- traefik.http.routers.logto.middlewares=logto-cors
- "traefik.http.middlewares.logto-cors.headers.accessControlAllowOriginList=${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}:${LOGTO_CONSOLE_PORT:-3002}"
- traefik.http.middlewares.logto-cors.headers.accessControlAllowMethods=GET,POST,PUT,PATCH,DELETE,OPTIONS
- traefik.http.middlewares.logto-cors.headers.accessControlAllowHeaders=Authorization,Content-Type
- traefik.http.middlewares.logto-cors.headers.accessControlAllowCredentials=true
- traefik.http.services.logto.loadbalancer.server.port=3001
- traefik.http.routers.logto-console.rule=PathPrefix(`/`)
- traefik.http.routers.logto-console.entrypoints=admin-console
- traefik.http.routers.logto-console.tls=true
- traefik.http.routers.logto-console.service=logto-console
- traefik.http.services.logto-console.loadbalancer.server.port=3002
volumes:
- ./docker/logto-bootstrap.sh:/scripts/logto-bootstrap.sh:ro
- bootstrapdata:/data
networks:
- cameleer
@@ -167,10 +113,8 @@ services:
image: ${CAMELEER_IMAGE:-gitea.siegeln.net/cameleer/cameleer-saas}:${VERSION:-latest}
restart: unless-stopped
depends_on:
postgres:
logto:
condition: service_healthy
logto-bootstrap:
condition: service_completed_successfully
volumes:
- bootstrapdata:/data/bootstrap:ro
- certs:/certs
@@ -193,28 +137,6 @@ services:
networks:
- cameleer
clickhouse:
image: clickhouse/clickhouse-server:latest
restart: unless-stopped
environment:
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-cameleer_ch}
volumes:
- chdata:/var/lib/clickhouse
- ./docker/clickhouse-init.sql:/docker-entrypoint-initdb.d/init.sql:ro
- ./docker/clickhouse-users.xml:/etc/clickhouse-server/users.d/default-user.xml
- ./docker/clickhouse-config.xml:/etc/clickhouse-server/config.d/prometheus.xml:ro
healthcheck:
test: ["CMD-SHELL", "clickhouse-client --password ${CLICKHOUSE_PASSWORD:-cameleer_ch} --query 'SELECT 1'"]
interval: 10s
timeout: 5s
retries: 3
labels:
- prometheus.scrape=true
- prometheus.path=/metrics
- prometheus.port=9363
networks:
- cameleer
networks:
cameleer:
driver: bridge

View File

@@ -0,0 +1,4 @@
FROM clickhouse/clickhouse-server:latest
COPY init.sql /docker-entrypoint-initdb.d/init.sql
COPY users.xml /etc/clickhouse-server/users.d/default-user.xml
COPY prometheus.xml /etc/clickhouse-server/config.d/prometheus.xml

View File

@@ -1,10 +1,16 @@
<clickhouse>
<users>
<default remove="remove">
</default>
<default>
<password from_env="CLICKHOUSE_PASSWORD" />
<profile>default</profile>
<networks>
<ip>::/0</ip>
</networks>
<password from_env="CLICKHOUSE_PASSWORD" />
<quota>default</quota>
<access_management>0</access_management>
</default>
</users>
</clickhouse>

View File

@@ -0,0 +1,41 @@
#!/bin/sh
set -e
echo "[entrypoint] Seeding Logto database..."
npm run cli db seed -- --swe 2>/dev/null || true
echo "[entrypoint] Starting Logto..."
npm start &
LOGTO_PID=$!
echo "[entrypoint] Waiting for Logto to be ready..."
for i in $(seq 1 120); do
if 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))" 2>/dev/null; then
echo "[entrypoint] Logto is ready."
break
fi
if [ "$i" -eq 120 ]; then
echo "[entrypoint] ERROR: Logto not ready after 120s"
exit 1
fi
sleep 1
done
# Run bootstrap if not already done
BOOTSTRAP_FILE="/data/logto-bootstrap.json"
if [ -f "$BOOTSTRAP_FILE" ]; then
CACHED_SECRET=$(jq -r '.m2mClientSecret // empty' "$BOOTSTRAP_FILE" 2>/dev/null)
CACHED_SPA=$(jq -r '.spaClientId // empty' "$BOOTSTRAP_FILE" 2>/dev/null)
if [ -n "$CACHED_SECRET" ] && [ -n "$CACHED_SPA" ]; then
echo "[entrypoint] Bootstrap already complete."
else
echo "[entrypoint] Incomplete bootstrap found, re-running..."
/scripts/logto-bootstrap.sh
fi
else
echo "[entrypoint] Running bootstrap..."
/scripts/logto-bootstrap.sh
fi
echo "[entrypoint] Logto is running (PID $LOGTO_PID)."
wait $LOGTO_PID

View File

@@ -0,0 +1,3 @@
FROM postgres:16-alpine
COPY init-databases.sh /docker-entrypoint-initdb.d/init-databases.sh
RUN chmod +x /docker-entrypoint-initdb.d/init-databases.sh

View File

@@ -0,0 +1,7 @@
FROM traefik:v3
RUN apk add --no-cache openssl
COPY traefik.yml /etc/traefik/traefik.yml
COPY traefik-dynamic.yml /etc/traefik/dynamic.yml
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

View File

@@ -0,0 +1,60 @@
#!/bin/sh
set -e
CERTS_DIR="/certs"
# Skip if certs already exist (idempotent)
if [ ! -f "$CERTS_DIR/cert.pem" ]; then
mkdir -p "$CERTS_DIR"
if [ -n "$CERT_FILE" ] && [ -n "$KEY_FILE" ]; then
# User-supplied certificate
echo "[certs] Installing user-supplied certificate..."
cp "$CERT_FILE" "$CERTS_DIR/cert.pem"
cp "$KEY_FILE" "$CERTS_DIR/key.pem"
if [ -n "$CA_FILE" ]; then
cp "$CA_FILE" "$CERTS_DIR/ca.pem"
fi
# Validate key matches cert
CERT_MOD=$(openssl x509 -noout -modulus -in "$CERTS_DIR/cert.pem" 2>/dev/null | md5sum)
KEY_MOD=$(openssl rsa -noout -modulus -in "$CERTS_DIR/key.pem" 2>/dev/null | md5sum)
if [ "$CERT_MOD" != "$KEY_MOD" ]; then
echo "[certs] ERROR: Certificate and key do not match!"
rm -f "$CERTS_DIR/cert.pem" "$CERTS_DIR/key.pem" "$CERTS_DIR/ca.pem"
exit 1
fi
SELF_SIGNED=false
echo "[certs] Installed user-supplied certificate."
else
# Generate self-signed certificate
HOST="${PUBLIC_HOST:-localhost}"
echo "[certs] Generating self-signed certificate for $HOST..."
openssl req -x509 -newkey rsa:4096 \
-keyout "$CERTS_DIR/key.pem" -out "$CERTS_DIR/cert.pem" \
-days 365 -nodes \
-subj "/CN=$HOST" \
-addext "subjectAltName=DNS:$HOST,DNS:*.$HOST"
SELF_SIGNED=true
echo "[certs] Generated self-signed certificate for $HOST."
fi
# Write metadata for SaaS app to seed DB
SUBJECT=$(openssl x509 -noout -subject -in "$CERTS_DIR/cert.pem" 2>/dev/null | sed 's/subject=//')
FINGERPRINT=$(openssl x509 -noout -fingerprint -sha256 -in "$CERTS_DIR/cert.pem" 2>/dev/null | sed 's/.*=//')
NOT_BEFORE=$(openssl x509 -noout -startdate -in "$CERTS_DIR/cert.pem" 2>/dev/null | sed 's/notBefore=//')
NOT_AFTER=$(openssl x509 -noout -enddate -in "$CERTS_DIR/cert.pem" 2>/dev/null | sed 's/notAfter=//')
HAS_CA=false
[ -f "$CERTS_DIR/ca.pem" ] && HAS_CA=true
cat > "$CERTS_DIR/meta.json" <<METAEOF
{"subject":"$SUBJECT","fingerprint":"$FINGERPRINT","selfSigned":$SELF_SIGNED,"hasCa":$HAS_CA,"notBefore":"$NOT_BEFORE","notAfter":"$NOT_AFTER"}
METAEOF
mkdir -p "$CERTS_DIR/staged" "$CERTS_DIR/prev"
chmod 775 "$CERTS_DIR" "$CERTS_DIR/staged" "$CERTS_DIR/prev"
chmod 660 "$CERTS_DIR"/*.pem 2>/dev/null || true
else
echo "[certs] Certificates already exist, skipping generation."
fi
# Start Traefik
exec traefik "$@"

View File

@@ -20,5 +20,5 @@ tls:
stores:
default:
defaultCertificate:
certFile: /etc/traefik/certs/cert.pem
keyFile: /etc/traefik/certs/key.pem
certFile: /certs/cert.pem
keyFile: /certs/key.pem

View File

@@ -21,4 +21,3 @@ providers:
network: cameleer
file:
filename: /etc/traefik/dynamic.yml

View File

@@ -47,8 +47,14 @@ TRAD_POST_LOGOUT_URIS="[\"${PROTO}://${HOST}\",\"${PROTO}://${HOST}/server\",\"$
log() { echo "[bootstrap] $1"; }
pgpass() { PGPASSWORD="${PG_PASSWORD:-cameleer_dev}"; export PGPASSWORD; }
# Install jq + curl
apk add --no-cache jq curl >/dev/null 2>&1
# Install jq + curl if not already available (deps are baked into cameleer-logto image)
if ! command -v jq >/dev/null 2>&1 || ! command -v curl >/dev/null 2>&1; then
if command -v apk >/dev/null 2>&1; then
apk add --no-cache jq curl >/dev/null 2>&1
elif command -v apt-get >/dev/null 2>&1; then
apt-get update -qq && apt-get install -y -qq jq curl >/dev/null 2>&1
fi
fi
# Read cached secrets from previous run
if [ -f "$BOOTSTRAP_FILE" ]; then

View File

@@ -1,135 +0,0 @@
#!/bin/sh
set -e
# Cameleer SaaS — Vendor Seed Script
# Creates the saas-vendor global role and vendor user.
# Run ONCE on the hosted SaaS environment AFTER standard bootstrap.
# NOT part of docker-compose.yml — invoked manually or by CI.
LOGTO_ENDPOINT="${LOGTO_ENDPOINT:-http://logto:3001}"
MGMT_API_RESOURCE="https://default.logto.app/api"
API_RESOURCE_INDICATOR="https://api.cameleer.local"
PG_HOST="${PG_HOST:-postgres}"
PG_USER="${PG_USER:-cameleer}"
PG_DB_LOGTO="logto"
# Vendor credentials (override via env vars)
VENDOR_USER="${VENDOR_USER:-vendor}"
VENDOR_PASS="${VENDOR_PASS:-vendor}"
VENDOR_NAME="${VENDOR_NAME:-SaaS Vendor}"
log() { echo "[vendor-seed] $1"; }
pgpass() { PGPASSWORD="${PG_PASSWORD:-cameleer_dev}"; export PGPASSWORD; }
# Install jq + curl
apk add --no-cache jq curl >/dev/null 2>&1
# ============================================================
# Get Management API token
# ============================================================
log "Reading M2M credentials from bootstrap file..."
BOOTSTRAP_FILE="/data/logto-bootstrap.json"
if [ ! -f "$BOOTSTRAP_FILE" ]; then
log "ERROR: Bootstrap file not found at $BOOTSTRAP_FILE — run standard bootstrap first"
exit 1
fi
M2M_ID=$(jq -r '.m2mClientId' "$BOOTSTRAP_FILE")
M2M_SECRET=$(jq -r '.m2mClientSecret' "$BOOTSTRAP_FILE")
if [ -z "$M2M_ID" ] || [ "$M2M_ID" = "null" ] || [ -z "$M2M_SECRET" ] || [ "$M2M_SECRET" = "null" ]; then
log "ERROR: M2M credentials not found in bootstrap file"
exit 1
fi
log "Getting Management API token..."
TOKEN_RESPONSE=$(curl -s -X POST "${LOGTO_ENDPOINT}/oidc/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&client_id=${M2M_ID}&client_secret=${M2M_SECRET}&resource=${MGMT_API_RESOURCE}&scope=all")
TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.access_token' 2>/dev/null)
[ -z "$TOKEN" ] || [ "$TOKEN" = "null" ] && { log "ERROR: Failed to get token"; exit 1; }
log "Got Management API token."
api_get() { curl -s -H "Authorization: Bearer $TOKEN" "${LOGTO_ENDPOINT}${1}" 2>/dev/null || echo "[]"; }
api_post() { curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d "$2" "${LOGTO_ENDPOINT}${1}" 2>/dev/null || true; }
# ============================================================
# Create saas-vendor global role
# ============================================================
log "Checking for saas-vendor role..."
EXISTING_ROLES=$(api_get "/api/roles")
VENDOR_ROLE_ID=$(echo "$EXISTING_ROLES" | jq -r '.[] | select(.name == "saas-vendor" and .type == "User") | .id')
if [ -n "$VENDOR_ROLE_ID" ]; then
log "saas-vendor role exists: $VENDOR_ROLE_ID"
else
# Collect all API resource scope IDs
EXISTING_RESOURCES=$(api_get "/api/resources")
API_RESOURCE_ID=$(echo "$EXISTING_RESOURCES" | jq -r ".[] | select(.indicator == \"$API_RESOURCE_INDICATOR\") | .id")
ALL_SCOPE_IDS=$(api_get "/api/resources/$API_RESOURCE_ID/scopes" | jq '[.[].id]')
log "Creating saas-vendor role with all scopes..."
VENDOR_ROLE_RESPONSE=$(api_post "/api/roles" "{
\"name\": \"saas-vendor\",
\"description\": \"SaaS vendor — full platform control across all tenants\",
\"type\": \"User\",
\"scopeIds\": $ALL_SCOPE_IDS
}")
VENDOR_ROLE_ID=$(echo "$VENDOR_ROLE_RESPONSE" | jq -r '.id')
log "Created saas-vendor role: $VENDOR_ROLE_ID"
fi
# ============================================================
# Create vendor user
# ============================================================
log "Checking for vendor user '$VENDOR_USER'..."
VENDOR_USER_ID=$(api_get "/api/users?search=$VENDOR_USER" | jq -r ".[] | select(.username == \"$VENDOR_USER\") | .id")
if [ -n "$VENDOR_USER_ID" ]; then
log "Vendor user exists: $VENDOR_USER_ID"
else
log "Creating vendor user '$VENDOR_USER'..."
VENDOR_RESPONSE=$(api_post "/api/users" "{
\"username\": \"$VENDOR_USER\",
\"password\": \"$VENDOR_PASS\",
\"name\": \"$VENDOR_NAME\"
}")
VENDOR_USER_ID=$(echo "$VENDOR_RESPONSE" | jq -r '.id')
log "Created vendor user: $VENDOR_USER_ID"
fi
# Assign saas-vendor role
if [ -n "$VENDOR_ROLE_ID" ] && [ "$VENDOR_ROLE_ID" != "null" ]; then
api_post "/api/users/$VENDOR_USER_ID/roles" "{\"roleIds\": [\"$VENDOR_ROLE_ID\"]}" >/dev/null 2>&1
log "Assigned saas-vendor role."
fi
# ============================================================
# Add vendor to all existing organizations with owner role
# ============================================================
log "Adding vendor to all organizations..."
ORG_OWNER_ROLE_ID=$(api_get "/api/organization-roles" | jq -r '.[] | select(.name == "owner") | .id')
ORGS=$(api_get "/api/organizations")
ORG_COUNT=$(echo "$ORGS" | jq 'length')
for i in $(seq 0 $((ORG_COUNT - 1))); do
ORG_ID=$(echo "$ORGS" | jq -r ".[$i].id")
ORG_NAME=$(echo "$ORGS" | jq -r ".[$i].name")
api_post "/api/organizations/$ORG_ID/users" "{\"userIds\": [\"$VENDOR_USER_ID\"]}" >/dev/null 2>&1
if [ -n "$ORG_OWNER_ROLE_ID" ] && [ "$ORG_OWNER_ROLE_ID" != "null" ]; then
curl -s -X PUT -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "{\"organizationRoleIds\": [\"$ORG_OWNER_ROLE_ID\"]}" \
"${LOGTO_ENDPOINT}/api/organizations/$ORG_ID/users/$VENDOR_USER_ID/roles" >/dev/null 2>&1
fi
log " Added to org '$ORG_NAME' ($ORG_ID) with owner role."
done
log ""
log "=== Vendor seed complete! ==="
log " Vendor user: $VENDOR_USER / $VENDOR_PASS"
log " Role: saas-vendor (global) + owner (in all orgs)"
log " This user has platform:admin scope and cross-tenant access."

File diff suppressed because it is too large Load Diff

View File

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

1626
installer/install.ps1 Normal file

File diff suppressed because it is too large Load Diff

1372
installer/install.sh Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,28 @@
# syntax=docker/dockerfile:1
# Stage 1: Build custom sign-in UI
FROM --platform=$BUILDPLATFORM node:22-alpine AS build
ARG REGISTRY_TOKEN
WORKDIR /ui
COPY package.json package-lock.json .npmrc ./
COPY ui/sign-in/package.json ui/sign-in/package-lock.json ui/sign-in/.npmrc ./
RUN --mount=type=cache,target=/root/.npm echo "//gitea.siegeln.net/api/packages/cameleer/npm/:_authToken=${REGISTRY_TOKEN}" >> .npmrc && npm ci
COPY . .
COPY ui/sign-in/ .
RUN npm run build
# Stage 2: Logto with sign-in UI + bootstrap
FROM ghcr.io/logto-io/logto:latest
# Install bootstrap dependencies (curl, jq for API calls; postgresql-client for DB reads)
RUN apt-get update && apt-get install -y --no-install-recommends \
curl jq postgresql-client \
&& rm -rf /var/lib/apt/lists/*
# Custom sign-in UI
COPY --from=build /ui/dist/ /etc/logto/packages/experience/dist/
# Bootstrap scripts
COPY docker/logto-bootstrap.sh /scripts/logto-bootstrap.sh
COPY docker/cameleer-logto/logto-entrypoint.sh /scripts/entrypoint.sh
RUN chmod +x /scripts/*.sh
ENTRYPOINT ["/scripts/entrypoint.sh"]