Upgrades

Pin your version

Never run :latest in production. An unpinned tag means an unplanned restart can pull a new version with new migrations at an arbitrary time.

Sim publishes images to GHCR, tagged by release alongside latest:

ghcr.io/simstudioai/simstudio
ghcr.io/simstudioai/realtime
ghcr.io/simstudioai/migrations

Pin app, realtime, and migrations to the same tag. They share a database schema. An app newer than its migrations runs against a schema missing columns it expects; an app older than its migrations runs against a schema it does not understand. Mismatched tags is the most common self-inflicted upgrade failure.

app:
  image:
    tag: "v1.2.3"  # a tag from the releases page
realtime:
  image:
    tag: "v1.2.3"  # a tag from the releases page
migrations:
  image:
    tag: "v1.2.3"  # a tag from the releases page

When image.tag is unset it defaults to the chart's appVersion, which moves when you upgrade the chart. Setting it explicitly decouples the two. For maximum determinism, pin image.digest instead:

app:
  image:
    digest: "sha256:..."

Images track latest by default. To pin, set SIM_VERSION in .env to a tag from the releases page:

# .env
SIM_VERSION=v1.2.3

One variable drives all three schema-coupled images, so they cannot drift apart. The cron service is deliberately excluded — it only makes HTTP calls and shares no schema, so it tracks latest unless you pin SIM_CRON_VERSION.

How migrations run

Migrations are Drizzle SQL files applied by a dedicated image.

  • Kubernetes — an init container on the app Deployment. Every app pod waits for migrations to complete before it starts, so a failed migration blocks the rollout instead of starting an app against a mismatched schema. The container is idempotent, so it is a no-op on pods that start after the first.
  • Docker Compose — a one-shot migrations service with restart: no that runs before the app.

Migrations are forward-only. There are no down-migrations, which is why the pre-upgrade backup below is not optional.

Upgrade procedure

Read the release notes

Check the releases page for new required environment variables and breaking changes. When the chart's minor version moves, also read its README.md upgrade notes — chart upgrades occasionally rename or remove values keys.

Take a backup

Snapshot the database immediately before upgrading. Because migrations are forward-only, this snapshot is your only rollback path for schema changes.

# Managed Postgres — take a manual snapshot
aws rds create-db-snapshot --db-instance-identifier sim-db \
  --db-snapshot-identifier "sim-pre-upgrade-$(date +%Y%m%d)"

# Bundled Postgres — the Helm chart's database is named `sim` by default
# (Docker Compose uses `simstudio`; the cloud example values files override to `simstudio`)
kubectl exec -n simstudio statefulset/sim-postgresql -- \
  pg_dump -U postgres -Fc sim > "pre-upgrade-$(date +%F).dump"

Rehearse against real data

Migration surprises are usually data-shaped rather than schema-shaped, so a staging run against a copy of production data catches far more than a run against an empty database.

Apply

helm upgrade sim ./helm/sim \
  --namespace simstudio \
  --values my-values.yaml

Preview first if the chart version changed:

helm diff upgrade sim ./helm/sim -n simstudio --values my-values.yaml

Then watch the rollout:

kubectl rollout status -n simstudio deploy/sim-app --timeout=10m
kubectl logs -n simstudio deploy/sim-app -c migrations --tail=100
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d
docker compose -f docker-compose.prod.yml logs migrations

There is a short window where the app is unavailable while containers restart. Compose has no rolling-update mechanism — plan a maintenance window, or run Kubernetes if you need zero-downtime upgrades.

Verify

Run the verification checklist. At minimum: sign in, open a workflow, execute it, upload a file, and confirm the background jobs are still firing.

When a migration fails

The app pods will not become ready — this is by design.

kubectl logs -n simstudio deploy/sim-app -c migrations --tail=200
docker compose -f docker-compose.prod.yml logs migrations

Common causes:

SymptomCause
permission denied to create extension "vector"The database user lacks superuser rights. Create the pgvector extension manually as an admin, then re-run.
Connection refused / timeoutDATABASE_URL wrong, or the database is not reachable from the pod. Check network policy and credentials.
Lock timeout on a large tableA long-running query is blocking DDL. Drain traffic and retry during a quiet window.
Constraint violationPre-existing data conflicts with a new constraint. Capture the error, restore the pre-upgrade backup, and open an issue with the exact message.

Do not manually edit the migrations table to skip a failed migration — the schema and Sim's expectations will diverge in ways that surface much later.

Rolling back

Application-only rollback (no migrations ran, or the new migrations are additive):

helm rollback sim -n simstudio
# Docker Compose — set the previous tag and restart
docker compose -f docker-compose.prod.yml up -d

Rollback after a schema change requires restoring the database to the pre-upgrade backup, because migrations are forward-only:

  1. Scale the app to zero.
  2. Restore the pre-upgrade database snapshot.
  3. Redeploy the previous image tag on all three images.
  4. Verify.

This loses everything written since the snapshot. It is why the pre-upgrade backup and a staging rehearsal matter more here than in most systems.

Common Questions

You can roll back the application with helm rollback. Schema changes cannot be rolled back — migrations are forward-only — so reverting a release that added migrations means restoring the pre-upgrade database backup and losing anything written since.
Migrations apply in sequence, so jumping several releases generally works mechanically. It is riskier — those combinations get less testing, and you take several releases' worth of behavior changes at once. Upgrade in smaller steps where you can.
On Kubernetes with replicaCount > 1, the rolling update keeps the app available, though migrations run before the new pods start. On Docker Compose there is a restart window — Compose has no rolling-update mechanism.

On this page