Troubleshooting

Start here: 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 catches most failures without you having to guess which subsystem is broken. It reads env files, so it is not a Kubernetes tool — on Helm, skip to the checks below.

npx sim-setup doctor
npx sim-setup doctor --fix     # repair what it safely can
npx 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 if anything failed, so it drops straight into a script. See Verify Your Install for what each group of checks covers.

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

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  # macOS/Windows
OLLAMA_URL=http://192.168.1.x:11434           # Linux (use actual IP)

A workflow cannot reach a service on your network

Outbound requests to private, reserved, and loopback addresses are blocked by default, 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,*.svc.cluster.local
EGRESS_ALLOWED_IP_RANGES=10.0.0.0/8

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.

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. From Docker on macOS or Windows, use http://host.docker.internal:1234 rather than localhost. On Linux, use the 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.

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 reverse proxy passes WebSocket upgrades (see Docker guide)

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

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

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.

That leaves two cases a bundle does address: the endpoint is signed by a private CA — a corporate TLS-inspecting proxy, or an internal service — or it serves an incomplete chain, omitting an intermediate. Adding the private root to NODE_EXTRA_CA_CERTS resolves the first. An incomplete chain is different: Node still fails with UNABLE_TO_GET_ISSUER_CERT because the missing intermediate has to be served by the endpoint, so repair the server's chain rather than expecting a bundle to cover it.

For the private-CA case, the image already ships current CA certificates and runs as a non-root user, so installing packages inside it is not the fix. Mount your CA bundle and point Node at it — an incomplete chain is not fixed here, only on the server:

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

Blank page after login

  1. Check browser console for errors
  2. Verify NEXT_PUBLIC_APP_URL matches your actual domain
  3. Clear browser cookies and local storage
  4. Check that all services are running: docker compose -f docker-compose.prod.yml ps

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: use WSL2.

wsl --install

Line ending issues:

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

Scheduled workflows never run

The most common self-hosting surprise.

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

A stale LAST SCHEDULE or failing jobs usually means CRON_SECRET is missing or does not match between the cron pods and the app. Call the endpoint by hand to see the status code:

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.

Collaboration breaks with multiple replicas

Two users editing the same workflow stop seeing each other, or live status never updates — with no error anywhere.

This is Redis. Pub/sub and the Socket.IO adapter have no cross-pod fallback:

kubectl exec -n simstudio deploy/sim-app -- printenv REDIS_URL
kubectl exec -n simstudio deploy/sim-realtime -- printenv REDIS_URL

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

REDIS_URL uses rediss:// pointed at a bare IP address. TLS certificates cannot be verified against an IP, so set REDIS_TLS_SERVERNAME to the DNS name the certificate was issued for — or use a DNS hostname in the URL instead.

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 files over 50 MB 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.

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.

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 reconnect, so this degrades rather than breaks. See Networking.

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.

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. There is no recovery — the original key must be restored.

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: Postgres Pod Pending

kubectl describe pvc -n simstudio

Almost always no default StorageClass, no PV provisioner installed, or a StorageClass that does not support ReadWriteOnce. Set global.storageClass to pick a specific one.

Getting Help