Troubleshooting

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

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.

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 for what each group of checks covers.

502 Bad Gateway

# 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

# 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

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

# 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:

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

pgvector not found

Use the correct PostgreSQL image:

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

Kubernetes app pods never become ready

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

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 for migration-failure recovery.

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

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

  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

  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)

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.

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.

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:

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.

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.

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:

# 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
# 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

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:

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.

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

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

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

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:

    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

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.

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 need the scheduler and a deployed Schedule trigger.

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

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:

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:

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

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.

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

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.

wsl --install

Line ending issues:

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

Get help