services: frontend: build: context: ../ dockerfile: deploy/docker/frontend.Dockerfile args: VITE_THUNDERBOLT_CLOUD_URL: "/v1" VITE_AUTH_MODE: "sso" ports: - "${FRONTEND_PORT:-3000}:8080" depends_on: - backend backend: build: context: ../ dockerfile: deploy/docker/backend.Dockerfile ports: - "${BACKEND_PORT:-8000}:8000" env_file: .env environment: NODE_ENV: production AUTH_MODE: oidc WAITLIST_ENABLED: "false" DATABASE_DRIVER: postgres DATABASE_URL: postgresql://postgres:postgres@postgres:5432/postgres # OIDC_ISSUER must equal KC_HOSTNAME on the keycloak service: it's the value # Keycloak embeds in token `iss` claims and Better Auth validates against. Browser # reaches Keycloak via the host port mapping at localhost:KEYCLOAK_PORT. # OIDC_DISCOVERY_URL lets the backend fetch the discovery document via the internal # Docker hostname (`keycloak:8080`) without depending on host port mapping. # Keycloak's KC_HOSTNAME_BACKCHANNEL_DYNAMIC=true ensures the token_endpoint and # jwks_uri returned in that document also use the internal hostname for backend use. OIDC_ISSUER: http://localhost:${KEYCLOAK_PORT:-8180}/realms/thunderbolt OIDC_DISCOVERY_URL: http://keycloak:8080/realms/thunderbolt/.well-known/openid-configuration OIDC_CLIENT_ID: thunderbolt-app OIDC_CLIENT_SECRET: thunderbolt-enterprise-secret BETTER_AUTH_URL: http://localhost:${FRONTEND_PORT:-3000} BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:?Set BETTER_AUTH_SECRET in .env} APP_URL: http://localhost:${FRONTEND_PORT:-3000} # Includes both the browser-facing IdP origin (issuer host) and the internal # discovery host — Better Auth's SSO plugin validates both against trustedOrigins. TRUSTED_ORIGINS: http://localhost:${FRONTEND_PORT:-3000},http://localhost:${KEYCLOAK_PORT:-8180},http://keycloak:8080 CORS_ORIGINS: http://localhost:${FRONTEND_PORT:-3000} # Unlike OIDC_DISCOVERY_URL above, this URL is echoed to the BROWSER in the # /v1/powersync/token response, so it must be host-reachable — the internal # `powersync:8080` Docker hostname doesn't resolve in the browser, leaving the # SharedWorker stuck offline. Use the host port mapping (localhost:POWERSYNC_PORT). POWERSYNC_URL: http://localhost:${POWERSYNC_PORT:-8080} # Must match the base64-decoded `k` in deploy/config/powersync-config.yaml # AND be >=32 chars (backend/src/config/settings.ts:76). POWERSYNC_JWT_SECRET: enterprise-thunderbolt-powersync-jwt-default-secret POWERSYNC_JWT_KID: enterprise-powersync RATE_LIMIT_ENABLED: "false" depends_on: postgres: condition: service_healthy keycloak: condition: service_healthy extra_hosts: - "host.docker.internal:host-gateway" postgres: image: postgres:17-alpine environment: POSTGRES_USER: postgres POSTGRES_DB: postgres POSTGRES_PASSWORD: postgres # Read by 01-powersync.sh on first init to set the powersync_role password. # Must match the password embedded in the powersync container's PS_PG_URI below. POWERSYNC_DB_PASSWORD: myhighlyrandompassword volumes: - pg_data:/var/lib/postgresql/data - ./docker/postgres-init:/docker-entrypoint-initdb.d command: ["postgres", "-c", "wal_level=logical"] ports: - "${POSTGRES_PORT:-5433}:5432" healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s timeout: 5s retries: 5 powersync: image: journeyapps/powersync-service:latest command: ["start", "-r", "unified"] environment: POWERSYNC_CONFIG_PATH: /config/config.yaml PS_PG_URI: postgresql://powersync_role:myhighlyrandompassword@postgres:5432/postgres PS_STORAGE_URI: postgresql://postgres:postgres@postgres:5432/powersync_storage # Base64 of the JWT secret used by both backend (signs) and powersync (verifies). # The PS_ prefix is required by PowerSync's YAML config loader for !env vars. # Default value below = base64("enterprise-thunderbolt-powersync-jwt-default-secret"), # which matches the default backend POWERSYNC_JWT_SECRET. PS_JWT_KEY_BASE64: ZW50ZXJwcmlzZS10aHVuZGVyYm9sdC1wb3dlcnN5bmMtand0LWRlZmF1bHQtc2VjcmV0 # Label paired with the JWT secret. Must match the backend's POWERSYNC_JWT_KID. PS_JWT_KID: enterprise-powersync volumes: - ./config/powersync-config.yaml:/config/config.yaml ports: - "${POWERSYNC_PORT:-8080}:8080" depends_on: postgres: condition: service_healthy keycloak: image: quay.io/keycloak/keycloak:26.0 command: start-dev --import-realm environment: KC_BOOTSTRAP_ADMIN_USERNAME: admin KC_BOOTSTRAP_ADMIN_PASSWORD: admin KC_HTTP_PORT: "8080" KC_HOSTNAME: http://localhost:${KEYCLOAK_PORT:-8180} KC_HOSTNAME_BACKCHANNEL_DYNAMIC: "true" volumes: - ./config/keycloak-realm.json:/opt/keycloak/data/import/thunderbolt-realm.json ports: - "${KEYCLOAK_PORT:-8180}:8080" healthcheck: test: ["CMD-SHELL", "exec 3<>/dev/tcp/localhost/8080"] interval: 10s timeout: 5s retries: 15 volumes: pg_data: