Skip to main content

Troubleshoot a self-hosted deployment

Start by establishing which layer is broken, then jump to that section. Working downward from the symptom the user reported usually costs more time than checking the four layers in order. This page covers infrastructure — the platform not starting, not migrating, not authenticating. For a task that runs and produces the wrong result, the problem is the agent, not the deployment.

Prerequisites

  • Shell access to the Compose host, or kubectl against the namespace
  • jq for reading the JSON logs

Steps

1. Establish which layer is broken

Run these four in order and stop at the first that fails.
Under Compose, postgres_init, app_migrations, kratos-migrate, and rclone-init are one-shot. Exited (0) is success. Any other code is the failure, and the services that depend on them will not have started.

2. Read the logs as structured data

Every Python service emits one JSON object per line, so filter on fields:
Tracebacks are in the exception field with newlines escaped, so a failure is one line, not fifty. See observability for the full field list.

3. The stack will not start

Compose aborts before creating any container. A required variable is empty. SANDBOX_ACTIVATION_AUTH_SECRET and SANDBOX_CLEANUP_AUTH_SECRET are declared ${VAR:?message}, so Compose refuses rather than starting an unauthenticated sandbox path. Set both in .env, at least 32 bytes each. Port already allocated. Find the holder and stop it, or change the published port.
The db container will not start after pulling a newer checkout. postgres:18 stores data under /var/lib/postgresql/<version>/docker, so the Compose file mounts ./data/postgres at /var/lib/postgresql, not the legacy /var/lib/postgresql/data. A data directory laid out for the old mount point will not start. The API container restarts in a loop. Read the first error, not the last:
SECRET_MANAGER_ENCRYPTION_KEY environment variable must be set is the most common. SecretManagerFactory validates its configuration at construction, so the process exits at startup instead of failing later on the first secret read. Pods stay Pending.
FailedScheduling naming a RuntimeClass means mcpManager.runtimeClass points at one the cluster does not have. Unschedulable PVCs mean no default StorageClass. The migration Job sits in Init:0/1. Its wait-for-db init container polls nc -z <host> <port> with no timeout, printing Waiting for database.... Check that global.database.host resolves from inside the namespace.

4. The database or migrations are wrong

Confirm connectivity first, from the pod that fails:
Then the schema state:
Mismatched output means migrations have not fully applied. Migrations must run from /app/apps/apialembic.ini uses a relative script_location, so running from the repository root fails with a missing-script_location error. For relation already exists, Can't locate revision, and multiple heads, see database and migrations.

5. Nobody can log in

Authentication spans Kratos, the frontend, and the backend, and the failure is usually a URL mismatch rather than a broken service.
ORY_SDK_URL is for server-side calls from the frontend container; ORY_BROWSER_URL is where the browser goes. They are allowed to differ, and must, when pods cannot resolve the public domain. See networking.

6. Tasks are created but never run

Task execution is a Temporal workflow, so check the worker and Temporal, not the API.
The worker and the API must agree on all three Temporal values — WORKFLOW__TEMPORAL_SERVER_URL, WORKFLOW__TEMPORAL_NAMESPACE, and WORKFLOW__TEMPORAL_TASK_QUEUE. A worker polling a different task queue than the API submits to produces exactly this symptom, with no error on either side. Under Compose, temporal has a 120-second health-check start period and the worker waits for it. First start is slow; that is not a fault. Open the Temporal UI to see whether the workflow was started at all, and where it stopped:
If the workflow does not appear, the API never submitted it. If it appears and fails, the history shows which activity and why.

7. Tool calls or MCP servers fail

Instances are created but unreachable. With mcpManager.backend: kubernetes and the gateway_api feature on, the manager creates HTTPRoutes against mcpManager.gateway. Absent Gateway API CRDs or Gateway means nothing programs them. Instances start and immediately go idle. With mcpManager.serverless.enabled, MCP_IDLE_TIMEOUT reclaims uncalled instances. Both the API and the worker must have MCP_LAZY_PROVISIONING_ENABLED set to the same value — the worker dispatches agent tool calls, so without it a reclaimed instance is never brought back for agents. An MCP server cannot reach its upstream. The instance egress NetworkPolicy denies all cluster-internal and link-local ranges. An upstream inside the cluster is blocked by design; add it to mcpManager.instanceNetworkPolicy.extraEgress.

8. File uploads or artifacts fail

Presigned URLs are the usual cause, and the error surfaces in the browser rather than in any server log.
Empty means presigned URLs point at the in-cluster object store address, which the browser cannot resolve. Set global.storage.publicEndpoint. A CORS rejection after that means global.storage.cors.allowedOrigins was empty at bootstrap, which skips applying a rule entirely.

Verify

After any fix, confirm the whole path rather than the symptom:
Then run one task end to end and confirm it reaches a terminal state. That is the only check that exercises the API, the worker, Temporal, the MCP Manager, and object storage together.

Troubleshooting

Failures that look like one problem and are another: Everything is healthy and every action is denied. OpenFGA has no authorization data — a fresh store after a restore, or a bootstrap that did not run. The authorization reader fails closed, so a working platform where nothing is permitted looks like a permissions bug rather than a missing database. helm upgrade succeeded and the platform runs the old version. The migration Job is a normal resource, not a Helm hook, so it does not gate the rollout. Check the Job separately. See upgrades. Logs are plain text instead of JSON. Something called logging.basicConfig after setup_logging, replacing the handler that carries the formatter and the redaction filters. This is a logging defect and a secret-leak risk, not a cosmetic one. A Prometheus scrape returns 404. Expected. No AgentArea service exposes /metrics, despite the chart rendering METRICS_ENABLED. See observability. Provider icons are broken and OAuth callbacks fail. Both are served from API_BASE_URL. When global.api.publicUrl is empty the chart derives it from the backend ingress host, assuming https, and falls back to a ClusterIP URL if ingress is off. Stored credentials fail with InvalidToken. SECRET_MANAGER_ENCRYPTION_KEY no longer matches the ciphertext in encrypted_secrets. There is no recovery path without the original key. See secrets backends.