# Troubleshooting (/platform/self-hosting/troubleshooting)



Start with the checks for your deployment type, then follow the symptom below.

## Run `sim-setup doctor` [#run-sim-setup-doctor]

On a production or local-development Compose install, or a source checkout, run the built-in checker first. It does not detect the Ollama stack. It checks configuration and common dependency failures. It reads env files, so it is not a Kubernetes tool — on Helm, skip to the checks below.

```bash
bunx sim-setup doctor
bunx sim-setup doctor --fix     # repair what it safely can
bunx sim-setup doctor --json    # machine-readable, for CI
```

On a standalone Compose install, run it from the directory holding your `.env` and Compose file. From a source checkout, run it there — it resolves the per-application env files itself and needs no root `.env`. It exits `1` when any check fails, so scripts can use the exit status. See [Verify Your Install](/platform/self-hosting/verify#sim-setup-doctor) for what each group of checks covers.

## 502 Bad Gateway [#502-bad-gateway]

```bash
# Check app is running — use the file that started your install
docker compose -f docker-compose.prod.yml ps simstudio
docker compose -f docker-compose.prod.yml logs simstudio

# Common causes: out of memory, database not ready
```

## Database connection failed [#database-connection-failed]

```bash
# The file that started your install — no bare `docker compose` works, the
# repo ships no default docker-compose.yml.
COMPOSE_FILE=docker-compose.prod.yml

# Check database is running
docker compose -f "$COMPOSE_FILE" ps db

# Test connection
docker compose -f "$COMPOSE_FILE" exec db psql -U postgres -c "SELECT 1"
```

Verify `DATABASE_URL` format: `postgresql://user:pass@host:5432/database`

## Migration errors [#migration-errors]

Migrations run in their own `migrations` service and image — the app image does not contain the migration tooling.

```bash
# View migration logs
docker compose -f docker-compose.prod.yml logs migrations

# Re-run them
docker compose -f docker-compose.prod.yml up --force-recreate migrations
```

On Kubernetes, migrations are an init container on the app pod:

```bash
kubectl logs -n simstudio deploy/sim-app -c migrations --tail=200
```

## pgvector not found [#pgvector-not-found]

Use the correct PostgreSQL image:

```yaml
image: pgvector/pgvector:pg17  # NOT postgres:17
```

## Kubernetes app pods never become ready [#kubernetes-app-pods-never-become-ready]

Check the migrations init container first — a failed migration deliberately blocks the rollout:

```bash
kubectl logs -n simstudio deploy/sim-app -c migrations --tail=200
kubectl describe pod -n simstudio '<pod>'
```

Common causes: `DATABASE_URL` unreachable, the database user lacking rights to create the `vector` extension, or an OOMKill from insufficient memory. See [Upgrades](/platform/self-hosting/upgrades) for migration-failure recovery.

## Kubernetes `ImagePullBackOff` [#kubernetes-imagepullbackoff]

Either the tag does not exist in the registry (`helm get values sim` and check), or you are pulling from a private registry without `global.imagePullSecrets`. When mirroring into a private registry, set `global.useRegistryForAllImages: true` — otherwise third-party images still point at Docker Hub.

## Kubernetes PostgreSQL pod pending [#kubernetes-postgresql-pod-pending]

```bash
kubectl describe pvc -n simstudio
```

Common causes are no default StorageClass, no PV provisioner, or a StorageClass that does not support `ReadWriteOnce`. Set `global.storageClass` to pick a specific one.

## Blank page after login [#blank-page-after-login]

1. Check browser console for errors
2. Verify `NEXT_PUBLIC_APP_URL` matches your actual domain
3. After collecting the error, try a fresh browser session to distinguish a stale session from a deployment failure
4. Check that all services are running: `docker compose -f docker-compose.prod.yml ps`

## WebSocket and realtime not working [#websocket-and-realtime-not-working]

1. Verify reverse proxy routes `/socket.io` to the realtime service (default port 3002). `NEXT_PUBLIC_SOCKET_URL` is only needed if realtime is on a separate host.
2. Verify realtime service is running: `docker compose -f docker-compose.prod.yml ps realtime`
3. Ensure the proxy passes WebSocket upgrades (see [Networking](/platform/self-hosting/networking))

## WebSockets disconnect every 30 seconds [#websockets-disconnect-every-30-seconds]

The load balancer's backend timeout is closing them. On GKE, attach a `BackendConfig` with `timeoutSec: 3600` to the realtime Service; on AWS, raise the ALB `idle_timeout`. Clients may reconnect; repeated disconnects still interrupt collaboration. See [Networking](/platform/self-hosting/networking).

## Agent output arrives all at once [#agent-output-arrives-all-at-once]

Your reverse proxy is buffering the response stream. Set `proxy_buffering off` (Nginx) or `flush_interval -1` (Caddy). See [Networking](/platform/self-hosting/networking).

## Collaboration breaks with multiple replicas [#collaboration-breaks-with-multiple-replicas]

If members on different replicas do not see edits or live status, check Redis configuration and connectivity in both services. Pub/sub and the Socket.IO adapter have no cross-pod local fallback:

```bash
kubectl exec -n simstudio deploy/sim-app -- sh -c 'test -n "$REDIS_URL" && echo "REDIS_URL is set"'
kubectl exec -n simstudio deploy/sim-realtime -- sh -c 'test -n "$REDIS_URL" && echo "REDIS_URL is set"'
```

Both pods must have `REDIS_URL`. On Helm they share one Secret, so setting it under `app.env` covers both. See [Redis](/platform/self-hosting/redis).

## App crashes at startup with a REDIS\_TLS\_SERVERNAME error [#app-crashes-at-startup-with-a-redis_tls_servername-error]

The app requires `REDIS_TLS_SERVERNAME` for an IPv4-literal `rediss://` URL. That override is not read by realtime and does not cover IPv6 literals. Prefer a DNS hostname matching the certificate for the shared deployment; see [Redis](/platform/self-hosting/redis#tls-to-an-ip-address).

## Certificate errors (CERT\_HAS\_EXPIRED) [#certificate-errors-cert_has_expired]

Rule out expiry and clock drift first: the endpoint's certificate may genuinely have expired, or the host's clock may have drifted far enough to put a valid certificate outside its window. Check the certificate dates and the host date before changing any trust configuration — no CA bundle fixes either.

For a private certificate authority (CA), such as a corporate TLS-inspecting proxy or an internal service, mount the trusted CA certificates and configure `NODE_EXTRA_CA_CERTS`. For `UNABLE_TO_GET_ISSUER_CERT`, check whether the endpoint omits an intermediate certificate and repair its served chain. A trusted bundle can contain multiple certificates, but adding a root alone does not supply a missing intermediate.

The image ships CA certificates and runs as a non-root account. Mount your corporate CA bundle and set the runtime variable; do not disable verification. Verify the affected transport, because a client with explicit TLS `ca` settings may use its own trust configuration:

```yaml
# docker-compose.prod.yml
services:
  simstudio:
    volumes:
      - /etc/ssl/certs/corporate-ca.crt:/certs/corporate-ca.crt:ro
    environment:
      - NODE_EXTRA_CA_CERTS=/certs/corporate-ca.crt
```

```yaml
# Helm — mount a ConfigMap holding the CA
app:
  env:
    NODE_EXTRA_CA_CERTS: /certs/corporate-ca.crt
  extraVolumes:
    - name: corporate-ca
      configMap:
        name: corporate-ca
  extraVolumeMounts:
    - name: corporate-ca
      mountPath: /certs
      readOnly: true
```

`NODE_TLS_REJECT_UNAUTHORIZED=0` disables certificate verification entirely and should never be used outside a throwaway test.

## A workflow cannot reach a service on your network [#a-workflow-cannot-reach-a-service-on-your-network]

Guarded requests to private and reserved addresses are blocked unless a supported self-hosted exception or allowlist applies. Loopback exceptions are scoped by URL provenance and port, so a workflow pointed at your Docker host, a LAN service, or a Kubernetes service name fails with a message naming the blocker — the private or loopback address it resolved to, a blocked port, or `must use https:// to a public destination` when the URL is plain HTTP — and pointing at the allowlist variables.

Name the destination:

```bash
EGRESS_ALLOWED_HOSTS=host.docker.internal,vllm.ai.svc.cluster.local
EGRESS_ALLOWED_IP_RANGES=10.4.2.17/32,10.4.9.0/24
```

What naming a destination does and does not grant — plain HTTP, the blocked-port list, database hosts, cloud metadata, Sim Cloud — is covered once in [Security](/platform/self-hosting/security#the-ssrf-boundary).

Two things this does not cover:

* Inside a container `localhost` is the container itself, so it will never reach a service on your host. Use `host.docker.internal` (the Compose files map it) and name it above.
* URLs harvested from content or from a third-party API response — an image URL, a file imported by URL, an MCP OAuth endpoint on a different origin than the MCP server itself — never reach a private network, allowlist or not. Nor does an HTTP block's `proxyUrl`.

## Ollama models not showing [#ollama-models-not-showing]

Inside Docker, `localhost` = the container, not your host machine.

```bash
# For host-machine Ollama, use:
OLLAMA_URL=http://host.docker.internal:11434
```

## LM Studio requests route to Ollama [#lm-studio-requests-route-to-ollama]

Sim identifies dynamically discovered LM Studio and vLLM models by their `vllm/` prefix. If the endpoint is unavailable and you manually enter the raw LM Studio model identifier, Sim treats that unknown identifier as an Ollama model.

1. Set `VLLM_BASE_URL`, and `VLLM_API_KEY` too if you enabled LM Studio's API authentication — without the key, discovery requests omit the bearer token and fail. No Compose file sets either for you — `docker-compose.ollama.yml` only sets `OLLAMA_URL` — so add them to the `.env` file next to your Compose file — `simstudio` declares `env_file: .env` in every shipped file — then recreate the service and confirm it landed:

   Use the Compose file that started your installation — `docker-compose.prod.yml` for the documented production setup, `docker-compose.local.yml` for a source-built stack, or `docker-compose.ollama.yml` if you started from the Ollama stack:

   ```bash
   COMPOSE_FILE=docker-compose.prod.yml   # or docker-compose.local.yml, docker-compose.ollama.yml
   docker compose -f "$COMPOSE_FILE" up -d --force-recreate simstudio
   docker compose -f "$COMPOSE_FILE" exec simstudio printenv VLLM_BASE_URL
   ```

   An empty result means the variable never reached the container, and model discovery cannot run.

2. In LM Studio, enable **Serve on Local Network** and API authentication so the container can connect safely.

3. With the shipped Compose host alias, use `http://host.docker.internal:1234` rather than `localhost`, including on Linux. If you use a custom container setup without that alias, configure it or use a reachable host IP.

4. The server root and a URL ending in `/v1` are both accepted.

5. Reload the workspace and select the discovered `vllm/<model-id>` option from the model picker.

## File uploads fail with a CORS error [#file-uploads-fail-with-a-cors-error]

The bucket's CORS policy does not allow your Sim origin. Uploads go directly from the browser to object storage via presigned `PUT`, so server-side configuration being correct is not enough.

If small uploads succeed but cloud uploads over 50 MiB fail during completion, check the app logs for the provider's part-listing request. The server completes multipart uploads from provider-authoritative state; for S3, its identity needs `s3:ListMultipartUploadParts`. See [Object Storage](/platform/self-hosting/object-storage).

## Knowledge base upload fails [#knowledge-base-upload-fails]

Embeddings need a provider — set `OPENAI_API_KEY`, configure Azure OpenAI, set `KB_EMBEDDING_MODEL=gemini-embedding-001` with a Gemini key, or set `KB_EMBEDDING_MODEL=ollama/<model>` with `OLLAMA_URL` to embed on your own Ollama. On a self-hosted deployment `OPENROUTER_API_KEY` also serves the OpenAI-family models on its own, as a fallback behind any OpenAI or Azure credentials you have. If one is configured, verify pgvector is installed on the database.

A document that fails with `vector 0 has N unexpected dimensions` means `EMBEDDING_OUTPUT_DIMS` does not match what the model actually emits. The message names both widths. If the width the model returned is one of `384`, `768`, `1024`, `1536`, or `3072`, set the variable to it and recreate the knowledge base. If it is anything else, no column can store it — choose a model that emits one of those five instead, since setting an unstorable width silently falls back to `1536` and the next document fails the same way. Existing knowledge bases keep the width they were created with.

## Scheduled workflows never run [#scheduled-workflows-never-run]

Scheduled workflows need the scheduler and a deployed Schedule trigger.

**Docker Compose** — check the `cron` service is running and read its logs:

```bash
docker compose -f docker-compose.prod.yml logs --tail=50 cron
```

A `401` there means the app and the scheduler disagree on `CRON_SECRET`.

**Kubernetes** — check the CronJobs are present and firing:

```bash
kubectl get cronjobs -n simstudio
kubectl get jobs -n simstudio --sort-by=.metadata.creationTimestamp | tail
```

For stale `LAST SCHEDULE`, inspect CronJob suspension, controller events, active Jobs, and scheduling deadlines. `Forbid` can skip a tick while an earlier Job is active. For a failed Job, inspect its logs; a 401 indicates authentication. Call the endpoint by hand to distinguish those cases:

```bash
kubectl exec -n simstudio deploy/sim-app -- sh -c \
  'curl -s -o /dev/null -w "%{http_code}\n" \
   -H "Authorization: Bearer $CRON_SECRET" \
   http://localhost:3000/api/schedules/execute'
```

Wrap it in `sh -c` with single quotes so `$CRON_SECRET` expands **inside the pod** — otherwise your local shell substitutes an empty value and you get a misleading `401`.

`401` means the secret does not match. `202` means the endpoint accepted the run; it does not tell you whether a schedule was actually due, so confirm in the Logs view.

## Gmail, Drive, and Outlook triggers never fire [#gmail-drive-and-outlook-triggers-never-fire]

These are **polling** triggers, driven by the per-minute `/api/webhooks/poll/*` jobs. Check the scheduler is running them — `docker compose -f docker-compose.prod.yml logs cron`, or `kubectl get cronjobs -n simstudio` for a recent `LAST SCHEDULE`.

Microsoft Teams chat triggers are the different case: they use a Microsoft Graph subscription capped at about three days, renewed by the twice-daily `renew-subscriptions` job. If Teams triggers work for a couple of days and then stop, that job is not running. See [Background Jobs](/platform/self-hosting/background-jobs).

## Credentials unreadable after a restore [#credentials-unreadable-after-a-restore]

Integrations show as connected but fail, or provider keys error on decrypt. `ENCRYPTION_KEY` does not match the value in use when the backup was taken. Restore the original key. A different key cannot decrypt that stored data.

## Windows-specific issues [#windows-specific-issues]

These apply to running Sim **from source** for development, not to the Docker or Kubernetes deployments, which are unaffected by the host OS.

**Turbopack errors on Windows:** run the source development environment in WSL2.

```bash
wsl --install
```

**Line ending issues:**

```bash
# Configure git to use LF
git config --global core.autocrlf input
```

## Get help [#get-help]

* [GitHub Issues](https://github.com/simstudioai/sim/issues)
* [Slack](https://join.slack.com/t/sim-ott9864/shared_invite/zt-43lp8tc5v-0qrrqHGBKUsvQlpoouH~TA)
