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 </dev/null || true environment: PUBLIC_HOST: ${PUBLIC_HOST:-localhost} CERT_FILE: ${CERT_FILE:-} KEY_FILE: ${KEY_FILE:-} 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 restart: unless-stopped environment: POSTGRES_DB: ${POSTGRES_DB:-cameleer_saas} POSTGRES_USER: ${POSTGRES_USER:-cameleer} 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 timeout: 5s retries: 5 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 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: LOGTO_ENDPOINT: http://logto:3001 LOGTO_ADMIN_ENDPOINT: http://logto:3002 LOGTO_PUBLIC_ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost} PUBLIC_HOST: ${PUBLIC_HOST:-localhost} PUBLIC_PROTOCOL: ${PUBLIC_PROTOCOL:-https} PG_HOST: postgres PG_USER: ${POSTGRES_USER:-cameleer} PG_PASSWORD: ${POSTGRES_PASSWORD:-cameleer_dev} PG_DB_SAAS: ${POSTGRES_DB:-cameleer_saas} SAAS_ADMIN_USER: ${SAAS_ADMIN_USER:-admin} SAAS_ADMIN_PASS: ${SAAS_ADMIN_PASS:-admin} volumes: - ./docker/logto-bootstrap.sh:/scripts/logto-bootstrap.sh:ro - bootstrapdata:/data networks: - cameleer cameleer-saas: image: ${CAMELEER_IMAGE:-gitea.siegeln.net/cameleer/cameleer-saas}:${VERSION:-latest} restart: unless-stopped depends_on: postgres: condition: service_healthy logto-bootstrap: condition: service_completed_successfully volumes: - bootstrapdata:/data/bootstrap:ro - certs:/certs 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} 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:-} 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 networks: - cameleer clickhouse: image: clickhouse/clickhouse-server:latest restart: unless-stopped 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:ro healthcheck: test: ["CMD-SHELL", "clickhouse-client --query 'SELECT 1'"] interval: 10s timeout: 5s retries: 3 networks: - cameleer networks: cameleer: driver: bridge cameleer-traefik: name: cameleer-traefik driver: bridge volumes: pgdata: chdata: certs: bootstrapdata: