Skip to content

Coolify Deployment

Where the API token lives, and where it no longer does

Section titled “Where the API token lives, and where it no longer does”
hostcoolify_token in /etc/logrite-ops/config.jsonwhy
devremoved, 2026-09-29no Coolify container since 2026-09-23 and deploy_backend: "console", so it pointed at a dead API. Backed up to config.json.bak-20260929-precoolify (0600).
prodkeptjobsearch is still Coolify-owned, and the fifteen …--preadopt- containers are the rollback path.

Removing it from dev was safe because nothing there reads it any more — status_report and deploy_watcher both branch on deploy_backend, and the watcher now builds its Coolify client on demand rather than in __init__, so the console backend never constructs one at all. An empty token is now a named refusal rather than Authorization: Bearer and an HTTP 401: a 401 reads as “your token is wrong, go and rotate it”, when the truth on dev is “this host has no Coolify, deliberately” — the opposite instruction, and a confidently-phrased wrong diagnosis costs more than an error.

This guide covers deploying the entire Logrite stack on Coolify. Each service is a separate Coolify resource within a single Logrite project. All services should share a Docker network for internal communication.

  • Coolify instance running and accessible
  • GitHub access to Deployd-io organization repos
  • OpenSearch accessible (either on the same VM or as a Coolify service)
  • Domain DNS configured in Cloudflare pointing to the Coolify server
ServiceDomainPort
Frontend (Main)app.logrite.ai3000
Backend (Main)leapfrogapi.deployd.io8080
Analyzer (Main)analyzer.logrite.ai9000
Analyzer (Dev)analyzer-dev.logrite.ai9001
Semantic Searchsearch.logrite.ai9100
AI Serviceaiservice.logrite.ai9300
MCP Servermcp.logrite.ai9500
Webhook Serverwebhook.logrite.ai9999
OpenSearch Dashboardsanalytics.logrite.ai5601
SettingValue
RepoDeployd-io/Logrite-AI
Branchmain
Build PackDockerfile
Port8080
Domainleapfrogapi.deployd.io
Health CheckGET /swagger-ui.html

Persistent Volume (required): mount a host directory at /root/logrite/scans. The backend clones repos there during a scan, and the path is now derived from the project — it is not configurable. Without the mount, checkouts live in the container’s writable layer and are destroyed by every redeploy, so checkout reuse silently never happens and ScanCheckoutSweeper has nothing to sweep. Prod ran without one until 2026-08-27. In Coolify this is Storages → a directory mount (local_file_volumes, is_directory: true); via the API it is POST /api/v1/applications/{uuid}/storages with type=file plus is_directory: true — type=directory is rejected.

