Files
simbarag/docs/deployment.md
T
ryan b02bf0a6df
CI / backend (pull_request) Successful in 42s
CI / frontend (pull_request) Failing after 6s
CI / backend (push) Successful in 41s
CI / frontend (push) Failing after 7s
Sync Obsidian both ways so notes Simba writes reach your devices
The container ran Obsidian Sync in pull-only mode, so notes, tasks and journal
entries the agent wrote stayed in its local copy. OBSIDIAN_SYNC_MODE now
defaults to bidirectional (merge on conflict); pull-only and mirror-remote
remain available.

Blue and green share the vault volume, and two sync clients writing one vault
and its sync state would race during a deploy. Setup and sync now run under a
flock on a file in the vault, so a new container starts syncing only after the
old container's sync process exits. Clearing Obsidian's stale sync lock is safe
inside that lock. Setup runs in the background, so startup no longer waits for
Obsidian login.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 08:10:48 -04:00

8.1 KiB

Deployment

Production Compose defines postgres (pgvector on PostgreSQL 16), optional simbakit, and two application services: raggr-blue on port 8080 and raggr-green on port 8081. Enable one application profile for a normal startup. The image builds the React frontend with npm ci, exports the frozen uv.lock dependency graph, and installs hashed Python packages system-wide so python and aerich work in startup and Compose exec commands. Runtime Node/MCP tools and uv are version pinned.

Configuration before deployment

Copy .env.example to .env, then replace the database placeholders and configure:

  • DATABASE_URL: postgres:// with host, database, user and a non-placeholder password. With the included database, its credentials must match POSTGRES_USER, POSTGRES_PASSWORD, and POSTGRES_DB.
  • JWT_SECRET_KEY: random signing secret of at least 32 characters.
  • FERNET_KEY: URL-safe base64 encoding of 32 random bytes. Required by encrypted model fields even without email. Preserve this key across deployments and restore it with encrypted database backups.
  • OPENAI_API_KEY: required because the OpenAI fallback client always initializes, including when LLAMA_SERVER_URL is set.

Generate keys locally with openssl rand -hex 32 for JWT and uv run --frozen python -c 'from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())' for Fernet. Store their output securely; never include configuration values in CI logs.

make validate-config

Validation runs again in startup.sh before migrations. Failure exits nonzero and names the invalid settings without printing values. It rejects unsafe defaults, malformed database/service URLs, invalid Fernet keys, empty model names, nonpositive/nonfinite timeouts, invalid numeric/boolean settings, context budgets exceeding the window, and invalid checkpoint paths. Successful validation checks syntax/policy, not network connectivity or credential validity.

OIDC is optional, but if enabled configure issuer, client ID, client secret and public callback URI together. Production requires HTTPS issuer/callback URLs; register https://your-host/api/user/oidc/callback with the provider. With discovery disabled, also set the four manual endpoint variables. Unused optional integration secrets are not required by validation. The current agent module nevertheless constructs Tavily at import time and its SDK rejects an empty key; until that initialization is made optional, application startup also needs a valid Tavily key for web search. Configured Twilio, Mailgun and SendBlue webhook integrations require their signature secret. Signature validation must remain enabled in production even for unused channels, because their routes are still registered. Metrics remain disabled (403) with an empty METRICS_TOKEN; when enabled it must be at least 32 characters. CRON_SECRET and EMAIL_HMAC_SECRET follow the same length policy if supplied.

TAVILY_API_KEY is the correct spelling. Compose and startup accept the legacy environment spelling TAVILIY_API_KEY when the canonical value is empty; migrate existing configuration to the canonical name. For local Python started directly, use the canonical name in .env.

Compose forwards context window, prompt budget, response reserve, llama read timeout, stream stall timeout, metrics token, manual OIDC endpoints, rate limits, and checkpoint/summary paths. Defaults are 32768 context tokens, 16000 prompt tokens, 4096 reserved response tokens, 120 seconds llama read timeout, and 30 seconds stream stall timeout. Align context settings with the smallest backend context window, including fallback.

raggr-checkpoints persists /app/checkpoints for both colors. The default checkpoint file is checkpoints.db; the default summary file is the checkpoint path plus .summaries.db. If overriding either path, keep it inside a persistent mounted directory and ensure its parent exists. Use separate files. Back up PostgreSQL, this volume, and encryption keys together.

The Compose bind mounts include ./obvault and ./token.json (Google OAuth, read-only). Provision a token file when enabling Google integrations; otherwise an empty regular file can occupy the mount path. Do not allow Docker to silently create a directory at token.json. If SimbaKit is enabled, supply PetKit credentials; app validation does not validate that separate service.

Startup and migrations

# Build and start a selected color only, after configuration is ready:
docker compose --profile blue build raggr-blue
docker compose --profile blue up -d --wait raggr-blue
make logs
make migrate-history

Startup validates configuration, runs aerich upgrade, optionally starts Obsidian sync in the background (OBSIDIAN_SYNC_MODE, default bidirectional so notes and tasks Simba writes reach your devices; containers take turns via a lock file in the shared vault), and replaces the shell with the Python process. A migration failure stops startup; the application is never launched. Obsidian login/setup failures still log a warning and allow the application to continue.

An empty database without Aerich metadata may need explicit initialization. Inspect its state first. Run the migration tool in a one-off container because the app cannot start until migrations pass:

docker compose up -d postgres
docker compose --profile blue run --rm --no-deps raggr-blue aerich init-db

Do not run init-db against an existing schema or fake migrations without a reviewed reconciliation plan. The pgvector extension initialization only runs on a fresh PostgreSQL volume; for an existing database, have the database owner ensure the vector extension exists before indexing. aerich init-db initializes ORM tables, not pgvector document indexing.

To generate and review changes, use make migrate-new and inspect migrations/models/. Manual application/history use make migrate and make migrate-history. The Makefile chooses the running color. Production migrations must be backward-compatible while both colors overlap; column removals and renames require staged changes. Make database backups before schema changes.

Blue/green rollout

./scripts/deploy.sh builds the inactive color, starts it with --wait, and stops the old color only when the new container is healthy. A failed new color is stopped while the old color remains serving. make redeploy first performs git pull, then calls that script. These commands perform a real deployment and should be run only by the deployment operator.

An external reverse proxy must route to both ports and check /api/health. For example:

simba.example.com {
  reverse_proxy host.docker.internal:8080 host.docker.internal:8081 {
    lb_policy first
    lb_try_duration 5s
    lb_try_interval 250ms
    health_uri /api/health
    health_interval 2s
    health_timeout 2s
    fail_duration 10s
  }
}

The proxy configuration is outside Compose. During rollouts both colors briefly share checkpoints and Obsidian files; avoid concurrent reindex operations and consider sync ownership when continuous Obsidian sync is enabled.

The current app.py entry point runs Quart in debug mode. Production process serving/debug hardening and broader readiness semantics require separate application changes; configuration validation does not resolve those concerns. Docker's existing health check remains /api/health.

CI and release checks

Gitea Actions runs backend Ruff, unit tests and deterministic context evaluation, plus frontend renderer tests, TypeScript checks and build. CI receives no production secrets and performs no deployment. Python resolves from uv.lock with frozen commands; npm uses package-lock.json. Lockfiles must match manifests; do not regenerate them during an image build or CI run.

Before rollout, require these checks to pass, validate the deployment environment, verify persistent mounts, and ensure migrations support both versions. Frozen application dependencies do not pin OS apt packages or transitive dependencies of globally installed integration CLIs; stricter image reproducibility can additionally use image digests and separately locked tool bundles.