Configuration
Every configurable value in this repository comes from one file: .env at the repository root. There is no second location, no per-service env file, and no per-stack override file.
This page explains how that one file reaches four very different consumers, why it lives at the root and not somewhere tidier, and what you have to touch when you add a new variable.
The three files
| File | Committed | Purpose |
|---|---|---|
.env.minimal | Yes | What a first run needs. One variable. Copy it to .env and set your key. |
.env.example | Yes | The complete surface — every variable, grouped, with defaults and commentary. Reference material, not a starting point. |
.env | No (.gitignore) | Yours. Created by copying one of the above. |
cp .env.minimal .env # then set OPENAI_API_KEY
The file must be named exactly .env and must sit at the repository root. Docker Compose only auto-loads a file with that name from the project directory — anything else requires passing --env-file on every single docker compose invocation, which would break the plain docker compose up that the quick start advertises.
How one file reaches four consumers
%%{init: {'theme':'base', 'themeVariables': {
'primaryColor': '#2563eb','primaryTextColor': '#ffffff','primaryBorderColor': '#1e40af',
'lineColor': '#64748b','secondaryColor': '#f59e0b','tertiaryColor': '#10b981',
'background': 'transparent'}}}%%
flowchart TB
accTitle: How one file reaches four consumers
classDef core fill:#2563eb,stroke:#1e40af,color:#ffffff
classDef external fill:#f59e0b,stroke:#b45309,color:#000000
classDef success fill:#10b981,stroke:#047857,color:#ffffff
classDef infra fill:#64748b,stroke:#334155,color:#ffffff
ENV[".env<br/>repository root"]
COMPOSE["Docker Compose<br/>variable interpolation"]
PYD["Pydantic Settings<br/>shared/config.py"]
CONTAINERS["Containers<br/>orchestrator, agents, MCP, frontend"]
HOSTPY["Host-run Python<br/>uvicorn, seed, evals"]
HOSTWEB["Host-run frontend<br/>pnpm dev"]
HOSTNET["Host-run .NET<br/>dotnet run"]
ENV --> COMPOSE
ENV --> PYD
COMPOSE -->|"environment: blocks"| CONTAINERS
PYD --> HOSTPY
HOSTWEB -.->|"does not read .env<br/>falls back to a hardcoded default"| ENV
HOSTNET -.->|"does not read .env<br/>needs exported vars"| ENV
class ENV success
class COMPOSE,PYD core
class CONTAINERS,HOSTPY infra
class HOSTWEB,HOSTNET external
1. Docker Compose — variable interpolation
Compose reads .env from the project directory and uses it to expand ${VAR} references inside the compose file. It does not inject the file into containers.
environment:
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
LLM_MODEL: ${LLM_MODEL:-gpt-4.1}
A container only receives what its environment: block names. A variable you add to .env but not to the compose file will be silently absent inside every container — this is the single most common configuration mistake in this repo, and it produces a default-valued setting rather than an error.
Most of the agent services inherit one shared block via a YAML anchor:
environment: &agent-env # declared on the orchestrator
...
environment:
<<: *agent-env # every specialist merges it
OTEL_SERVICE_NAME: ecommerce.product-discovery
So a variable added to &agent-env reaches the orchestrator and all five specialists at once. The MCP servers and auth-server have their own blocks and do not inherit it.
2. Pydantic Settings — host-run Python
shared/config.py resolves the same file by absolute path, computed from the location of config.py itself rather than from the current working directory:
_REPO_ROOT = _resolve_repo_root(Path(__file__))
_ENV_FILE = _REPO_ROOT / ".env"
This is why cd agents/python && uv run uvicorn product_discovery.main:app picks up the root .env even though the working directory is two levels down. It is deliberate, and it carries two fixed bugs worth knowing about, both documented in the source:
- The repo root is three levels above
config.py, not two. An earlierparents[2]resolved to<repo>/agents, which contains no.env, so Pydantic’senv_fileloading never fired and every setting silently fell back to its default even with a real.envpresent. - Inside the Docker image,
config.pyis copied flatly to/app/shared/config.py— the build context is./agents/python, so that path depth does not exist.parents[3]raisedIndexErrorand crashed every container at import time._resolve_repo_rootfalls back to the immediate parent there, which contains no.env— correct, because containers get their config from the composeenvironment:block.
Do not change how this path is resolved without re-reading those comments.
3. The frontend — does not read .env
Next.js reads web/.env.local, not the repository root, so pnpm dev outside Docker sees nothing from the root .env. It does not need to: NEXT_PUBLIC_API_URL has a hardcoded fallback that matches the compose default.
// web/src/lib/api.ts
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080";
So cd web && pnpm dev works with no configuration at all, as long as the orchestrator is on :8080. Create web/.env.local only if you need it somewhere else.
NEXT_PUBLIC_* is inlined at build time, not read at runtime. Changing it means rebuilding the image, not restarting the container. Two consequences already recorded elsewhere in this repo: a second next dev started against a warm build directory serves the first one’s API URL, and a cloud deployment cannot know its own API URL before the infrastructure that assigns it exists.
4. The .NET stack — containers yes, host no
docker-compose.dotnet.yml interpolates from the same root .env in exactly the same way, so the containerised .NET stack needs no separate configuration.
Running .NET on the host (dotnet run) reads environment variables and appsettings.json — not .env. Export what you need first:
set -a && source .env && set +a # bash/zsh
dotnet run --project agents/dotnet/src/ECommerceAgents.Orchestrator
Adding a new variable
A new setting has to be declared in more than one place. This is the actual reason .env.example runs to 210 lines — not the location of the file.
agents/python/shared/config.py— add the field toSettings, with a default and a comment explaining what it does and what happens when it is wrong..env.example— add it to the right group, with the default and the commentary.docker-compose.yml— add it to theenvironment:block of every service that needs it, as${VAR:-default}. Use the&agent-envanchor when all agents need it.docker-compose.dotnet.yml— the same, if the .NET stack honours it.- The .NET side —
AgentSettings/appsettings.json, if it applies there. docs/deployment.md— add a row to the environment table.
Steps 1 and 3 are the ones that break independently: a field added to Settings but not to the compose block works perfectly on the host and silently uses the default in every container.
Do not add it to .env.minimal. That file exists to stay at one variable.
What must never go in .env
.env is gitignored, but it is still a plaintext file on a development machine. Production secrets belong in a secret manager — Key Vault, or your platform’s equivalent — injected at runtime.
shared/config.py fails fast on weak or placeholder secrets when ENVIRONMENT is not development, and warns loudly even when it is. The placeholders shipped in .env.example are explicitly rejected outside development, so copying it verbatim into a deployed environment cannot work by accident.
Related
- Quick Start — the fastest path to a running stack
- Deployment — the full environment variable table, per service
- Security Guide — secret handling and the threat model
- Releasing — how versions and images are cut
Source: docs/configuration.md — this page is generated from the repository.