SERVER_PORT=8080
SERVER_FORWARD_HEADERS_STRATEGY=FRAMEWORK
SPRING_DATASOURCE_URL=jdbc:postgresql://<DB_HOST>:5432/leapfrog
SPRING_DATASOURCE_USERNAME=deployd
SPRING_DATASOURCE_PASSWORD=<password>
SPRING_DATASOURCE_DRIVER_CLASS_NAME=org.postgresql.Driver
SPRING_DATASOURCE_HIKARI_MINIMUM_IDLE=2
SPRING_DATASOURCE_HIKARI_MAXIMUM_POOL_SIZE=10
SPRING_DATASOURCE_HIKARI_IDLE_TIMEOUT=120000
SPRING_DATASOURCE_HIKARI_CONNECTION_TIMEOUT=300000
SPRING_DATASOURCE_HIKARI_LEAK_DETECTION_THRESHOLD=300000
SPRING_JPA_DATABASE_PLATFORM=org.hibernate.dialect.PostgreSQLDialect
SPRING_JPA_HIBERNATE_DDL_AUTO=update
SPRING_JPA_SHOW_SQL=true
SPRING_JPA_PROPERTIES_HIBERNATE_FORMAT_SQL=true
SPRING_WEBSOCKET_SESSION_TIMEOUT=600000
SPRING_MAIL_DEFAULT_ENCODING=UTF-8
SPRING_MAIL_HOST=smtp.office365.com
SPRING_MAIL_PORT=587
SPRING_MAIL_PASSWORD=<password>
SPRING_MAIL_PROTOCOL=smtp
SPRING_MAIL_TEST_CONNECTION=false
SPRING_MAIL_PROPERTIES_MAIL_SMTP_AUTH=true
SPRING_MAIL_PROPERTIES_MAIL_SMTP_STARTTLS_ENABLE=true
SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_BITBUCKET_CLIENT_ID=<client-id>
SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_BITBUCKET_CLIENT_SECRET=<client-secret>
SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_BITBUCKET_AUTHORIZATION_GRANT_TYPE=authorization_code
SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_BITBUCKET_REDIRECT_URI=https://leapfrogapi.deployd.io/login/oauth2/code/bitbucket
SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_BITBUCKET_CLIENT_NAME=Bitbucket
SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_BITBUCKET_SCOPE=account,repository
SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_BITBUCKET_AUTHORIZATION_URI=https://bitbucket.org/site/oauth2/authorize
SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_BITBUCKET_TOKEN_URI=https://bitbucket.org/site/oauth2/access_token
SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_BITBUCKET_USER_INFO_URI=https://api.bitbucket.org/2.0/user
SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_BITBUCKET_USER_NAME_ATTRIBUTE=username
SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GITHUB_CLIENT_ID=<client-id>
SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GITHUB_CLIENT_SECRET=<client-secret>
SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GITHUB_REDIRECT_URI=https://leapfrogapi.deployd.io/login/oauth2/code/github
SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GITHUB_SCOPE=user:email,read:user,repo,read:org
SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GITHUB_CLIENT_NAME=GitHub
SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GITHUB_AUTHORIZATION_URI=https://github.com/login/oauth/authorize
SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GITHUB_TOKEN_URI=https://github.com/login/oauth/access_token
SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GITHUB_USER_INFO_URI=https://api.github.com/user
SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GITHUB_USER_NAME_ATTRIBUTE=id
SECURITY_JWT_SECRET_KEY=<jwt-secret>
# 4 hours. Sliding refresh re-issues fresh tokens via the X-Refresh-Token
# response header on requests past 75% of the token's lifetime, so active
# users never expire. Idle users are logged out after 4h of no traffic.
SECURITY_JWT_EXPIRATION_TIME=14400000
AI_RATE_LIMIT_INTERVAL_MILLIS=1000
EMAIL_FORGOTPASSWORD=Password Reset Confirmation
EMAIL_SIGNUP=Verify your E-mail ID
EMAIL_ADDMEMBERS=You have been invited!!
EMAIL_NOTIFYADMIN=You have a new request!!
EMAIL_NOTIFYMEMBERS=You have been added!!
EMAIL_REJECTNOTIFICATION=Your request has been rejected!!
SPRINGDOC_API_DOCS_ENABLED=true
SPRINGDOC_SWAGGER_UI_ENABLED=true
SPRINGDOC_SWAGGER_UI_PATH=/swagger-ui.html
COMPANYNAME=Deployd
LOCALURL=https://logriteiq.logrite.ai/api/v1
RESETPASSWORDURL=https://logriteiq.logrite.ai/user
BASEDEVAPPLICATIONPATH=https://logriteiq.logrite.ai/
LOGWRITE_ANALYZEAI_URL=https://analyzer.logrite.ai
CLIENT_CLIENTID=logrite
MASK_VARIABLES=password,accessToken,refreshToken,otp,pin,last4,ssn,ccNumber,ccNbr,creditCardNumber,creditCardNbr

Backend: the toolchain is a prebuilt base image, not an inlined stage

Section titled “Backend: the toolchain is a prebuilt base image, not an inlined stage”

Dockerfile.coolify is what prod builds (dev builds Dockerfile.devdeploy). Its first stage is:

ARG BASE=127.0.0.1:5000/deployd-io/logrite-ai-base:latest
FROM ${BASE} AS toolchain

That base is ~13.5 GB of polyglot toolchain — five JDKs, three Node versions, three PHPs, three .NET SDKs, Kotlin, Scala, Groovy, PowerShell, Zig, Ruby, sbt — which the code-fix flow needs at runtime to compile the repositories a user scans. Dockerfile.base defines it and is the source of truth; ops/notify/prebuild/backend-base.sh builds and publishes it, tagged by that file’s hash so an edit publishes a new tag instead of reusing old contents under a moving name.

