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:
| File | Purpose |
|---|---|
docker-compose.yml | Base stack: the Stratos service, the AppView indexer, Postgres |
docker-compose.feedgen.yml | Optional overlay that adds the feed generator |
.env.example | Annotated configuration template — copy to .env |
Base Stack (docker-compose.yml)
The root docker-compose.yml brings up three services:
stratos(port3100) — the Stratos service. Actor storage defaults to SQLite, persisted in thestratos-datavolume at/app/data.indexer(port3002) — the standalone indexer that feeds an AppView. It writes into the Postgresbskydatabase (BSKY_DB_POSTGRES_URL).postgres(port5432) — 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:
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 -dChoosing 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:
docker compose exec postgres createdb -U stratos stratosthen 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:
services:
feedgen:
environment:
FEEDGEN_SQLITE_PATH: /app/data/feedgen.sqliteLayer 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.