Skip to content

Deployment Examples

Everything needed to stand up Stratos lives in the repository root — there are no per-scenario bundles to copy. Two Compose files and one environment template cover the supported deployments:

FilePurpose
docker-compose.ymlBase stack: the Stratos service, the AppView indexer, Postgres
docker-compose.feedgen.ymlOptional overlay that adds the feed generator
.env.exampleAnnotated configuration template — copy to .env

Base Stack (docker-compose.yml)

The root docker-compose.yml brings up three services:

  • stratos (port 3100) — the Stratos service. Actor storage defaults to SQLite, persisted in the stratos-data volume at /app/data.
  • indexer (port 3002) — the standalone indexer that feeds an AppView. It writes into the Postgres bsky database (BSKY_DB_POSTGRES_URL).
  • postgres (port 5432) — Postgres 16 backing the indexer's AppView database.

An optional MinIO service for S3-compatible blob storage is included, commented out.

Copy the environment template, fill in the required values, then start the stack:

bash
cp .env.example .env
# edit .env — at minimum set STRATOS_SERVICE_DID, STRATOS_PUBLIC_URL,
# STRATOS_ALLOWED_DOMAINS, STRATOS_SYNC_TOKEN, and (for the bundled
# indexer) BSKY_DB_POSTGRES_URL
docker compose up -d

Choosing a Storage Backend

The Stratos service defaults to SQLite (STORAGE_BACKEND=sqlite), which keeps per-actor databases on the mounted volume — a good fit for single-node and development instances.

For high-traffic or high-availability deployments, switch the service to PostgreSQL by setting STORAGE_BACKEND=postgres and either STRATOS_POSTGRES_URL or the individual STRATOS_PG_* variables. See the Database Storage Backend section of the Configuration reference for the full variable list and precedence rules.

The bundled postgres service provisions only the indexer's AppView database (bsky) — there is no stratos database by default. To reuse the same instance for Stratos actor storage, create a separate database first:

bash
docker compose exec postgres createdb -U stratos stratos

then point STRATOS_POSTGRES_URL at it (for example postgres://stratos:stratos@postgres:5432/stratos). Do not point STRATOS_POSTGRES_URL at the bsky database — it is shared with the indexer's AppView.

Blob storage is a separate choice: local (default) or s3. See Blob Storage for MinIO/S3 settings.

Feed Generator Overlay (docker-compose.feedgen.yml)

To run the boundary-scoped feed generator alongside the service, layer the feedgen overlay on top of the base stack. The overlay adds a feedgen service (SQLite-backed) and an ephemeral Cloudflare tunnel so the feedgen's did:web document is reachable over HTTPS.

Feedgen keeps its materialized record projection in memory by default: posts, post boundaries, actor subscription cursors, and PDS-custody cursors all rebuild after a restart. The overlay still mounts a small feedgen-control volume and sets FEEDGEN_MEMBERSHIP_SQLITE_PATH=/app/data/feedgen-membership.sqlite. That database holds only enrolled-actor and space-member snapshots, from which runtime components source their hot maps for fast lookup and recovery/reconciliation.

Those durable snapshots are not an authorization grant. Before actor-subscription replayed records are admitted, Feedgen checks current membership with the authority; an unavailable or invalid answer fails closed and leaves the replay to retry. The overlay also sets the core-dump limit to zero because the process can hold private content.

Feed reads are separately admission-gated. From process start, and again through every enrollment-stream reconnect and reconciliation, zone.stratos.feedgen.getFeed returns HTTP 503 FeedNotReady. It becomes available only after a complete current-authority reconciliation. A pass bounded by FEEDGEN_RECONCILE_MAX_ACTORS is partial and does not release that gate. Keep FEEDGEN_SUBSCRIBE_ENROLLMENTS enabled: setting it to false leaves feed reads unavailable, so it is not a static-feed or manual-soak serving mode.

Persisting the private record projection is separately opt-in. Add a deployment-specific Compose override:

yaml
services:
  feedgen:
    environment:
      FEEDGEN_SQLITE_PATH: /app/data/feedgen.sqlite

Layer that file after docker-compose.feedgen.yml. The overlay's feedgen-control volume remains required; the opt-in record file additionally retains private posts, boundaries, and both replay cursors. Protect that volume as private content. It is also where the membership snapshots live, but those snapshots remain a recovery baseline rather than the authority for replay authorization.

Using FEEDGEN_STORAGE_BACKEND=postgres with FEEDGEN_POSTGRES_URL persists both the record projection and membership snapshots in PostgreSQL. Protect that database as private content.

A bare docker compose -f docker-compose.yml -f docker-compose.feedgen.yml up -d is not enough on its own: the feedgen's identity is derived from FEEDGEN_HOST (FEEDGEN_SERVICE_DID=did:web:${FEEDGEN_HOST}), and that host is the tunnel's ephemeral *.trycloudflare.com address — so FEEDGEN_HOST (the public host, no scheme) and FEEDGEN_SIGNING_KEY must be set before the feedgen starts.

The working order is: bring the tunnel up first, take the public host from its logs, generate (or reuse) a secp256k1 signing key, then start the feedgen with FEEDGEN_HOST and FEEDGEN_SIGNING_KEY exported. The header comments in docker-compose.feedgen.yml document the exact flow and the manual compose path — start there if you script the startup for your own deployment.

Configuration Reference

.env.example is the template for the common configuration — copy it to .env and fill in the required values. It is not exhaustive: the storage-backend variables (STORAGE_BACKEND, STRATOS_POSTGRES_URL, STRATOS_PG_*) are documented in the Database Storage Backend and Blob Storage sections linked above. The Configuration page groups every variable by concern — enrollment modes, domain boundaries, service enrollments, signing keys, storage, and blobs.