2026-03-30 10:13:39 +02:00
|
|
|
services:
|
2026-04-05 18:14:25 +02:00
|
|
|
traefik-certs:
|
|
|
|
|
image: alpine:latest
|
|
|
|
|
restart: "no"
|
|
|
|
|
entrypoint: ["sh", "-c"]
|
|
|
|
|
command:
|
|
|
|
|
- |
|
2026-04-10 18:29:02 +02:00
|
|
|
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
|
2026-04-05 18:14:25 +02:00
|
|
|
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"
|
2026-04-10 18:29:02 +02:00
|
|
|
SELF_SIGNED=true
|
2026-04-05 18:14:25 +02:00
|
|
|
echo "Generated self-signed cert for $$PUBLIC_HOST"
|
|
|
|
|
fi
|
2026-04-10 18:29:02 +02:00
|
|
|
|
|
|
|
|
# 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
|
2026-04-11 08:04:47 +02:00
|
|
|
chmod 775 /certs /certs/staged /certs/prev
|
|
|
|
|
chmod 660 /certs/*.pem 2>/dev/null || true
|
2026-04-05 18:14:25 +02:00
|
|
|
environment:
|
|
|
|
|
PUBLIC_HOST: ${PUBLIC_HOST:-localhost}
|
2026-04-10 18:29:02 +02:00
|
|
|
CERT_FILE: ${CERT_FILE:-}
|
|
|
|
|
KEY_FILE: ${KEY_FILE:-}
|
|
|
|
|
CA_FILE: ${CA_FILE:-}
|
2026-04-05 18:14:25 +02:00
|
|
|
volumes:
|
|
|
|
|
- certs:/certs
|
|
|
|
|
|
2026-04-04 15:09:49 +02:00
|
|
|
traefik:
|
|
|
|
|
image: traefik:v3
|
|
|
|
|
restart: unless-stopped
|
2026-04-05 18:14:25 +02:00
|
|
|
depends_on:
|
|
|
|
|
traefik-certs:
|
|
|
|
|
condition: service_completed_successfully
|
2026-04-04 15:09:49 +02:00
|
|
|
ports:
|
|
|
|
|
- "80:80"
|
|
|
|
|
- "443:443"
|
2026-04-07 00:44:33 +02:00
|
|
|
- "3002:3002"
|
2026-04-04 15:09:49 +02:00
|
|
|
volumes:
|
|
|
|
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
|
|
|
|
- ./traefik.yml:/etc/traefik/traefik.yml:ro
|
2026-04-05 23:30:38 +02:00
|
|
|
- ./docker/traefik-dynamic.yml:/etc/traefik/dynamic.yml:ro
|
2026-04-05 18:14:25 +02:00
|
|
|
- certs:/etc/traefik/certs:ro
|
2026-04-04 15:09:49 +02:00
|
|
|
networks:
|
|
|
|
|
- cameleer
|
2026-04-08 22:37:51 +02:00
|
|
|
- cameleer-traefik
|
2026-04-04 15:09:49 +02:00
|
|
|
|
2026-03-30 10:13:39 +02:00
|
|
|
postgres:
|
|
|
|
|
image: postgres:16-alpine
|
2026-04-04 15:09:49 +02:00
|
|
|
restart: unless-stopped
|
2026-03-30 10:13:39 +02:00
|
|
|
environment:
|
2026-04-04 15:09:49 +02:00
|
|
|
POSTGRES_DB: ${POSTGRES_DB:-cameleer_saas}
|
|
|
|
|
POSTGRES_USER: ${POSTGRES_USER:-cameleer}
|
|
|
|
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-cameleer_dev}
|
2026-03-30 10:13:39 +02:00
|
|
|
volumes:
|
|
|
|
|
- pgdata:/var/lib/postgresql/data
|
2026-04-04 15:09:49 +02:00
|
|
|
- ./docker/init-databases.sh:/docker-entrypoint-initdb.d/init-databases.sh:ro
|
|
|
|
|
healthcheck:
|
2026-04-04 23:38:02 +02:00
|
|
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-cameleer} -d ${POSTGRES_DB:-cameleer_saas}"]
|
2026-04-04 15:09:49 +02:00
|
|
|
interval: 5s
|
|
|
|
|
timeout: 5s
|
|
|
|
|
retries: 5
|
|
|
|
|
networks:
|
|
|
|
|
- cameleer
|
|
|
|
|
|
|
|
|
|
logto:
|
2026-04-06 15:12:11 +02:00
|
|
|
image: ${LOGTO_IMAGE:-gitea.siegeln.net/cameleer/cameleer-logto}:${VERSION:-latest}
|
2026-04-04 15:09:49 +02:00
|
|
|
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
|
2026-04-05 21:10:03 +02:00
|
|
|
ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}
|
2026-04-07 00:44:33 +02:00
|
|
|
ADMIN_ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}:3002
|
2026-04-04 15:09:49 +02:00
|
|
|
TRUST_PROXY_HEADER: 1
|
2026-04-07 00:44:33 +02:00
|
|
|
NODE_TLS_REJECT_UNAUTHORIZED: "0" # dev only — accept self-signed cert for internal OIDC discovery
|
2026-04-05 00:22:22 +02:00
|
|
|
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
|
2026-04-04 15:09:49 +02:00
|
|
|
labels:
|
|
|
|
|
- traefik.enable=true
|
2026-04-05 23:06:41 +02:00
|
|
|
- traefik.http.routers.logto.rule=PathPrefix(`/`)
|
|
|
|
|
- traefik.http.routers.logto.priority=1
|
2026-04-05 18:14:25 +02:00
|
|
|
- traefik.http.routers.logto.entrypoints=websecure
|
|
|
|
|
- traefik.http.routers.logto.tls=true
|
2026-04-07 00:56:10 +02:00
|
|
|
- traefik.http.routers.logto.service=logto
|
2026-04-07 00:54:09 +02:00
|
|
|
- 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
|
2026-04-04 15:09:49 +02:00
|
|
|
- traefik.http.services.logto.loadbalancer.server.port=3001
|
2026-04-07 00:44:33 +02:00
|
|
|
- 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
|
2026-04-04 15:09:49 +02:00
|
|
|
networks:
|
2026-04-07 00:25:18 +02:00
|
|
|
- cameleer
|
2026-04-04 15:09:49 +02:00
|
|
|
|
2026-04-05 00:22:22 +02:00
|
|
|
logto-bootstrap:
|
|
|
|
|
image: postgres:16-alpine
|
|
|
|
|
depends_on:
|
|
|
|
|
logto:
|
|
|
|
|
condition: service_healthy
|
|
|
|
|
restart: "no"
|
|
|
|
|
entrypoint: ["sh", "/scripts/logto-bootstrap.sh"]
|
|
|
|
|
environment:
|
|
|
|
|
LOGTO_ENDPOINT: http://logto:3001
|
|
|
|
|
LOGTO_ADMIN_ENDPOINT: http://logto:3002
|
2026-04-05 21:10:03 +02:00
|
|
|
LOGTO_PUBLIC_ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}
|
2026-04-05 17:07:20 +02:00
|
|
|
PUBLIC_HOST: ${PUBLIC_HOST:-localhost}
|
2026-04-05 18:14:25 +02:00
|
|
|
PUBLIC_PROTOCOL: ${PUBLIC_PROTOCOL:-https}
|
2026-04-05 00:22:22 +02:00
|
|
|
PG_HOST: postgres
|
|
|
|
|
PG_USER: ${POSTGRES_USER:-cameleer}
|
|
|
|
|
PG_PASSWORD: ${POSTGRES_PASSWORD:-cameleer_dev}
|
2026-04-05 02:50:51 +02:00
|
|
|
PG_DB_SAAS: ${POSTGRES_DB:-cameleer_saas}
|
|
|
|
|
SAAS_ADMIN_USER: ${SAAS_ADMIN_USER:-admin}
|
|
|
|
|
SAAS_ADMIN_PASS: ${SAAS_ADMIN_PASS:-admin}
|
2026-04-05 00:22:22 +02:00
|
|
|
volumes:
|
|
|
|
|
- ./docker/logto-bootstrap.sh:/scripts/logto-bootstrap.sh:ro
|
|
|
|
|
- bootstrapdata:/data
|
|
|
|
|
networks:
|
|
|
|
|
- cameleer
|
|
|
|
|
|
2026-04-04 15:09:49 +02:00
|
|
|
cameleer-saas:
|
|
|
|
|
image: ${CAMELEER_IMAGE:-gitea.siegeln.net/cameleer/cameleer-saas}:${VERSION:-latest}
|
|
|
|
|
restart: unless-stopped
|
|
|
|
|
depends_on:
|
|
|
|
|
postgres:
|
|
|
|
|
condition: service_healthy
|
2026-04-05 00:22:22 +02:00
|
|
|
logto-bootstrap:
|
|
|
|
|
condition: service_completed_successfully
|
2026-04-04 15:09:49 +02:00
|
|
|
volumes:
|
2026-04-05 00:22:22 +02:00
|
|
|
- bootstrapdata:/data/bootstrap:ro
|
2026-04-10 18:29:02 +02:00
|
|
|
- certs:/certs
|
2026-04-04 15:09:49 +02:00
|
|
|
environment:
|
|
|
|
|
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/${POSTGRES_DB:-cameleer_saas}
|
|
|
|
|
SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER:-cameleer}
|
|
|
|
|
SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD:-cameleer_dev}
|
2026-04-11 18:11:21 +02:00
|
|
|
CAMELEER_SAAS_IDENTITY_LOGTOENDPOINT: ${LOGTO_ENDPOINT:-http://logto:3001}
|
|
|
|
|
CAMELEER_SAAS_IDENTITY_LOGTOPUBLICENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}
|
|
|
|
|
CAMELEER_SAAS_PROVISIONING_PUBLICPROTOCOL: ${PUBLIC_PROTOCOL:-https}
|
|
|
|
|
CAMELEER_SAAS_PROVISIONING_PUBLICHOST: ${PUBLIC_HOST:-localhost}
|
|
|
|
|
CAMELEER_SAAS_IDENTITY_M2MCLIENTID: ${LOGTO_M2M_CLIENT_ID:-}
|
|
|
|
|
CAMELEER_SAAS_IDENTITY_M2MCLIENTSECRET: ${LOGTO_M2M_CLIENT_SECRET:-}
|
2026-04-04 15:09:49 +02:00
|
|
|
labels:
|
|
|
|
|
- traefik.enable=true
|
2026-04-05 23:06:41 +02:00
|
|
|
- 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
|
2026-04-04 15:09:49 +02:00
|
|
|
networks:
|
|
|
|
|
- cameleer
|
|
|
|
|
|
|
|
|
|
clickhouse:
|
|
|
|
|
image: clickhouse/clickhouse-server:latest
|
|
|
|
|
restart: unless-stopped
|
2026-04-12 13:59:59 +02:00
|
|
|
environment:
|
|
|
|
|
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-cameleer_ch}
|
2026-04-04 15:09:49 +02:00
|
|
|
volumes:
|
|
|
|
|
- chdata:/var/lib/clickhouse
|
2026-04-04 23:37:19 +02:00
|
|
|
- ./docker/clickhouse-init.sql:/docker-entrypoint-initdb.d/init.sql:ro
|
2026-04-12 14:27:35 +02:00
|
|
|
- ./docker/clickhouse-users.xml:/etc/clickhouse-server/users.d/default-user.xml
|
2026-04-12 18:24:08 +02:00
|
|
|
- ./docker/clickhouse-config.xml:/etc/clickhouse-server/config.d/prometheus.xml:ro
|
2026-04-04 15:09:49 +02:00
|
|
|
healthcheck:
|
2026-04-12 13:59:59 +02:00
|
|
|
test: ["CMD-SHELL", "clickhouse-client --password ${CLICKHOUSE_PASSWORD:-cameleer_ch} --query 'SELECT 1'"]
|
2026-04-04 15:09:49 +02:00
|
|
|
interval: 10s
|
|
|
|
|
timeout: 5s
|
|
|
|
|
retries: 3
|
2026-04-12 18:24:08 +02:00
|
|
|
labels:
|
|
|
|
|
- prometheus.scrape=true
|
|
|
|
|
- prometheus.path=/metrics
|
|
|
|
|
- prometheus.port=9363
|
2026-04-04 15:09:49 +02:00
|
|
|
networks:
|
|
|
|
|
- cameleer
|
|
|
|
|
|
|
|
|
|
networks:
|
|
|
|
|
cameleer:
|
|
|
|
|
driver: bridge
|
2026-04-08 22:37:51 +02:00
|
|
|
cameleer-traefik:
|
|
|
|
|
name: cameleer-traefik
|
|
|
|
|
driver: bridge
|
2026-03-30 10:13:39 +02:00
|
|
|
|
|
|
|
|
volumes:
|
|
|
|
|
pgdata:
|
2026-04-04 15:09:49 +02:00
|
|
|
chdata:
|
2026-04-05 18:14:25 +02:00
|
|
|
certs:
|
2026-04-05 00:22:22 +02:00
|
|
|
bootstrapdata:
|