Configuration reference
Every Ouroboros service is configured by environment variables, and this page lists every one of them, grouped by the service and feature that reads it. Use it when you set up a deployment, when a service refuses to start and names a variable, or when another page sends you here to look one up.
The list is generated from the repository's .env.example — the template every installation
starts from — so it always names exactly the variables the code reads.
Where variables are set
Each service reads its variables when it starts. Change one and restart that service; nothing picks up a change while it runs.
- In a container, set them in the container's environment — your compose file's
environment:block or your platform's secret store. A real environment variable always wins, so a container runs with exactly what it was started with. - On a development checkout, run
yarn setuponce. It copies.env.exampleinto a.envfile beside it (and beside each module's own template), generating a real secret for every placeholder. A module's own.envis read after the root one and wins where both declare a variable.
Never commit a real .env file. The committed .env.example holds development values only:
none of them is a credential, and none of them is fit for a deployment.
The rules every variable follows
- Everything Ouroboros-specific starts with
OURO_, so a container can inherit unrelated environment without a collision. The two exceptions are BetterAuth's ownBETTER_AUTH_URLandBETTER_AUTH_SECRET, which keep the names that library and its documentation use. - Platform standards stay unprefixed.
PORTis the port a service listens on, as container platforms set it;NODE_ENVandHOSTNAMEare read as the platform means them. Every image already setsPORT, and the UI and REST images setNODE_ENV=production. - Configuration is checked at start-up. A service with a missing or malformed variable exits at once, naming the variable — it never starts half-configured and fails on the first request instead.
- Secrets never reach the logs. A service redacts every secret from the configuration it logs.
Reading an entry
Each variable below has its own entry, which you can link to — for example
OURO_VAULT_MASTER_KEY. An entry starts with its Development default:
- A value, such as
15, is what.env.examplesets for development. Where the description says what an unset variable means, that is the value a deployment gets when you leave it out; addresses such ashttp://localhost:4000are development values that a deployment always replaces. - A secret, generated by
yarn setupmarks a placeholder that must never be deployed. Generate your own, for example withopenssl rand -base64 32, and give every service that shares it the same value. - Unset. Example: … is a variable the template leaves out. Set it only when you need what its description offers.
The description explains what the variable does, the values it accepts and what happens when it is wrong.
What can go wrong
- A service exits at start-up naming a variable. The variable is missing or its value is malformed. Look it up below, fix it in that service's environment and start the service again.
- Two services refuse to talk to each other. A secret they share differs between them —
OURO_ENGINE_SHARED_SECRETmust be identical for the REST service and the engine. Set both from the same value. - Sign-in fails at the last step. The addresses do not agree: the GitHub OAuth app's
callback,
BETTER_AUTH_URLand the UI's public address must name the same origin. - Every stored provider key stops working.
OURO_VAULT_MASTER_KEYchanged or was lost. It cannot be recovered from the database; back it up separately.
Database — ouroboros-db, served by docker compose up (see docker-compose.yml)
OURO_DB_HOST
Development default: localhost
Host the database is reachable on. The compose stack is always localhost; this is read by ouroboros-db/run.sh, which can migrate any PostgreSQL, not only that one.
OURO_DB_USER
Development default: ouroboros
Role that owns the local database. Created by the container on first boot; changing it after that needs docker compose down -v to take effect.
OURO_DB_PASSWORD
Development default: ouroboros
Password for the role above. Local development only — the compose stack publishes the port on 127.0.0.1, so nothing off this machine can reach it.
OURO_DB_NAME
Development default: ouroboros
Database name. Created by the container on first boot.
OURO_DB_SCHEMA
Development default: ouroboros
Schema that Flyway owns and migrates. Created by Flyway if it does not exist.
OURO_DB_PORT
Development default: 5432
Host port the database is published on. Change it if something already holds 5432; the port inside the compose network is always 5432.
OURO_DATABASE_URL
Development default: postgresql://ouroboros:ouroboros@localhost:5432/ouroboros
Connection string used by ouroboros-rest and by the ouroboros-db scripts. Keep it consistent with the five variables above — compose does not derive it for you.
Local model host — the optional --profile ollama container
OURO_OLLAMA_PORT
Development default: 11434
Host port the Ollama daemon is published on. Read only by docker-compose.yml, and only when that profile is selected; the port inside the compose network is always 11434.
Nothing in ouroboros-rest reads this. An Ollama connection's address is a Host field on a provider card, stored in provider_connections.base_url — one workspace may point at this container and another at a workstation down the hall, which is a row rather than a variable. Change this only if something already holds 11434 on your machine, and then type the port you chose into the card.
Service wiring — the URLs each module uses to reach the next one
OURO_REST_URL
Development default: http://localhost:4000
Base URL of ouroboros-rest, read by ouroboros-ui. The UI reaches nothing else. ouroboros-rest reads it too, for one thing: the GitHub OAuth redirect_uri, which has to be the URL registered against the OAuth app rather than whatever Host header a request arrived with. An origin — scheme, host and optional port, no path, no trailing slash. ouroboros-engine's simulated-run driver reads it as where to report the runs it scripts.
OURO_UI_URL
Development default: http://localhost:3000
Where a browser lands after signing in or out. ouroboros-ui's own origin: the OAuth callback is a redirect a person follows rather than a fetch a script made, so it has to end somewhere with a page on it, and ouroboros-rest serves none.
OURO_ENGINE_URL
Development default: http://localhost:8000
Base URL of ouroboros-engine, read by ouroboros-rest. The engine is internal and is never reachable from a browser.
OURO_ENGINE_SHARED_SECRET
Development default: a secret, generated by yarn setup.
Shared secret for the internal REST-to-engine call, sent as X-Ouro-Internal-Key and compared in constant time. Both sides must carry the same value.
OURO_RUN_SIMULATOR_SECRET
Development default: a secret, generated by yarn setup.
The second value X-Ouro-Internal-Key may carry, and the one that makes a run simulated. A run's simulated watermark follows the principal that opened it, and the only thing a caller proves on the internal channel is which secret it holds — so the simulated-run driver presents this one and a real executor presents OURO_ENGINE_SHARED_SECRET.
ouroboros-engine reads it too, in development: set, an engine that carries the driver (never the production image) mounts its /dev routes, and uv run python -m ouroboros_simulator presents it.
Optional. Unset means this deployment runs no simulator and every run opened through the ingestion contract is a real one. It may not equal OURO_ENGINE_SHARED_SECRET: two principals presenting one proof are one principal, and ouroboros-rest refuses to boot.
Generate a strong value, e.g.: openssl rand -base64 32
Authentication — ouroboros-rest
BETTER_AUTH_SECRET
Development default: a secret, generated by yarn setup.
What BetterAuth signs sessions and encrypts stored OAuth tokens with; at least 16 characters. Rotating it signs everybody out and makes stored tokens undecryptable.
Generate a strong value, e.g.: openssl rand -base64 32
BETTER_AUTH_URL
Development default: http://localhost:4000
The origin BetterAuth builds its own URLs from — its sign-in redirect and its OAuth callback. ouroboros-rest's public address, so the same value as OURO_REST_URL above; nothing derives one from the other, so keep them in step. An origin: scheme, host and optional port, with no path — BetterAuth adds its own /api/auth.
OURO_GITHUB_CLIENT_ID
Development default: dev-github-client-id
GitHub OAuth application used for sign-in — BetterAuth's provider reads both. Register a development app under Settings -> Developer settings -> OAuth Apps with the authorization callback URL http://localhost:4000/api/auth/callback/github and paste its credentials here. That URL is ${BETTER_AUTH_URL}/api/auth/callback/github: the library builds it from BETTER_AUTH_URL rather than from OURO_REST_URL, and GitHub compares what was registered against what the exchange presents, so a difference of one character is a sign-in that fails at the last hop.
OURO_GITHUB_CLIENT_SECRET
Development default: dev-github-client-secret
Described with OURO_GITHUB_CLIENT_ID above.
OURO_GITHUB_API_BASE_URL
Development default: https://api.github.com
Where GitHub's REST API is. The public API, written out rather than left to the client's own default so that the template and the code state the same address.
Change it for a GitHub Enterprise Server installation — the address its API answers on, including the path:
OURO_GITHUB_API_BASE_URL=https://ghe.example.com/api/v3
It moves the address and nothing else. The credential is still the workspace's or the source's own token, the rate-limit rules are unchanged, and every failure is classified by the same code. The one other thing that sets it is the e2e suite's compose override, which points it at the sandbox tracker a planning push may create issues in — see tests/e2e/README.md.
Credential vault — ouroboros-rest
OURO_VAULT_MASTER_KEY
Development default: b3Vyb2Jvcm9zLWRldi12YXVsdC1tYXN0ZXIta2V5ISE=
The key-encryption key the credential vault seals every workspace's data-encryption key with. Exactly 32 bytes, base64:
openssl rand -base64 32
Exactly, not at least. A value of nearly the right length is one that was edited by hand or truncated in transit, and a service that stretched it to fit would seal credentials with bytes derived from a mistake. ouroboros-rest validates it at boot and exits non-zero naming this variable, printing no part of the value.
What it protects: this key encrypts the ouroboros.tenant_keys table and nothing else. Every provider credential is encrypted with a per-workspace key sealed there, which is why moving custody to AWS KMS or Vault/OpenBao later re-wraps that one table and leaves every credential ciphertext untouched.
LOSING THIS VALUE LOSES EVERY STORED CREDENTIAL, and there is no recovery path — that is what per-tenant sealing means. In the default deployment its custody is the operator's problem, stated plainly rather than as "KMS-backed"; docs/SECURITY_MODEL.md is where the whole model is written down. Rotating it is a re-wrap of tenant_keys and rewrites no credential.
OURO_CORS_ORIGINS
Development default: http://localhost:3000
Browser origins allowed to call ouroboros-rest with credentials — the origins the session cookie is permitted to travel to. Comma-separated, and each entry is an origin rather than a URL: a scheme, a host and an optional port, with no trailing slash and no path. A wildcard is not accepted, because a credentialed cross-origin request may not be answered with one. The development value is ouroboros-ui, which is the only browser origin there is.
OURO_DASHBOARD_POLL_SECONDS
Development default: 15
Seconds a dashboard client is told to wait between polls — the value ouroboros-rest sends as X-Ouro-Poll-After on every dashboard answer, 200 and 304 alike. The UI treats the latest value as its effective interval, so raising this is how a deployment under load slows every open dashboard within one poll cycle, with nothing shipped to the client. Whole seconds, 1–3600; 15 is the interval the contract documents for a visible tab.
OURO_LISTEN_HOST
Development default: 127.0.0.1
Which interface ouroboros-rest binds, when a stack has to choose it explicitly. Accepted values are exactly 127.0.0.1 and 0.0.0.0; anything else fails validation at boot. LEAVE IT AT THE VALUE BELOW: unset (or 127.0.0.1, which is the same posture in development) is what every deployment should inherit — NODE_ENV alone decides the interface, loopback outside production, every interface in it. The one stack that sets it to 0.0.0.0 is the e2e override, where the containerised service runs non-production so the seeded password sign-in answers, and loopback would be an interface Docker's port publishing cannot route to. It moves the interface only; the sign-in gate stays NODE_ENV's and gains no second switch.
Build-farm runner identity and the agent gateway — the forwarded client certificate, the version floor
OURO_FARM_CLIENT_CERT_HEADER
Development default: unset. Example: x-ouro-client-cert
The request header a trusted reverse proxy forwards a runner's TLS client certificate in. Unset by default, and that is the posture every deployment that terminates TLS inside ouroboros-rest should stay in: with it unset the certificate is read from the TLS socket and from nowhere else.
Set it only if something in front of this service terminates TLS. A proxy that does so has already consumed the client certificate, and mTLS becomes decoration unless the proxy passes it through — docs/SECURITY_MODEL.md's farm-CA section is where that requirement is written down, with the nginx and Traefik directives that satisfy it.
WHY IT IS OFF BY DEFAULT: a certificate is public. It crosses the network in the clear at every handshake, so anybody can obtain a copy of one — and a header this service trusts unconditionally is a header anybody can send. Naming one here is an assertion by the operator that this header cannot reach the process except through their proxy.
OURO_FARM_MIN_AGENT_VERSION
Development default: 0.0.0-0
The oldest ouroboros-runner build the agent gateway accepts, as a semantic version. Unset by default: no agent-version floor.
The agent runs on customer machines and cannot be force-upgraded, so the right to refuse an old one has to exist from the first release. An agent whose hello reports a version below this is answered refuse {version.below_minimum} with a sentence naming the floor, and exits rather than reconnecting into the refusal — so raise it only once the fleet has a newer build to move to. The protocol-line floor is separate and is the build's own.
The template's value is 0.0.0-0, the lowest version SemVer can express: every build — including an unstamped development build, which reports 0.0.0-dev — is at or above it, so a copied template declares the floor without setting one. A fleet floor looks like 0.2.0. Unset means the same as this value.
OURO_FARM_PUBLIC_URL
Development default: https://localhost:8443
The https origin runner machines reach this deployment at. ouroboros-rest serves the runner installer itself — GET /install.sh — and writes this into it, so the build farm's one-liner downloads the agent from this deployment and enrols it into this deployment, with no public host between a build machine and the binary it runs. An origin: https, a host and an optional port, no path. Unset, the installer uses OURO_REST_URL when that is https, and declines with a message naming this variable when it is not. Locally it is the TLS-terminating proxy OURO_RUNNER_SERVER below names, because the agent speaks TLS only and ouroboros-rest serves plain HTTP on 4000.
OURO_FARM_RELEASES_DIR
Development default: ../ouroboros-runner/dist
The directory ouroboros-rest serves runner releases from: one subdirectory per version, each exactly as ouroboros-runner's make release writes dist/<version>/ and the GitHub release ouroboros-runner-v<version> carries it — three binaries, install.sh and SHA256SUMS. A deployment copies a release in; it is served at once, without a restart. Unset, the deployment serves no installer. A relative path is resolved against ouroboros-rest's working directory, so this value is make release's own output when REST runs from its module directory, as yarn dev does.
OURO_FARM_LOG_BUDGET_BYTES
Development default: 2147483648
The bytes of finished builds' logs one workspace keeps. Build logs live in PostgreSQL, so the retention sweep holds each workspace under this budget by removing the oldest finished jobs' logs — whole, never in part — as well as every log older than thirty days. A running build's log is never swept. A whole number of bytes between 1048576 (1 MiB) and 1099511627776 (1 TiB); unset means 2147483648 (2 GiB), the value written here.
Build-farm artifact store — ouroboros-rest
OURO_ARTIFACT_STORE
Development default: local
Where a build job's uploaded artifacts are kept: local — a directory ouroboros-rest mounts, the default — or s3, any S3-compatible store (S3 or MinIO). A local volume does not scale horizontally: two replicas do not share a disk, and a deployment that needs to switches to s3 by configuration alone. Each artifact row records the driver it was written with, so the two coexist while old rows are moved.
OURO_ARTIFACT_DIR
Development default: .artifacts
The local store's directory. A relative path is resolved against ouroboros-rest's working directory; .artifacts there is gitignored. A deployment mounts a volume and names it here. Read only when OURO_ARTIFACT_STORE is local.
OURO_ARTIFACT_S3_ENDPOINT
Development default: http://localhost:9000
The S3-compatible endpoint, as an http(s) origin with no path — the bucket is added as a path segment (path-style addressing, which MinIO requires and S3 serves). Read only when OURO_ARTIFACT_STORE is s3, when this and the three below are required. The values here are docker-compose.yml's minio profile's.
OURO_ARTIFACT_S3_BUCKET
Development default: ouroboros-artifacts
The bucket artifacts are written to. It must already exist: a store that created buckets on demand would turn a typo into a new, empty bucket.
OURO_ARTIFACT_S3_REGION
Development default: us-east-1
The region requests are signed for. MinIO accepts any; unset means us-east-1.
OURO_ARTIFACT_S3_ACCESS_KEY_ID
Development default: ouroboros
The S3 access key id.
OURO_ARTIFACT_S3_SECRET_ACCESS_KEY
Development default: ouroboros-dev-minio-secret
The S3 secret access key — redacted from every log line. The value here is the compose MinIO's development password, not a credential.
OURO_ARTIFACT_QUOTA_BYTES
Development default: 10737418240
The bytes of live artifacts one workspace keeps. A file that would cross it is not stored and the job carries an artifact_quota_exceeded warning — never a failed build. A whole number of bytes between 1024 and 1125899906842624 (1 PiB); unset means 10737418240 (10 GiB).
OURO_ARTIFACT_MAX_FILE_BYTES
Development default: 67108864
The per-file cap every job offer carries. A larger file is uploaded cut to it and listed as truncated. Between 1024 and 68719476736 (64 GiB); unset means 67108864 (64 MiB).
OURO_ARTIFACT_MAX_JOB_BYTES
Development default: 268435456
The per-job cap every job offer carries; files past it are listed as skipped. At least the per-file cap, and at most 68719476736; unset means 268435456 (256 MiB).
OURO_ARTIFACT_RETENTION_DAYS
Development default: 30
How many days an uploaded artifact is kept in a workspace that has not set its own artifacts retention tier — mockup 11's retained 30d. Between 1 and 3650; unset means 30.
Build-farm runner agent — ouroboros-runner
OURO_RUNNER_SERVER
Development default: https://localhost:8443
The control plane the agent enrols with and connects to, https:// only: the first request carries a token and every later one is authenticated by a client certificate. NOTE that the agent reads NO .env file — it runs on a customer's machine, configured by its service unit — so these six are fallbacks for the ouroboros-runner flags of the same meaning, listed here because this file is the complete list. Locally, ouroboros-rest serves plain HTTP on 4000, so running the agent against it needs a TLS-terminating proxy in front (the nginx and Traefik directives are in docs/SECURITY_MODEL.md § 7.6); this is where such a proxy listens.
OURO_RUNNER_TOKEN
Development default: orb_enroll_placeholder-mint-one-per-machine
The enrollment token, for ouroboros-runner enroll (--token). The variable exists to keep a token off the process list. It is minted by an owner or admin (POST /api/v1/farm/enrollment-tokens), spent on first use, and never written anywhere by the agent. The value here is a placeholder, not a token.
OURO_RUNNER_STATE_DIR
Development default: /var/lib/ouroboros-runner
Where the agent keeps its identity (--state-dir): the client certificate and key, the farm CA it pins, and its unacknowledged terminal frames — a 0700 directory of 0600 files.
OURO_RUNNER_SERVER_CA
Development default: /etc/ouroboros-runner/server-ca.pem
PEM roots to verify the control plane with, in place of the system's (--server-ca). Needed for a private deployment, and for the development proxy above, whose certificate no system trusts. Not the farm CA: that signs runner certificates, and the agent pins it by itself.
OURO_RUNNER_NO_SHELL
Development default: false
true: run no job directly on this machine. The hello then reports shell: false and every shell offer is declined, while container jobs still run — the refusal a machine holding signing keys makes. Shell jobs run with no sandbox, as the agent's user: the tenant's machine running the tenant's command (ouroboros-runner/README.md).
OURO_RUNNER_KEEP_WORKSPACE_ON_FAILURE
Development default: false
true: leave the workspace of a job that failed, timed out or errored under <state-dir>/work/<job id> for diagnosis. By default every workspace is removed when its job ends.
OURO_RUNNER_LOG_CAP_BYTES
Development default: 67108864
The most of one job's output the agent sends, 65536–268435456 — the same range as the control plane's own per-job cap. Output past it is reported as dropped, which the run console renders as an elision rather than as a log that looks complete. An agent that shipped four gigabytes for the control plane to discard would already have done the damage.
Local model providers — ouroboros-rest's engine-facing surface
OURO_LOCAL_PROVIDER_URLS
Development default: ollama=http://localhost:11434
Where this deployment's local model providers are, as comma-separated kind=url pairs:
OURO_LOCAL_PROVIDER_URLS=ollama=http://localhost:11434,openai_compatible=http://localhost:8001/v1
Unset by default, and that is the normal posture: most installations run no local model server, and a lease for one then answers 404 rather than pretending something is listening on a port nobody mentioned.
What it is for. The architecture says workers never hold provider credentials: ouroboros-engine asks ouroboros-rest to make every cloud model call and the key never leaves the control plane. Local providers are the one exception, because there is no key on that path to protect — an engine worker calling an Ollama daemon on the same box gains nothing from proxying its traffic — so it asks POST /internal/credentials/lease and is told an address. Only an address: a lease has no field a credential could arrive in.
Only ollama and openai_compatible may appear here. Naming anthropic, copilot or cursor stops the process at boot rather than being ignored, because an operator who wrote it believes their workers reach that provider directly, and a service that started anyway would leave them believing it. Each address is an absolute http:// or https:// URL and keeps its path — vLLM is served at /v1, and an address truncated to its origin sends every request to a 404 that reads like the model server being down.
From inside the compose network, "localhost" is the container rather than the machine: a daemon running on the host is reachable at host.docker.internal, and one running as a service in the stack is reachable by its service name.
A later release replaces this with provider_connections rows; until it lands, the operator's declaration is the only thing that can say where a local provider is.
The value below is Ollama's own default address, which is what a developer running it locally already has. A checkout without Ollama is not broken by it: a lease is an address rather than a health check, so the surface answers with this URL and the call that follows fails against nothing listening — which is the same thing that happens to any other local service that is not up. Delete the line to declare no local providers at all.
Provider health — the routing page's honest status strip
OURO_PROVIDER_HEALTH_INTERVAL_SECONDS
Development default: 60
How often ouroboros-rest checks whether a workspace's model providers are reachable, in seconds. Optional; 60 when unset, which is what the strip's promise is written against — a stopped Ollama daemon shows on the page within one cycle.
The delay is jittered by +/-25% around this value, first cycle included, so a fleet of self-hosted instances restarted together does not converge on one schedule and arrive at a provider's endpoint in the same second forever. This is the nominal interval, not the actual one.
It is also the age at which a local provider's last check counts as stale. Between 10 and 86400. There is deliberately no value that turns the sweep off: a strip that has stopped updating and a strip that honestly says "unknown" look different to a person, and only one of them is true.
What the sweep actually does is bounded by design: Ollama is asked for its tag listing, an OpenAI-compatible server for its model listing, and Anthropic for a models-list key validation. No completion request is ever issued — nothing here costs a workspace money, and nothing here is billed per call.
OURO_PROVIDER_HEALTH_KEY_CHECK_SECONDS
Development default: 900
How old a cloud provider's key validation may get before it is redone, in seconds. Optional; 900 (fifteen minutes) when unset. Between 60 and 86400.
Much slower than the sweep above, and separate from it, because the two ask different people. A local daemon is the operator's own machine. A vendor's key-validation endpoint is somebody else's rate-limited service being asked by every self-hosted Ouroboros in the world, and what the check detects — a rotated or revoked key — happens on a human timescale. The sweep still runs on the shorter interval; this is how old a cloud row's last check has to be before that sweep touches it.
OURO_BACKLOG_SYNC_INTERVAL_SECONDS
Development default: 300
How often the backlog sync polls GitHub, in seconds. Optional; 300 (five minutes) when unset. Between 60 and 86400.
This is what the intake page's "synced 40s ago" tag counts from. A cycle asks every enabled repository for the issues updated since its stored watermark, so an incremental poll costs roughly one request per repository — a workspace watching fifty of them spends about six hundred of a token's five thousand hourly requests at this cadence.
The delay is jittered by +/-25% around this value, first cycle included, the same rule the health sweep above follows and for the same reason. A cycle that stopped at its per-poll cap — a cold import of a large backlog — books the next one a second later instead of waiting a full interval, so a first sync is several quick cycles rather than an afternoon.
There is deliberately no value that turns polling off. A backlog that has quietly stopped updating and one whose freshness tag says how old it is look different to a person, and only one of them is honest; an operator who wants the backlog now has the manual re-sync.
OURO_ESTIMATION_CONCURRENCY
Development default: 4
How many issues the estimation pipeline sizes at once. Optional; 4 when unset. Between 1 and 32.
The bound on outbound calls to ouroboros-engine for sizing, and on the pool connections their writes hold. It is what keeps a cold import of a large backlog from opening one engine connection per issue: the work queue admits this many at a time and the rest wait their turn in memory.
Four keeps a single-process engine busy over a network hop without overwhelming it. Raising it much past the database pool's ten connections stops buying anything, because the extra work then queues on the pool instead.
OURO_ESTIMATION_CONFIDENCE_FLOOR
Development default: 70
Below what confidence an estimate sends its issue to needs human, as a percentage. Optional; 70 when unset. Between 0 and 100.
The transition is this service's policy, not the estimator's: ouroboros-engine reports how sure it is and has no needs_human field at all. 70 is the number the bundled heuristic's tables were calibrated against, restated here so an installation can disagree with it without a release — 0 accepts every estimate, 100 sends every issue to a person.
An estimate below the floor is still stored in full, trace and all. It is a real answer beside a needs human pill, not a failure.
OURO_ESTIMATION_STALE_SECONDS
Development default: 600
How long an issue may sit in estimating before the sweep re-queues it, in seconds. Optional; 600 (ten minutes) when unset. Between 60 and 86400.
Not a timeout on an estimate — the engine gateway's own five-second deadline is that — but how long a row may claim to be estimating with nothing estimating it. A process killed mid-flight is the case: the row says estimating, no queue holds it, and without this sweep the page would show that pill until somebody re-estimated by hand.
OURO_ESTIMATION_SWEEP_INTERVAL_SECONDS
Development default: 120
How often that sweep runs, in seconds. Optional; 120 (two minutes) when unset. Between 10 and 86400.
Faster than the backlog sync because it knocks on a different door: one indexed query against this deployment's own database, which usually returns nothing. A row stranded by a restart comes back within this plus OURO_ESTIMATION_STALE_SECONDS. Jittered by +/-25%, like every other loop here.
Run controls — ouroboros-rest
OURO_RUN_CONTROL_TTL_SECONDS
Development default: 120
How long a pause, resume or abort is worth delivering, in seconds. Optional; 120 (two minutes) when unset. Between 10 and 3600.
A control is a row on a durable queue, and one the executor has not answered by then is marked expired, which the console renders as "no response — the run may be between stages" rather than as success.
OURO_RUN_STEER_TTL_SECONDS
Development default: 300
How long a steer is worth delivering, in seconds. Optional; 300 (five minutes) when unset. Between 10 and 3600. Longer than the buttons', because a steer lands at the executor's next point of context injection and a nudge that arrives a little late is still the nudge.
OURO_RUN_CONTROL_SWEEP_SECONDS
Development default: 15
How often the control-expiry sweep runs, in seconds. Optional; 15 when unset. Between 5 and 3600. One indexed update against this deployment's own database; the console's listing and the executor fetch sweep their own run first, so this only decides how soon an expiry reaches the audit trail for a run nobody is watching. Jittered by +/-25%.
Workspace lifecycle — ouroboros-rest
OURO_LIFECYCLE_PURGE_SWEEP_SECONDS
Development default: 3600
How often the workspace-purge sweep runs, in seconds. Optional; 3600 when unset. Between 60 and 86400. A deleted workspace is recoverable for 30 days; the sweep purges each one whose window has closed — its rows, its artifact objects and its data-encryption key. Jittered by +/-25%.
Outbound webhooks & SIEM streaming — ouroboros-rest
OURO_WEBHOOK_DISPATCH_SECONDS
Development default: 5
How often the webhook dispatcher runs, in seconds. Optional; 5 when unset. Between 1 and 300. Each tick queues a delivery for every endpoint subscribed to a new event and sends every attempt that is due, so this is the delay between an event and its first attempt. Jittered by +/-25%.
OURO_WEBHOOK_MAX_ATTEMPTS
Development default: 5
How many attempts a delivery gets before it lands in the dead-letter queue. Optional; 5 when unset. Between 1 and 20. Retries back off exponentially (30s, 1m, 2m, 4m … capped at 1h).
OURO_WEBHOOK_INTERNAL_ALLOWLIST
Development default: unset. Example: collector.internal,10.20.0.0/16
Internal collectors a webhook may reach despite the SSRF policy, comma-separated: hostnames, IP addresses or CIDR blocks (collector.internal,10.20.0.0/16). Optional; empty means none. By default a webhook may not reach loopback, link-local (including the cloud metadata address 169.254.169.254), RFC1918, carrier-grade NAT or unique-local addresses — at save and again at every delivery. This never relaxes https. Left commented: unset means no internal target is reachable, which is the only safe default for a template that may be copied anywhere.
Backlog health & nightly re-estimation — ouroboros-rest
OURO_BACKLOG_STALE_DAYS
Development default: 30
How many days without a tracker update make an open ticket stale on the planning page's Backlog Health card. Optional; 30 when unset. Between 1 and 3650.
OURO_REESTIMATION_HOUR_UTC
Development default: 2
The UTC hour the nightly re-estimation job runs at — it queues open, unsized canonical tickets through the estimation pipeline. Optional; 2 when unset. Between 0 and 23.
OURO_REESTIMATION_JITTER_MINUTES
Development default: 30
How many minutes after that hour a night's run may land, chosen at random each night so a fleet of installations does not reach for its engines in the same second. Optional; 30 when unset. Between 1 and 180.
OURO_REESTIMATION_BATCH
Development default: 100
The most unsized tickets one night's run queues, across every workspace and shared out between them. What the bound leaves behind is picked up the next night. Optional; 100 when unset. Between 1 and 1000.
Flake scorer — ouroboros-rest
OURO_FLAKE_RESCORE_HOUR_UTC
Development default: 3
The UTC hour the nightly flake re-scorer runs at — it re-scores each workspace's active flake cases under the latest formula and records a bookkeeping row per workspace. Each night's pass lands at a random minute in the hour after it. Optional; 3 when unset. Between 0 and 23.
OURO_FLAKE_RESCORE_CAP
Development default: 2000
The most cases one workspace's nightly pass re-scores, least recently scored first; what the bound leaves is first in line the next night. Optional; 2000 when unset. Between 1 and 100000.
Knowledge — the fact staleness sweep — ouroboros-rest
OURO_FACT_SWEEP_HOUR_UTC
Development default: 4
The UTC hour the nightly fact staleness sweep runs at — it matches every merged PR of an enabled repository since each anchor was last checked against confirmed facts' anchors, and flags the facts they touch stale for review. A merge the source sync observes is swept at once as well. Each night's pass lands at a random minute in the hour after it. Optional; 4 when unset. Between 0 and 23.
Knowledge — the repo-map generator — ouroboros-rest
OURO_REPO_MAP_HOUR_UTC
Development default: 5
The UTC hour the nightly repo-map generator runs at — for every enabled repository it lists the tree once, reads CODEOWNERS and the newest detection scan, and publishes a new generated repo-map skill version only when the map changed. Each night's pass lands at a random minute in the hour after it. Optional; 5 when unset. Between 0 and 23.
Insights rollups — ouroboros-rest
OURO_INSIGHTS_ROLLUP_INTERVAL_SECONDS
Development default: 3600
How often the Insights rollup ticks, in seconds. Optional; 3600 (an hour) when unset. Between 60 and 86400. Each tick re-fills today's daily rows for every workspace and metric family, steps any backfill, and — on the first tick of a new UTC day — consolidates the trailing window below. Jittered by +/-25%, like every other loop here.
OURO_INSIGHTS_ROLLUP_CONSOLIDATE_DAYS
Development default: 3
How many days, ending yesterday, the nightly consolidation re-fills. Optional; 3 when unset. Between 1 and 31. Late data — a host sync that ran late, a revert that landed days after the merge it undoes — reaches the day it belongs to within this window.
OURO_INSIGHTS_ROLLUP_BACKFILL_DAYS
Development default: 90
How far back, in days, a metric family's first fill reaches. Optional; 90 (the page's longest range) when unset. Between 1 and 730. Also the furthest a consolidation after an outage reaches.
OURO_INSIGHTS_ROLLUP_DAYS_PER_TICK
Development default: 31
The most backfill days one tick fills per family. Optional; 31 when unset. Between 1 and 366. What a tick leaves, the next resumes at the cursor — an interrupted backfill neither skips nor double-counts a day.
Mail and the weekly Insights digest — ouroboros-rest
OURO_SMTP_URL
Development default: smtp://localhost:1025
The SMTP server ouroboros-rest sends mail through: smtp://host:port, or smtps://user:password@host:port for a relay that wants TLS and a login. Optional. Unset, this deployment sends no mail: the weekly Insights digest is off, the API says so (mail.transport: "none"), and subscribing is refused rather than accepted into silence.
The development value is mailpit, which catches every message and shows it at http://localhost:8025 — start it with docker compose --profile mail up -d mailpit. Nothing leaves the machine. It may carry a password, so boot logs print it with the password masked.
OURO_MAIL_FROM
Development default: no-reply@ouroboros.localhost
The address mail is sent from — a bare address, no display name; mail goes out as "Ouroboros". Required when OURO_SMTP_URL is set, and refused at boot without it.
OURO_INSIGHTS_DIGEST_INTERVAL_SECONDS
Development default: 300
How often ouroboros-rest checks whether a workspace's weekly digest has come due, in seconds. Optional; 300 (five minutes) when unset. Between 5 and 3600. This is how late after its slot a digest may leave, not how often one is sent: each workspace's slot is a day of the week and a UTC time its administrators choose (Monday 09:00 until they do). Jittered by +/-25%.
Onboarding — the Get Started wizard's template tiles and smart defaults
OURO_ONBOARDING_UNLOCK_THRESHOLD
Development default: unset. Example: 10
The merged-loop count that unlocks an advanced onboarding template, replacing each template's own rule — the Deep refactor tile's "unlock after 10 merged loops". Optional; unset, every template's shipped rule stands. A whole number between 0 and 10000; 0 unlocks every tier.
OURO_MANAGED_KEY_POOL
Development default: false
Which pools this deployment runs for its workspaces — what the wizard's Smart Defaults card is allowed to promise. Both are false unless declared, which is the self-hosted default: the card then says "bring your own keys" and "enroll a runner" instead of offering managed keys or a hosted runner that do not exist here. true or false; the pools themselves arrive in a later release.
OURO_HOSTED_RUNNER_POOL
Development default: false
Described with OURO_MANAGED_KEY_POOL above.
OURO_MANAGED_KEY_TRIAL_CENTS
Development default: unset. Example: 500
The trial credit the managed key pool gives a new workspace, in whole cents — 500 prints "$5 trial credit". Optional; unset, the managed row names no figure. Between 1 and 1000000, and refused at boot unless OURO_MANAGED_KEY_POOL is true.
OURO_DATA_REGION
Development default: unset. Example: eu-central-1
Where this deployment keeps its data, as you would name it — shown read-only on the Settings workspace card. Optional; unset, the card says "self-hosted". A label without spaces: letters, digits and . _ : / ( ) -, up to 64 characters. It describes where you deployed and moves nothing.
Observability
OURO_LOG_LEVEL
Development default: info
Log verbosity for ouroboros-engine: debug, info, warning or error.
Testing — ouroboros-rest's integration harness
OURO_TEST_DATABASE_DISPOSABLE
Development default: false
Whether yarn test:integration may empty the database between tests.
The harness normally starts a throwaway PostgreSQL of its own, migrates it, and empties it between tests; a container it started is disposable by definition and this variable is not consulted. It matters only when OURO_DATABASE_URL is exported to point the suite at a database somebody else started — the compose stack above, most likely — because emptying that one takes the development seed with it, and no migration puts it back.
So it stays false here, and the harness refuses to truncate. Set it to exactly true for a run against a database you are genuinely willing to lose. Nothing outside the test suites reads it.
Documentation screenshots — ouroboros-docs's capture harness
OURO_DOCS_CAPTURE_BASE_URL
Development default: http://localhost:3000
The seeded ouroboros-ui that yarn screenshots (in ouroboros-docs) signs in to and photographs. It signs in with the development seed's email/password credential, which ouroboros-rest only accepts outside production — so this is always a development or e2e stack, never a deployment. Nothing in the application reads it.