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 earlier parents[2] resolved to <repo>/agents, which contains no .env, so Pydantic’s env_file loading never fired and every setting silently fell back to its default even with a real .env present.
  • Inside the Docker image, config.py is copied flatly to /app/shared/config.py — the build context is ./agents/python, so that path depth does not exist. parents[3] raised IndexError and crashed every container at import time. _resolve_repo_root falls back to the immediate parent there, which contains no .env — correct, because containers get their config from the compose environment: 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.

  1. agents/python/shared/config.py — add the field to Settings, with a default and a comment explaining what it does and what happens when it is wrong.
  2. .env.example — add it to the right group, with the default and the commentary.
  3. docker-compose.yml — add it to the environment: block of every service that needs it, as ${VAR:-default}. Use the &agent-env anchor when all agents need it.
  4. docker-compose.dotnet.yml — the same, if the .NET stack honours it.
  5. The .NET sideAgentSettings / appsettings.json, if it applies there.
  6. 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.


Source: docs/configuration.md — this page is generated from the repository.