Skip to content

Commit dcc00ef

Browse files
committed
feat(self-host): align Docker Compose with Helm and overhaul self-hosting docs
Docker Compose shipped no scheduler, so scheduled workflows, every polling trigger, connector syncs, the outbox, and data drains silently never ran. Adds a cron service running the same 18 jobs the Helm chart schedules as CronJobs, and closes the remaining behavioral gaps between the two paths: bundled Redis in the chart, no hosted plan caps in chart defaults, pinned image tags, and fail-fast secrets. A CI check keeps the schedulers in sync. Also rewrites the self-hosting docs: 14 new pages, 8 updated, reorganized into Install / Configure / Operate.
1 parent feaddc4 commit dcc00ef

53 files changed

Lines changed: 3911 additions & 335 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/CONTRIBUTING.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,15 @@ After running this command, open [http://localhost:3000/](http://localhost:3000/
165165
git clone https://github.com/<your-username>/sim.git
166166
cd sim
167167

168+
# Generate the required secrets. The stack refuses to start without them
169+
# rather than booting with empty values.
170+
cat > .env << EOF
171+
BETTER_AUTH_SECRET=$(openssl rand -hex 32)
172+
ENCRYPTION_KEY=$(openssl rand -hex 32)
173+
INTERNAL_API_SECRET=$(openssl rand -hex 32)
174+
CRON_SECRET=$(openssl rand -hex 32)
175+
EOF
176+
168177
# Start Sim
169178
docker compose -f docker-compose.prod.yml up -d
170179
```

.github/workflows/ci.yml

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,12 @@ jobs:
294294
ecr_repo_secret: ECR_PII
295295
gh_runner: ubuntu-latest
296296
bs_runner: blacksmith-4vcpu-ubuntu-2404
297+
# No ECR repo is provisioned for cron, so it publishes to GHCR only.
298+
# The tag step below omits the ECR tag when the repo name is empty.
299+
- dockerfile: ./docker/cron.Dockerfile
300+
ghcr_image: ghcr.io/simstudioai/cron
301+
gh_runner: ubuntu-latest
302+
bs_runner: blacksmith-2vcpu-ubuntu-2404
297303
steps:
298304
- name: Checkout code
299305
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
@@ -338,15 +344,33 @@ jobs:
338344
ECR_REPO="${{ steps.ecr-repo.outputs.name }}"
339345
GHCR_IMAGE="${{ matrix.ghcr_image }}"
340346
341-
TAGS="${ECR_REGISTRY}/${ECR_REPO}:${{ github.sha }}"
347+
TAGS=""
348+
if [ -n "$ECR_REPO" ]; then
349+
TAGS="${ECR_REGISTRY}/${ECR_REPO}:${{ github.sha }}"
350+
fi
342351
343352
if [ "${{ github.ref }}" = "refs/heads/main" ] && [ -n "$GHCR_IMAGE" ]; then
344-
TAGS="${TAGS},${GHCR_IMAGE}:${{ github.sha }}-amd64"
353+
if [ -n "$TAGS" ]; then
354+
TAGS="${TAGS},${GHCR_IMAGE}:${{ github.sha }}-amd64"
355+
else
356+
TAGS="${GHCR_IMAGE}:${{ github.sha }}-amd64"
357+
fi
358+
fi
359+
360+
# An entry can legitimately resolve to no tags — e.g. the cron image has
361+
# no ECR repo, so on staging/dev (where GHCR tags are not applied) there
362+
# is nothing to push. Skip that build instead of failing the job.
363+
if [ -z "$TAGS" ]; then
364+
echo "No ECR repo and no GHCR tag for this entry on ${{ github.ref }} — skipping push."
365+
echo "skip=true" >> $GITHUB_OUTPUT
366+
else
367+
echo "skip=false" >> $GITHUB_OUTPUT
345368
fi
346369
347370
echo "tags=${TAGS}" >> $GITHUB_OUTPUT
348371
349372
- name: Build and push images
373+
if: steps.meta.outputs.skip != 'true'
350374
uses: ./.github/actions/docker-build
351375
with:
352376
provider: ${{ vars.CI_PROVIDER }}
@@ -470,6 +494,10 @@ jobs:
470494
image: ghcr.io/simstudioai/pii
471495
gh_runner: ubuntu-24.04-arm
472496
bs_runner: blacksmith-4vcpu-ubuntu-2404-arm
497+
- dockerfile: ./docker/cron.Dockerfile
498+
image: ghcr.io/simstudioai/cron
499+
gh_runner: ubuntu-24.04-arm
500+
bs_runner: blacksmith-4vcpu-ubuntu-2404-arm
473501

474502
steps:
475503
- name: Checkout code
@@ -515,6 +543,7 @@ jobs:
515543
- image: ghcr.io/simstudioai/migrations
516544
- image: ghcr.io/simstudioai/realtime
517545
- image: ghcr.io/simstudioai/pii
546+
- image: ghcr.io/simstudioai/cron
518547

519548
steps:
520549
- name: Login to GHCR

.github/workflows/helm.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,19 @@ jobs:
3232
with:
3333
version: v3.16.4
3434

35+
- name: Setup Bun
36+
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
37+
with:
38+
bun-version: 1.3.13
39+
40+
- name: Install dependencies
41+
run: bun install --frozen-lockfile
42+
43+
# Docker Compose and Kubernetes must run the same background jobs on the
44+
# same schedules; this fails the build if the two drift apart.
45+
- name: Scheduler parity (docker/crontab vs helm cronjobs)
46+
run: bun run scripts/check-cron-parity.ts
47+
3548
- name: Helm lint
3649
run: helm lint helm/sim --values helm/sim/ci/default-values.yaml
3750

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
<p align="center">
99
<a href="https://deepwiki.com/simstudioai/sim" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/Ask-DeepWiki-E6E6E6?labelColor=C3C3C3&color=E6E6E6" alt="Ask DeepWiki"></a>
10-
<a href="https://cursor.com/link/prompt?text=Help%20me%20set%20up%20Sim%20locally.%20Follow%20these%20steps%3A%0A%0A1.%20First%2C%20verify%20Docker%20is%20installed%20and%20running%3A%0A%20%20%20docker%20--version%0A%20%20%20docker%20info%0A%0A2.%20Clone%20the%20repository%3A%0A%20%20%20git%20clone%20https%3A%2F%2Fgithub.com%2Fsimstudioai%2Fsim.git%0A%20%20%20cd%20sim%0A%0A3.%20Start%20the%20services%20with%20Docker%20Compose%3A%0A%20%20%20docker%20compose%20-f%20docker-compose.prod.yml%20up%20-d%0A%0A4.%20Wait%20for%20all%20containers%20to%20be%20healthy%20(this%20may%20take%201-2%20minutes)%3A%0A%20%20%20docker%20compose%20-f%20docker-compose.prod.yml%20ps%0A%0A5.%20Verify%20the%20app%20is%20accessible%20at%20http%3A%2F%2Flocalhost%3A3000%0A%0AIf%20there%20are%20any%20errors%2C%20help%20me%20troubleshoot%20them.%20Common%20issues%3A%0A-%20Port%203000%2C%203002%2C%20or%205432%20already%20in%20use%0A-%20Docker%20not%20running%0A-%20Insufficient%20memory%20(needs%2012GB%2B%20RAM)%0A%0AFor%20local%20AI%20models%20with%20Ollama%2C%20use%20this%20instead%20of%20step%203%3A%0A%20%20%20docker%20compose%20-f%20docker-compose.ollama.yml%20--profile%20setup%20up%20-d"><img src="https://img.shields.io/badge/Set%20Up%20with-Cursor-E6E6E6?logo=cursor&logoColor=1A1A1A&labelColor=C3C3C3&color=E6E6E6" alt="Set Up with Cursor"></a>
10+
<a href="https://cursor.com/link/prompt?text=Help%20me%20set%20up%20Sim%20locally.%20Follow%20these%20steps%3A%0A%0A1.%20First%2C%20verify%20Docker%20is%20installed%20and%20running%3A%0A%20%20%20docker%20--version%0A%20%20%20docker%20info%0A%0A2.%20Clone%20the%20repository%3A%0A%20%20%20git%20clone%20https%3A%2F%2Fgithub.com%2Fsimstudioai%2Fsim.git%0A%20%20%20cd%20sim%0A%0A3.%20Generate%20required%20secrets%20%28the%20stack%20will%20not%20start%20without%20them%29%3A%0A%20%20%20cat%20%3E%20.env%20%3C%3C%20EOF%0A%20%20%20BETTER_AUTH_SECRET%3D%24%28openssl%20rand%20-hex%2032%29%0A%20%20%20ENCRYPTION_KEY%3D%24%28openssl%20rand%20-hex%2032%29%0A%20%20%20INTERNAL_API_SECRET%3D%24%28openssl%20rand%20-hex%2032%29%0A%20%20%20CRON_SECRET%3D%24%28openssl%20rand%20-hex%2032%29%0A%20%20%20EOF%0A%0A4.%20Start%20the%20services%20with%20Docker%20Compose%3A%0A%20%20%20docker%20compose%20-f%20docker-compose.prod.yml%20up%20-d%0A%0A4.%20Wait%20for%20all%20containers%20to%20be%20healthy%20(this%20may%20take%201-2%20minutes)%3A%0A%20%20%20docker%20compose%20-f%20docker-compose.prod.yml%20ps%0A%0A5.%20Verify%20the%20app%20is%20accessible%20at%20http%3A%2F%2Flocalhost%3A3000%0A%0AIf%20there%20are%20any%20errors%2C%20help%20me%20troubleshoot%20them.%20Common%20issues%3A%0A-%20Port%203000%2C%203002%2C%20or%205432%20already%20in%20use%0A-%20Docker%20not%20running%0A-%20Insufficient%20memory%20(needs%2012GB%2B%20RAM)%0A%0AFor%20local%20AI%20models%20with%20Ollama%2C%20use%20this%20instead%20of%20step%203%3A%0A%20%20%20docker%20compose%20-f%20docker-compose.ollama.yml%20--profile%20setup%20up%20-d"><img src="https://img.shields.io/badge/Set%20Up%20with-Cursor-E6E6E6?logo=cursor&logoColor=1A1A1A&labelColor=C3C3C3&color=E6E6E6" alt="Set Up with Cursor"></a>
1111
</p>
1212

1313
<p align="center">
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
---
2+
title: Air-Gapped Deployments
3+
description: Running Sim with a private registry and restricted or no outbound internet access
4+
---
5+
6+
import { Callout } from 'fumadocs-ui/components/callout'
7+
import { Step, Steps } from 'fumadocs-ui/components/steps'
8+
import { FAQ } from '@/components/ui/faq'
9+
10+
Sim runs in restricted-egress and fully air-gapped environments, with real functional limits. Read [what does not work](#what-does-not-work-without-internet-access) before committing to an air-gapped deployment.
11+
12+
## Mirroring images
13+
14+
Every image Sim uses:
15+
16+
| Image | Purpose |
17+
|---|---|
18+
| `ghcr.io/simstudioai/simstudio` | App |
19+
| `ghcr.io/simstudioai/realtime` | Realtime |
20+
| `ghcr.io/simstudioai/migrations` | Migrations |
21+
| `pgvector/pgvector:pg17` | PostgreSQL (if not using managed) |
22+
| `redis:7-alpine` | Redis — bundled by both Compose and the chart |
23+
| `ghcr.io/simstudioai/cron` | Scheduler (Docker Compose) |
24+
| `curlimages/curl:8.5.0` | CronJob runner (Kubernetes) |
25+
| `ghcr.io/simstudioai/pii` | PII redaction (optional) |
26+
| `ollama/ollama` | Local inference (optional) |
27+
| `otel/opentelemetry-collector-contrib` | Telemetry collector (optional) |
28+
| `busybox:1.36` | `helm test` connectivity probe (only needed if you run `helm test`) |
29+
30+
If you enable the Copilot service, also mirror `ghcr.io/simstudioai/copilot` and the `postgres` image its StatefulSet uses.
31+
32+
Mirror them at a pinned tag:
33+
34+
```bash
35+
VERSION=v0.7.44
36+
REGISTRY=registry.internal.acme.com
37+
38+
for img in simstudio realtime migrations cron; do
39+
docker pull "ghcr.io/simstudioai/$img:$VERSION"
40+
docker tag "ghcr.io/simstudioai/$img:$VERSION" "$REGISTRY/simstudioai/$img:$VERSION"
41+
docker push "$REGISTRY/simstudioai/$img:$VERSION"
42+
done
43+
44+
for img in "pgvector/pgvector:pg17" "redis:7-alpine" "curlimages/curl:8.5.0"; do
45+
docker pull "$img"
46+
docker tag "$img" "$REGISTRY/$img"
47+
docker push "$REGISTRY/$img"
48+
done
49+
```
50+
51+
For a truly disconnected network, `docker save` the images to a tarball, transfer it on approved media, and `docker load` on the other side.
52+
53+
## Pointing the chart at your registry
54+
55+
```yaml
56+
global:
57+
imageRegistry: "registry.internal.acme.com"
58+
# Route ALL images through your registry, not just simstudioai/* ones
59+
useRegistryForAllImages: true
60+
imagePullSecrets:
61+
- name: internal-registry-creds
62+
63+
app:
64+
image:
65+
tag: "v0.7.44"
66+
realtime:
67+
image:
68+
tag: "v0.7.44"
69+
migrations:
70+
image:
71+
tag: "v0.7.44"
72+
```
73+
74+
<Callout type="warn">
75+
`useRegistryForAllImages` defaults to `false`, which routes only `simstudioai/*` images through your registry and leaves the third-party ones — `redis`, `pgvector/pgvector`, `curlimages/curl`, `ollama/ollama`, `otel/opentelemetry-collector-contrib`, `busybox`, and `postgres` (Copilot only) — pointing at Docker Hub. In an air-gapped environment that produces `ImagePullBackOff` on components you did not think to mirror. Set it to `true`.
76+
</Callout>
77+
78+
Create the pull secret:
79+
80+
```bash
81+
kubectl create secret docker-registry internal-registry-creds \
82+
--namespace simstudio \
83+
--docker-server=registry.internal.acme.com \
84+
--docker-username=<user> \
85+
--docker-password=<password>
86+
```
87+
88+
The chart itself is a directory in the repository — clone it, or package it with `helm package` and transfer the `.tgz`.
89+
90+
## Egress allowlist
91+
92+
If you have restricted rather than zero egress, allow these destinations from the app.
93+
94+
**Required for the deployment itself** — nothing. The app, realtime, and migrations components have no license check and no activation step. (The optional self-hosted Copilot service is the exception: the chart requires a Sim-issued `copilot.server.env.LICENSE_KEY` when you enable it.)
95+
96+
<Callout type="warn">
97+
Sim does attempt one outbound call you did not ask for: **anonymous telemetry** to `https://telemetry.simstudio.ai/v1/traces`, on by default. In a restricted-egress deployment it simply fails, but set `NEXT_TELEMETRY_DISABLED=1` to stop the attempts — or point `TELEMETRY_ENDPOINT` at an internal collector. See [Observability](/platform/self-hosting/observability).
98+
</Callout>
99+
100+
**Required for the features you use:**
101+
102+
| Destination | Needed for |
103+
|---|---|
104+
| `api.openai.com` | OpenAI models and the default knowledge base embeddings |
105+
| `api.anthropic.com` | Anthropic models |
106+
| `generativelanguage.googleapis.com` | Gemini models and `gemini-embedding-001` embeddings |
107+
| `*.openai.azure.com` | Azure OpenAI |
108+
| Your object storage endpoint | File storage (server side) |
109+
| Your email provider | Invitations, verification |
110+
| Provider OAuth + API endpoints | Each integration you enable |
111+
| `www.copilot.sim.ai` | The Chat module (`COPILOT_API_KEY`); override with `SIM_AGENT_API_URL` |
112+
| `*.e2b.app` / Daytona endpoint | Remote code sandboxes, if enabled. The E2B control-plane domain is overridable with `E2B_DOMAIN` |
113+
114+
When object storage is configured, the **browser** also needs to reach its endpoint directly, because uploads are presigned `PUT`s sent from the client. If your users are on the restricted network too, that endpoint must be reachable from their machines.
115+
116+
<Callout type="error">
117+
There is no global forward-proxy setting. Sim does not read `HTTP_PROXY` / `HTTPS_PROXY`, so model-provider calls, integration calls, and email delivery cannot be routed through a forward proxy. (The HTTP Request block accepts a per-request `proxyUrl`, but that covers only that one block, not the platform's own outbound traffic.) Environments with a mandatory egress proxy need a transparent proxy or NAT-based egress instead.
118+
</Callout>
119+
120+
## What does not work without internet access
121+
122+
<Callout type="warn">
123+
Plan around these before committing. None of them have a local workaround today.
124+
</Callout>
125+
126+
| Feature | Why | Workaround |
127+
|---|---|---|
128+
| **Knowledge bases** | Embeddings are generated by a hosted provider — OpenAI, Azure OpenAI, or Gemini. There is no local embedding backend. | Allow egress to one of those endpoints, or do not use knowledge bases |
129+
| **Chat module** | Requires `COPILOT_API_KEY` and reaches `www.copilot.sim.ai` | Set `NEXT_PUBLIC_CHAT_DISABLED=true` to hide it |
130+
| **Hosted model providers** | Reach the provider's API | Run Ollama, vLLM, or LiteLLM inside the network |
131+
| **All integrations** | Reach the third-party service | Unavoidable — the service is external by definition |
132+
| **Webhook triggers** | The provider must reach your instance | Use polling triggers if the provider is reachable outbound |
133+
| **Remote sandboxes (E2B / Daytona)** | External services | The in-process isolated-vm sandbox still runs JavaScript, but it is not a full substitute — features that depend on a remote sandbox image (Python execution, shell tooling, document generation) are unavailable |
134+
135+
### Local model inference
136+
137+
The one substitution that works cleanly. Any OpenAI-compatible endpoint inside your network:
138+
139+
```bash
140+
# Ollama
141+
OLLAMA_URL=http://ollama.internal:11434
142+
143+
# vLLM
144+
VLLM_BASE_URL=http://vllm.internal:8000
145+
VLLM_API_KEY=<optional>
146+
147+
# LiteLLM proxy
148+
LITELLM_BASE_URL=http://litellm.internal:4000
149+
LITELLM_API_KEY=<optional>
150+
```
151+
152+
The chart can deploy Ollama in-cluster, with optional GPU support:
153+
154+
```yaml
155+
ollama:
156+
enabled: true
157+
persistence:
158+
size: 100Gi
159+
```
160+
161+
Models must be pulled into the volume from a connected machine and transferred, or pulled during a connected bootstrap window.
162+
163+
<Callout type="info">
164+
Local inference does **not** solve knowledge bases. Embeddings go to whichever hosted embedding provider `KB_EMBEDDING_MODEL` selects — OpenAI, Azure OpenAI, or Gemini — never through the configured chat model, so an Ollama deployment still cannot generate knowledge base embeddings.
165+
</Callout>
166+
167+
## Deployment sequence
168+
169+
<Steps>
170+
171+
<Step>
172+
173+
### Mirror and verify images
174+
175+
Confirm each mirrored image pulls from inside the network before deploying anything.
176+
177+
```bash
178+
crane manifest registry.internal.acme.com/simstudioai/simstudio:v0.7.44
179+
```
180+
181+
</Step>
182+
183+
<Step>
184+
185+
### Provision infrastructure
186+
187+
PostgreSQL with pgvector, Redis, object storage (MinIO or Ceph RGW work well on-premises — see [Object Storage](/platform/self-hosting/object-storage) for the `S3_ENDPOINT` and `S3_FORCE_PATH_STYLE` settings), and an internal SMTP relay.
188+
189+
</Step>
190+
191+
<Step>
192+
193+
### Deploy with the registry override
194+
195+
```bash
196+
helm install sim ./helm/sim \
197+
--namespace simstudio --create-namespace \
198+
--values airgapped-values.yaml
199+
```
200+
201+
</Step>
202+
203+
<Step>
204+
205+
### Disable what cannot work
206+
207+
```yaml
208+
app:
209+
env:
210+
NEXT_PUBLIC_CHAT_DISABLED: "true"
211+
```
212+
213+
Disable CronJobs whose features you cannot use, and skip the integrations you have no egress for.
214+
215+
</Step>
216+
217+
<Step>
218+
219+
### Verify
220+
221+
Run the [verification checklist](/platform/self-hosting/verify), skipping the steps that depend on unavailable features. Steps 8 (knowledge base) and 10 (integrations) are expected to fail in a fully disconnected deployment.
222+
223+
</Step>
224+
225+
</Steps>
226+
227+
## Upgrades
228+
229+
Every upgrade means re-mirroring images at the new tag before running `helm upgrade`. Mirror all three of app, realtime, and migrations at the **same** tag — see [Upgrades](/platform/self-hosting/upgrades).
230+
231+
Read the release notes from a connected machine first; there is no in-product changelog.
232+
233+
<FAQ items={[
234+
{ question: "Does Sim require a license server?", answer: "Not for the core deployment — app, realtime, and migrations have no license check or activation step. The Chat module calls www.copilot.sim.ai with COPILOT_API_KEY and can be disabled with NEXT_PUBLIC_CHAT_DISABLED, and the optional self-hosted Copilot service requires a Sim-issued LICENSE_KEY."},
235+
{ question: "Can I use knowledge bases in an air-gapped deployment?", answer: "No. Embeddings require a hosted provider — OpenAI, Azure OpenAI, or Gemini (KB_EMBEDDING_MODEL=gemini-embedding-001) — and there is no local embedding backend, so knowledge bases need egress to one of those endpoints. Running Ollama locally does not help: embeddings do not route through the configured chat model."},
236+
{ question: "Why do I get ImagePullBackOff on Postgres or Redis after setting imageRegistry?", answer: "global.useRegistryForAllImages defaults to false, so only simstudioai/* images route through your registry — third-party images still point at Docker Hub. Set it to true and mirror pgvector, redis, curl, ollama, the OTel collector, and busybox as well."},
237+
{ question: "Can I run local models instead of hosted providers?", answer: "Yes. Ollama, vLLM, and LiteLLM all work as OpenAI-compatible endpoints inside your network, and the chart can deploy Ollama in-cluster with optional GPU support. Models must be transferred in or pulled during a connected window." },
238+
]} />

0 commit comments

Comments
 (0)