Why dev never suffered this. dev’s registry has held logrite-ai-base since 2026-09-03, and ops/notify/prebuild/backend.sh passes it as LOGRITE_BACKEND_BASE. Prod’s registry held only logrite-ai-service, so prod recompiled all 28 toolchains on every deploy. If prod is ever rebuilt, publish the base before the first backend deploy:

Terminal window
sudo bash ops/notify/prebuild/backend-base.sh /path/to/Logrite-AI
curl -s http://127.0.0.1:5000/v2/deployd-io/logrite-ai-base/tags/list # must list a tag

If the registry is empty the deploy fails loudly at pull access denied rather than shipping something stale — the opposite of the warden/shim defect above, and deliberate.

DockerfileToolchainSyncTest enforces the shape: the block between the LOGRITE-TOOLCHAIN markers must be exactly an ARG and a FROM, the default must be the loopback registry, and Dockerfile.base must still exist. It used to assert the inlined copy matched Dockerfile.base; it now asserts there is no copy.

SettingValue
RepoDeployd-io/Logrite-UI
Branchmain
Build PackNixpacks
Port3000
Domainapp.logrite.ai

VITE_ env vars are build-time only — they are baked into the JS bundle. Changing them requires a full rebuild, not just a restart.

⚠️ Coolify gotcha — is_buildtime flag. Coolify stores each env var with two independent flags: is_buildtime (made available to the Nixpacks build container) and is_runtime (injected into the running container). The Coolify UI sometimes creates a new env var with is_buildtime=false by default. For VITE_* vars this is a silent footgun — the running container will show the variable in env, but the built JS bundle won’t have it (Vite inlines from the build container’s process env). After adding any new VITE_* env in the Coolify UI, click the variable row → enable “Available at build time”, then trigger a fresh deploy (not just Restart). To audit from the host: docker exec coolify-db psql -U coolify -d coolify -c "SELECT key, is_buildtime, is_runtime FROM environment_variables WHERE key LIKE 'VITE_%' AND resourceable_id=1 ORDER BY key;" — every row should show is_buildtime=t.

NIXPACKS_NODE_VERSION=22
VITE_API_BASE_URL=https://leapfrogapi.deployd.io
VITE_WS_API_URL=ws://leapfrogapi.deployd.io/ws
VITE_WSS_API_URL=wss://leapfrogapi.deployd.io/ws
VITE_GITHUB_OAUTH_URL=https://leapfrogapi.deployd.io/oauth2/authorization/github
VITE_BITBUCKET_OAUTH_URL=https://leapfrogapi.deployd.io/oauth2/authorization/bitbucket
VITE_OPENSEARCH_URL=https://analytics.logrite.ai
# PROXY_API takes precedence over OPENSEARCH_API. Setting it routes all
# OpenSearch traffic through the backend proxy (JWT validation + service
# credentials). Leave PROXY_API blank for legacy direct access.
VITE_ANALYTICS_PROXY_API=https://leapfrogapi.deployd.io/api/v1/analytics
VITE_ANALYTICS_API=https://analyticsapi.logrite.ai
VITE_OPENSEARCH_DASHBOARD_URL=https://analytics.logrite.ai
# DASHBOARD_PROXY_API takes precedence over OPENSEARCH_DASHBOARD_URL.
# Required — OSD's auth chain only accepts Basic (dashboards-svc) or an OSD
# session cookie; Logrite Bearer JWTs fail with {"ok":false,"err":
# "Authentication Exception"}. The backend proxy injects dashboards-svc
# Basic auth and strips inbound Authorization headers.
VITE_DASHBOARD_PROXY_API=https://leapfrogapi.deployd.io/api/v1/dashboards
VITE_PORT=3000
VITE_HOST=true
VITE_APP_NAME=Logrite
VITE_BUILD_SOURCEMAP=false
VITE_DISABLE_HMR=true
VITE_SEMANTIC_SEARCH_API_URL=https://search.logrite.ai
VITE_AI_SERVICE_API=https://aiservice.logrite.ai
VITE_AI_SERVICE_WS=wss://aiservice.logrite.ai/ws
VITE_ANALYZER_INSTANCE=main
VITE_ANALYZER_PORT=9000
SettingValue
RepoDeployd-io/logrite-analyzer
Branchmain
Build PackDockerfile
Port9000
Domainanalyzer.logrite.ai
Health CheckGET /health

