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”| host | coolify_token in /etc/logrite-ops/config.json | why |
|---|---|---|
| dev | removed, 2026-09-29 | no 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). |
| prod | kept | jobsearch 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.
Overview
Section titled “Overview”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.
Prerequisites
Section titled “Prerequisites”- Coolify instance running and accessible
- GitHub access to
Deployd-ioorganization repos - OpenSearch accessible (either on the same VM or as a Coolify service)
- Domain DNS configured in Cloudflare pointing to the Coolify server
Domain Map
Section titled “Domain Map”| Service | Domain | Port |
|---|---|---|
| Frontend (Main) | app.logrite.ai | 3000 |
| Backend (Main) | leapfrogapi.deployd.io | 8080 |
| Analyzer (Main) | analyzer.logrite.ai | 9000 |
| Analyzer (Dev) | analyzer-dev.logrite.ai | 9001 |
| Semantic Search | search.logrite.ai | 9100 |
| AI Service | aiservice.logrite.ai | 9300 |
| MCP Server | mcp.logrite.ai | 9500 |
| Webhook Server | webhook.logrite.ai | 9999 |
| OpenSearch Dashboards | analytics.logrite.ai | 5601 |
Service Configurations
Section titled “Service Configurations”Backend Main (Spring Boot / Java 17)
Section titled “Backend Main (Spring Boot / Java 17)”| Setting | Value |
|---|---|
| Repo | Deployd-io/Logrite-AI |
| Branch | main |
| Build Pack | Dockerfile |
| Port | 8080 |
| Domain | leapfrogapi.deployd.io |
| Health Check | GET /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=8080SERVER_FORWARD_HEADERS_STRATEGY=FRAMEWORKSPRING_DATASOURCE_URL=jdbc:postgresql://<DB_HOST>:5432/leapfrogSPRING_DATASOURCE_USERNAME=deploydSPRING_DATASOURCE_PASSWORD=<password>SPRING_DATASOURCE_DRIVER_CLASS_NAME=org.postgresql.DriverSPRING_DATASOURCE_HIKARI_MINIMUM_IDLE=2SPRING_DATASOURCE_HIKARI_MAXIMUM_POOL_SIZE=10SPRING_DATASOURCE_HIKARI_IDLE_TIMEOUT=120000SPRING_DATASOURCE_HIKARI_CONNECTION_TIMEOUT=300000SPRING_DATASOURCE_HIKARI_LEAK_DETECTION_THRESHOLD=300000SPRING_JPA_DATABASE_PLATFORM=org.hibernate.dialect.PostgreSQLDialectSPRING_JPA_HIBERNATE_DDL_AUTO=updateSPRING_JPA_SHOW_SQL=trueSPRING_JPA_PROPERTIES_HIBERNATE_FORMAT_SQL=trueSPRING_WEBSOCKET_SESSION_TIMEOUT=600000SPRING_MAIL_DEFAULT_ENCODING=UTF-8SPRING_MAIL_HOST=smtp.office365.comSPRING_MAIL_PORT=587SPRING_MAIL_PASSWORD=<password>SPRING_MAIL_PROTOCOL=smtpSPRING_MAIL_TEST_CONNECTION=falseSPRING_MAIL_PROPERTIES_MAIL_SMTP_AUTH=trueSPRING_MAIL_PROPERTIES_MAIL_SMTP_STARTTLS_ENABLE=trueSPRING_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_codeSPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_BITBUCKET_REDIRECT_URI=https://leapfrogapi.deployd.io/login/oauth2/code/bitbucketSPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_BITBUCKET_CLIENT_NAME=BitbucketSPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_BITBUCKET_SCOPE=account,repositorySPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_BITBUCKET_AUTHORIZATION_URI=https://bitbucket.org/site/oauth2/authorizeSPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_BITBUCKET_TOKEN_URI=https://bitbucket.org/site/oauth2/access_tokenSPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_BITBUCKET_USER_INFO_URI=https://api.bitbucket.org/2.0/userSPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_BITBUCKET_USER_NAME_ATTRIBUTE=usernameSPRING_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/githubSPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GITHUB_SCOPE=user:email,read:user,repo,read:orgSPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GITHUB_CLIENT_NAME=GitHubSPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GITHUB_AUTHORIZATION_URI=https://github.com/login/oauth/authorizeSPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GITHUB_TOKEN_URI=https://github.com/login/oauth/access_tokenSPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GITHUB_USER_INFO_URI=https://api.github.com/userSPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_GITHUB_USER_NAME_ATTRIBUTE=idSECURITY_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=14400000AI_RATE_LIMIT_INTERVAL_MILLIS=1000EMAIL_FORGOTPASSWORD=Password Reset ConfirmationEMAIL_SIGNUP=Verify your E-mail IDEMAIL_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=trueSPRINGDOC_SWAGGER_UI_ENABLED=trueSPRINGDOC_SWAGGER_UI_PATH=/swagger-ui.htmlCOMPANYNAME=DeploydLOCALURL=https://logriteiq.logrite.ai/api/v1RESETPASSWORDURL=https://logriteiq.logrite.ai/userBASEDEVAPPLICATIONPATH=https://logriteiq.logrite.ai/LOGWRITE_ANALYZEAI_URL=https://analyzer.logrite.aiCLIENT_CLIENTID=logriteMASK_VARIABLES=password,accessToken,refreshToken,otp,pin,last4,ssn,ccNumber,ccNbr,creditCardNumber,creditCardNbrBackend: 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:latestFROM ${BASE} AS toolchainThat 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:
sudo bash ops/notify/prebuild/backend-base.sh /path/to/Logrite-AIcurl -s http://127.0.0.1:5000/v2/deployd-io/logrite-ai-base/tags/list # must list a tagIf 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.
Frontend Main (React / Vite)
Section titled “Frontend Main (React / Vite)”| Setting | Value |
|---|---|
| Repo | Deployd-io/Logrite-UI |
| Branch | main |
| Build Pack | Nixpacks |
| Port | 3000 |
| Domain | app.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_buildtimeflag. Coolify stores each env var with two independent flags:is_buildtime(made available to the Nixpacks build container) andis_runtime(injected into the running container). The Coolify UI sometimes creates a new env var withis_buildtime=falseby default. ForVITE_*vars this is a silent footgun — the running container will show the variable inenv, but the built JS bundle won’t have it (Vite inlines from the build container’s process env). After adding any newVITE_*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 showis_buildtime=t.
NIXPACKS_NODE_VERSION=22VITE_API_BASE_URL=https://leapfrogapi.deployd.ioVITE_WS_API_URL=ws://leapfrogapi.deployd.io/wsVITE_WSS_API_URL=wss://leapfrogapi.deployd.io/wsVITE_GITHUB_OAUTH_URL=https://leapfrogapi.deployd.io/oauth2/authorization/githubVITE_BITBUCKET_OAUTH_URL=https://leapfrogapi.deployd.io/oauth2/authorization/bitbucketVITE_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/analyticsVITE_ANALYTICS_API=https://analyticsapi.logrite.aiVITE_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/dashboardsVITE_PORT=3000VITE_HOST=trueVITE_APP_NAME=LogriteVITE_BUILD_SOURCEMAP=falseVITE_DISABLE_HMR=trueVITE_SEMANTIC_SEARCH_API_URL=https://search.logrite.aiVITE_AI_SERVICE_API=https://aiservice.logrite.aiVITE_AI_SERVICE_WS=wss://aiservice.logrite.ai/wsVITE_ANALYZER_INSTANCE=mainVITE_ANALYZER_PORT=9000Analyzer Main (Node.js / TypeScript)
Section titled “Analyzer Main (Node.js / TypeScript)”| Setting | Value |
|---|---|
| Repo | Deployd-io/logrite-analyzer |
| Branch | main |
| Build Pack | Dockerfile |
| Port | 9000 |
| Domain | analyzer.logrite.ai |
| Health Check | GET /health |
The repo’s Dockerfile defaults to port 8080 internally. Override with the PORT env var.
PORT=9000NODE_ENV=productionAI_PROVIDER=groqGROQ_API_KEY=<groq-api-key>GROQ_MODEL=llama-3.1-8b-instantAnalyzer Dev (Node.js / TypeScript)
Section titled “Analyzer Dev (Node.js / TypeScript)”| Setting | Value |
|---|---|
| Repo | Deployd-io/logrite-analyzer |
| Branch | dev |
| Build Pack | Nixpacks or Dockerfile |
| Port | 9001 |
| Domain | analyzer-dev.logrite.ai |
| Health Check | GET /health |
NIXPACKS_NODE_VERSION=22PORT=9001NODE_ENV=productionAI_PROVIDER=groqGROQ_API_KEY=<groq-api-key>GROQ_MODEL=llama-3.1-8b-instantSemantic Search (Node.js / TypeScript)
Section titled “Semantic Search (Node.js / TypeScript)”| Setting | Value |
|---|---|
| Repo | Deployd-io/logrite-semantic-api |
| Branch | main |
| Build Pack | Nixpacks |
| Port | 9100 |
| Domain | search.logrite.ai |
| Health Check | GET /health |
NIXPACKS_NODE_VERSION=22PORT=9100NODE_ENV=productionOPENSEARCH_HOST=<opensearch-host-or-ip>OPENSEARCH_PORT=9200OPENSEARCH_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)”| Setting | Value |
|---|---|
| Repo | Deployd-io/logrite-ai-service |
| Branch | main |
| Build Pack | Nixpacks |
| Port | 9300 |
| Domain | aiservice.logrite.ai |
| Health Check | GET /health |
| Persistent Volume | Mount at ./data/ for SQLite conversations DB |
NIXPACKS_NODE_VERSION=22PORT=9300NODE_ENV=productionAI_PROVIDER=groqGROQ_API_KEY=<groq-api-key>GROQ_MODEL=openai/gpt-oss-120bGROQ_FALLBACK_MODEL=llama-3.3-70b-versatileAGENT_MAX_ITERATIONS=10SEMANTIC_API_URL=https://search.logrite.aiOPENSEARCH_URL=http://<opensearch-host>:9200# Set both when Security Plugin is enabled.OPENSEARCH_USERNAME=OPENSEARCH_PASSWORD=BACKEND_API_URL=https://leapfrogapi.deployd.ioANALYZER_API_URL=https://analyzer.logrite.aiSQLITE_DB_PATH=./data/conversations.dbTOOL_TIMEOUT_MS=30000TOOL_CONCURRENCY=3TOOL_MAX_RETRIES=2TOOL_RETRY_BASE_DELAY_MS=500SYSTEM_PROMPT_TIME_GRANULARITY=hourENABLE_QUERY_CACHE=trueQUERY_CACHE_SIMILARITY_THRESHOLD=0.85QUERY_CACHE_TTL_MS=3600000ANALYTICS_TEMPERATURE=0.0SEARCH_TEMPERATURE=0.1EXPLANATION_TEMPERATURE=0.3CREATIVE_TEMPERATURE=0.7ENABLE_RESPONSE_VALIDATION=trueMIN_RESPONSE_CONFIDENCE=0.6ENABLE_TOOL_RESULT_VALIDATION=trueENABLE_AUTO_FALLBACK=trueMAX_FALLBACK_DEPTH=2GOOGLE_SEARCH_API_KEY=GOOGLE_SEARCH_CX_ID=BRAVE_SEARCH_API_KEY=MCP Server (Python / uvicorn)
Section titled “MCP Server (Python / uvicorn)”| Setting | Value |
|---|---|
| Repo | Deployd-io/logrite-mcp-server |
| Branch | main |
| Build Pack | Dockerfile |
| Port | 9500 |
| Domain | mcp.logrite.ai |
| Health Check | GET /health |
MCP_PORT=9500OPENSEARCH_URL=http://<opensearch-host>:9200# Set both when Security Plugin is enabled.OPENSEARCH_USERNAME=OPENSEARCH_PASSWORD=COMPLIANCE_INDEX=compliance-logsREQUEST_TIMEOUT=10Webhook Server (Node.js)
Section titled “Webhook Server (Node.js)”| Setting | Value |
|---|---|
| Repo | Deployd-io/logrite-webhook |
| Branch | main |
| Build Pack | Dockerfile |
| Port | 9999 |
| Domain | webhook.logrite.ai (or internal only) |
| Health Check | GET /health |
DISCORD_WEBHOOK_URL=<discord-webhook-url>SERVER_IP=<server-ip>SERVER_NAME=Logrite Production ServerGITHUB_WEBHOOK_SECRET=<github-webhook-secret>Data Prepper (Docker)
Section titled “Data Prepper (Docker)”| Setting | Value |
|---|---|
| Build | Dockerfile (at /data-prepper/Dockerfile in LogriteMasterControl) |
| Base image | opensearchproject/data-prepper:2.10.0 (pinned — was :latest and caused restart loops) |
| Ports | 21890 (traces), 21891 (logs) |
| Domain | None (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.
OpenSearch + Dashboards (Docker)
Section titled “OpenSearch + Dashboards (Docker)”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/:
| Setting | Value |
|---|---|
| Engine image | opensearchproject/opensearch:3.3.1 (Security Plugin included) |
| Dashboards image | opensearchproject/opensearch-dashboards:3.3.1 (must match engine minor version) |
| Engine port | 9200 |
| Dashboards port | 5601 |
| Heap | OPENSEARCH_JAVA_OPTS=-Xms2g -Xmx2g (~50% of container memory) |
| Host kernel | vm.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.
Startup Order
Section titled “Startup Order”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)Persistent Volumes
Section titled “Persistent Volumes”| Service | Mount Path | Purpose |
|---|---|---|
| Backend | /root/logrite/scans/ | Git repo checkouts for scans — host dir /home/deployd/logrite/scans on both VMs |
| AI Service | ./data/ | SQLite conversation history |
Networking
Section titled “Networking”localhost vs Container Hostnames
Section titled “localhost vs Container Hostnames”Inside a Coolify container, localhost refers to the container itself — not the host machine. Any OPENSEARCH_URL=http://localhost:9200 will fail.
Options:
-
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. -
Use host IP — point to
http://<server-ip>:9200for OpenSearch or other host-level services. -
Use Coolify internal network — put all services on the same Docker network and use container hostnames. Fastest, no TLS overhead between services.
Concurrent Builds
Section titled “Concurrent Builds”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:
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:
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:
- builds the service’s Docker image on GitHub runners (buildx,
cache-from/to: ghalayer caching), - pushes
ghcr.io/deployd-io/<service>:latest+:<sha>(org-private packages;GITHUB_TOKENwithpackages: write— no PAT needed for pushing), POST https://webhook.logrite.ai/coolify/deploywith{"app":"<slug>","sha":…}andAuthorization: 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 / rollback
Section titled “Cutover / rollback”Cutover per app (after docker login ghcr.io on the host as root — private pulls):
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).- Map the app slug in the webhook’s
COOLIFY_APP_UUIDSenv + restart webhook app. - 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).
Scan returns 500
Section titled “Scan returns 500”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_ env vars not taking effect
Section titled “VITE_ env vars not taking effect”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:
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.
Analyzer Dockerfile port mismatch
Section titled “Analyzer Dockerfile port mismatch”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.
OpenSearch unreachable
Section titled “OpenSearch unreachable”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.
Groq tool call errors
Section titled “Groq tool call errors”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.