The repo’s Dockerfile defaults to port 8080 internally. Override with the PORT env var.

PORT=9000
NODE_ENV=production
AI_PROVIDER=groq
GROQ_API_KEY=<groq-api-key>
GROQ_MODEL=llama-3.1-8b-instant
SettingValue
RepoDeployd-io/logrite-analyzer
Branchdev
Build PackNixpacks or Dockerfile
Port9001
Domainanalyzer-dev.logrite.ai
Health CheckGET /health
NIXPACKS_NODE_VERSION=22
PORT=9001
NODE_ENV=production
AI_PROVIDER=groq
GROQ_API_KEY=<groq-api-key>
GROQ_MODEL=llama-3.1-8b-instant
SettingValue
RepoDeployd-io/logrite-semantic-api
Branchmain
Build PackNixpacks
Port9100
Domainsearch.logrite.ai
Health CheckGET /health
NIXPACKS_NODE_VERSION=22
PORT=9100
NODE_ENV=production
OPENSEARCH_HOST=<opensearch-host-or-ip>
OPENSEARCH_PORT=9200
OPENSEARCH_PROTOCOL=http
# Set both when the OpenSearch Security Plugin is enabled (Coolify image only).
# Leave blank if OpenSearch has no auth.
OPENSEARCH_USERNAME=
OPENSEARCH_PASSWORD=

AI Service / Cortex (Node.js / TypeScript)

Section titled “AI Service / Cortex (Node.js / TypeScript)”
SettingValue
RepoDeployd-io/logrite-ai-service
Branchmain
Build PackNixpacks
Port9300
Domainaiservice.logrite.ai
Health CheckGET /health
Persistent VolumeMount at ./data/ for SQLite conversations DB
NIXPACKS_NODE_VERSION=22
PORT=9300
NODE_ENV=production
AI_PROVIDER=groq
GROQ_API_KEY=<groq-api-key>
GROQ_MODEL=openai/gpt-oss-120b
GROQ_FALLBACK_MODEL=llama-3.3-70b-versatile
AGENT_MAX_ITERATIONS=10
SEMANTIC_API_URL=https://search.logrite.ai
OPENSEARCH_URL=http://<opensearch-host>:9200
# Set both when Security Plugin is enabled.
OPENSEARCH_USERNAME=
OPENSEARCH_PASSWORD=
BACKEND_API_URL=https://leapfrogapi.deployd.io
ANALYZER_API_URL=https://analyzer.logrite.ai
SQLITE_DB_PATH=./data/conversations.db
TOOL_TIMEOUT_MS=30000
TOOL_CONCURRENCY=3
TOOL_MAX_RETRIES=2
TOOL_RETRY_BASE_DELAY_MS=500
SYSTEM_PROMPT_TIME_GRANULARITY=hour
ENABLE_QUERY_CACHE=true
QUERY_CACHE_SIMILARITY_THRESHOLD=0.85
QUERY_CACHE_TTL_MS=3600000
ANALYTICS_TEMPERATURE=0.0
SEARCH_TEMPERATURE=0.1
EXPLANATION_TEMPERATURE=0.3
CREATIVE_TEMPERATURE=0.7
ENABLE_RESPONSE_VALIDATION=true
MIN_RESPONSE_CONFIDENCE=0.6
ENABLE_TOOL_RESULT_VALIDATION=true
ENABLE_AUTO_FALLBACK=true
MAX_FALLBACK_DEPTH=2
GOOGLE_SEARCH_API_KEY=
GOOGLE_SEARCH_CX_ID=
BRAVE_SEARCH_API_KEY=
SettingValue
RepoDeployd-io/logrite-mcp-server
Branchmain
Build PackDockerfile
Port9500
Domainmcp.logrite.ai
Health CheckGET /health
MCP_PORT=9500
OPENSEARCH_URL=http://<opensearch-host>:9200
# Set both when Security Plugin is enabled.
OPENSEARCH_USERNAME=
OPENSEARCH_PASSWORD=
COMPLIANCE_INDEX=compliance-logs
REQUEST_TIMEOUT=10
SettingValue
RepoDeployd-io/logrite-webhook
Branchmain
Build PackDockerfile
Port9999
Domainwebhook.logrite.ai (or internal only)
Health CheckGET /health
DISCORD_WEBHOOK_URL=<discord-webhook-url>
SERVER_IP=<server-ip>
SERVER_NAME=Logrite Production Server
GITHUB_WEBHOOK_SECRET=<github-webhook-secret>
SettingValue
BuildDockerfile (at /data-prepper/Dockerfile in LogriteMasterControl)
Base imageopensearchproject/data-prepper:2.10.0 (pinned — was :latest and caused restart loops)
Ports21890 (traces), 21891 (logs)
DomainNone (internal only)

No env vars — configured via YAML. Update the OpenSearch sink in pipelines.yaml to point to the correct host before building. Pin major version bumps cautiously — pipelines.yaml schema can change.

For Coolify deployment of OpenSearch itself, use the official images instead of the source-built 3.4.0-SNAPSHOT (no matching tag → restart loops). Dockerfiles live at /root/logrite/opensearch/:

SettingValue
Engine imageopensearchproject/opensearch:3.3.1 (Security Plugin included)
Dashboards imageopensearchproject/opensearch-dashboards:3.3.1 (must match engine minor version)
Engine port9200
Dashboards port5601
HeapOPENSEARCH_JAVA_OPTS=-Xms2g -Xmx2g (~50% of container memory)
Host kernelvm.max_map_count=262144 (set on Coolify host, not in container)
Persistent volume/usr/share/opensearch/data (engine) — required to survive restarts

Engine and Dashboards must run on the same Docker network so Dashboards can reach https://opensearch:9200 by container hostname.

Services must start in dependency order:

1. OpenSearch (critical dependency)
2. Backend, Frontend, Webhook Server (independent)
3. Analyzer (needs Groq API only)
4. Semantic Search (needs OpenSearch)
5. AI Service (needs Semantic Search + Analyzer + OpenSearch + Backend)
6. MCP Server (needs OpenSearch + Backend)
7. Data Prepper (needs OpenSearch)
ServiceMount PathPurpose
Backend/root/logrite/scans/Git repo checkouts for scans — host dir /home/deployd/logrite/scans on both VMs
AI Service./data/SQLite conversation history

Inside a Coolify container, localhost refers to the container itself — not the host machine. Any OPENSEARCH_URL=http://localhost:9200 will fail.

Options:

  1. Use public domains (simplest) — services call each other via https://analyzer.logrite.ai, https://search.logrite.ai, etc. Works without shared networking but adds TLS overhead.

  2. Use host IP — point to http://<server-ip>:9200 for OpenSearch or other host-level services.

  3. Use Coolify internal network — put all services on the same Docker network and use container hostnames. Fastest, no TLS overhead between services.

By default Coolify allows 2 concurrent builds across all services. When multiple repos are pushed at the same time (e.g., Logrite-UI + Logrite-AI + logrite-analyzer), Coolify starts building 2 simultaneously, which can exhaust CPU/RAM and cause failures.

Setting: concurrent_builds = 1 (serial deployments)

This is set directly in the Coolify database — the Coolify UI does not expose this field:

Terminal window
docker exec coolify-db psql -U coolify coolify \
-c "UPDATE server_settings SET concurrent_builds = 1 WHERE server_id = 0 RETURNING concurrent_builds, deployment_queue_limit;"

With concurrent_builds=1, Coolify runs one build at a time. deployment_queue_limit=1 means at most 1 build can wait — so at most 2 are in-flight (1 building + 1 queued). Any further triggers are rejected by Coolify (re-push or trigger via Coolify UI).

To verify the current setting:

Terminal window
docker exec coolify-db psql -U coolify coolify \
-c "SELECT concurrent_builds, deployment_queue_limit FROM server_settings;"

This setting is already applied on the production Logrite Coolify instance.

CI-built images — pull-based deploys (GitHub Actions → GHCR → Coolify)

Section titled “CI-built images — pull-based deploys (GitHub Actions → GHCR → Coolify)”

Pilot: Backend + Frontend (2026-07-22). Instead of Coolify building on the prod box (5–15 min of CPU/RAM contention with live services), each repo carries a standalone .github/workflows/docker-deploy.yml that on every push to main:

  1. builds the service’s Docker image on GitHub runners (buildx, cache-from/to: gha layer caching),
  2. pushes ghcr.io/deployd-io/<service>:latest + :<sha> (org-private packages; GITHUB_TOKEN with packages: write — no PAT needed for pushing),
  3. POST https://webhook.logrite.ai/coolify/deploy with {"app":"<slug>","sha":…} and Authorization: Bearer <COOLIFY_DEPLOY_TOKEN> (GH repo secret).

The logrite-webhook service (commit 7088e0b) exposes that endpoint plus GET /coolify/health (Coolify reachability probe). This hop exists because the Coolify API has no public HTTPS endpoint — the webhook container reaches it internally at http://192.168.200.210:8000 (host-published 0.0.0.0:8000 bind). Webhook env: COOLIFY_DEPLOY_TOKEN, COOLIFY_API_TOKEN, COOLIFY_API_URL, COOLIFY_APP_UUIDS — a JSON map {"backend":"<uuid>","frontend":"<uuid>"}. While an app slug is unmapped the trigger is an inert no-op ({"status":"skipped"}), so CI can run green before cutover. Host copy of the trigger token: /home/victor/.config/coolify/deploy-trigger-token (0600).

Measured cold-build times (no cache): Frontend 1m53s, Backend 4m33s — vs 5–15 min on-box, and the box does zero build work.

Backend reuses the repo’s existing Dockerfile (identical recipe to Coolify’s dockerfile build). Frontend gained a Dockerfile (node:22-alpine, npm ci + vite build → serve dist -s, same serving model as the old nixpacks start command); all VITE_* values are passed as build args in the workflow — they are public browser-bundle config, not secrets.

Cutover per app (after docker login ghcr.io on the host as root — private pulls):

  1. PATCH /api/v1/applications/{uuid} → build_pack: "dockerimage", docker_registry_image_name: "ghcr.io/deployd-io/<service>", docker_registry_image_tag: "latest" (domains/env/mounts/healthchecks are retained — same app, only the source changes).
  2. Map the app slug in the webhook’s COOLIFY_APP_UUIDS env + restart webhook app.
  3. Trigger a deploy and verify health.

Rollback: PATCH build_pack back to dockerfile/nixpacks and redeploy — the git source config is still on the app. Pinning to a known-good build: set docker_registry_image_tag to that commit’s <sha> tag and redeploy (every CI build keeps its :<sha> tag in GHCR).

The backend clones repos under /root/logrite/scans/<workspaceId>/<projectId> during analysis (derived by GitPathRules.canonicalScanPath; the old user-supplied checkoutFolder is ignored). A missing directory is created before cloning, so a 500 here means the mount itself is absent or unwritable — check Storages on the app, not the path.

VITE_ variables are embedded at build time, not runtime. After changing them, you must rebuild the frontend — a restart alone won’t pick up the changes.

A second, easier-to-miss cause: in Coolify, env vars carry independent is_buildtime and is_runtime flags, and a newly-added var is sometimes created with is_buildtime=false. The container will then show the variable in env but the built bundle won’t have it — Vite inlines from the build container’s process env, not the runtime container’s. After adding any new VITE_* env, open the variable row in the Coolify UI and enable “Available at build time”, then trigger a fresh deploy. To audit:

Terminal window
docker exec coolify-db psql -U coolify -d coolify -c \
"SELECT key, is_buildtime, is_runtime FROM environment_variables
WHERE key LIKE 'VITE_%' AND resourceable_id=1 ORDER BY key;"

Symptom of this exact bug: the FE makes a cross-origin call to a *_URL fallback host (e.g. https://analytics.logrite.ai/pxe/...) instead of the intended *_PROXY_API host (e.g. https://leapfrogapi.deployd.io/api/v1/dashboards/...). Inspect the served bundle with grep -oE 'https?://[^"]*' /app/dist/assets/chunk-*.js inside the FE container to confirm what URL got baked in.

The analyzer’s Dockerfile defaults PORT=8080 and EXPOSE 8080. Override with the PORT env var in Coolify (9000 for main, 9001 for dev). Ensure the Coolify “Ports Exposes” field matches.

If using OPENSEARCH_URL=http://localhost:9200, change to the actual host IP or Coolify container hostname. localhost inside a container points to the container itself.

400 Failed to call a function is a Groq API error — the LLM generated an invalid tool call. This is intermittent and not related to Coolify configuration. The AI service retries automatically.