# ============================================================================ # Krow API — Docker environment # # cd infrastructure && cp .env.docker.example .env # # Compose reads `.env` from the directory the compose file lives in, so this # copy belongs in infrastructure/, NOT the repository root .env used by # `make run`. Both are gitignored. # # Every value here is a placeholder. No real password or host belongs in a # file that is committed. # ============================================================================ # ── Application ───────────────────────────────────────────────────────────── APP_ENV=production # development | staging | production LOG_LEVEL=info # debug | info | warn | error VERSION=1.0.0 # build arg only IMAGE=doormile/krowbackend:latest # what `docker compose build` tags and `up` runs # ── Where the API is published ────────────────────────────────────────────── # Loopback by default: put a reverse proxy in front to terminate TLS. The API # speaks plain HTTP and its session cookie is Secure outside development, so a # browser will not send that cookie back over a plain connection anyway. API_BIND=127.0.0.1 API_PORT=8080 # Browser origins allowed to call this API, comma-separated. Exact strings: # scheme included, no trailing slash. Cookies are the auth mechanism, so # anything listed here can hold a live session. # # "*" is REJECTED at startup — browsers refuse Allow-Origin: "*" together with # credentials, so it would break every authenticated call rather than loosen # anything. HTTP_CORS_ORIGINS=https://platform.krowforce.com,https://mcp.krowforce.com # SameSite on the session cookie: lax | none | strict. # # "lax" is right while the API is on *.krowforce.com — the two origins above # are then cross-ORIGIN (CORS applies) but same-SITE (the cookie is still # sent). Move the API to any other registrable domain and this must become # "none", or login will succeed and every following request will arrive # anonymous. HTTP_COOKIE_SAMESITE=lax # ── Database ──────────────────────────────────────────────────────────────── # Point at a managed PostgreSQL. With docker-compose.local-db.yml layered on # top, set DATABASE_HOST=postgres instead. DATABASE_HOST=your-database-host.example.com DATABASE_PORT=5432 DATABASE_NAME=krow DATABASE_USER=krow DATABASE_PASSWORD=change-me DATABASE_SCHEMA=public # require encrypts but does not verify the server; verify-full also checks the # certificate against a CA and is what you want against a managed database. # # `disable` is REJECTED at startup when APP_ENV=production. That is deliberate. DATABASE_SSLMODE=require # ── Migration URL ─────────────────────────────────────────────────────────── # golang-migrate takes a single URL, and Compose cannot assemble or encode one. # # ⚠️ PERCENT-ENCODE THE PASSWORD. A password containing @ : / ? # or % will # otherwise be parsed as part of the host or the path, and the failure looks # like a wrong host rather than a wrong password: # # @ → %40 : → %3A / → %2F ? → %3F # → %23 % → %25 # # python3 -c 'import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=""))' 'your-password' # # search_path must match DATABASE_SCHEMA above. DATABASE_URL=postgres://krow:change-me@your-database-host.example.com:5432/krow?sslmode=require&search_path=public # ── Pool and timeouts ─────────────────────────────────────────────────────── DATABASE_MAX_OPEN_CONNS=25 DATABASE_MIN_IDLE_CONNS=2 DATABASE_CONN_MAX_LIFETIME=30m DATABASE_CONNECT_TIMEOUT=10s DATABASE_STATEMENT_TIMEOUT=10s # ── Model gateway ─────────────────────────────────────────────────────────── # THIS BLOCK WAS MISSING and a deployment copying this file could not start: # APP_ENV=production with no MODEL_API_KEY is refused, because the alternative # is an API that accepts agent runs and fails every one of them at the gateway. # # One wire protocol: the openai chat-completions shape. Groq, Gemini, # OpenRouter, Together, vLLM and a local Ollama all serve it, so switching # vendors is a base URL and a model id, not a code change. # # Empty MODEL_PROVIDER means openai — the only implementation. Empty # MODEL_BASE_URL means Groq. MODEL_PROVIDER=openai MODEL_BASE_URL=https://api.groq.com/openai/v1 # REQUIRED in production. There is no ANTHROPIC_API_KEY fallback: that variable # is now REFUSED at startup if it is set while this one is empty, because # silently authenticating to Groq with a key named for a vendor this service # cannot call is a lie the next operator has to unpick. Rename it here, and # replace the value — an Anthropic key boots fine and then fails every run with # 401, which the startup check cannot catch and only the model call can. MODEL_API_KEY= # Model ids must be ones MODEL_BASE_URL actually serves. A leftover claude-* # id is refused at startup by name and tier: nothing configured serves one, so # every run on that tier would 400 at the gateway. MODEL_FAST=llama-3.1-8b-instant MODEL_BALANCED=llama-3.3-70b-versatile MODEL_DEEP=llama-3.3-70b-versatile # Off. Most non-reasoning models — the llama ids above included — reject the # whole request rather than ignoring reasoning_effort. Turn it on only for a # model documented to take it. MODEL_REASONING_EFFORT= # ── HTTP timeouts ─────────────────────────────────────────────────────────── HTTP_READ_TIMEOUT=15s # 180s, not 30s. internal/config REFUSES TO START when this is below the deep # tier's 2m agent deadline: the server would abort the response mid-run and the # caller would see 502 from the proxy in front, a gateway error for something no # gateway did. 30s shipped here for a long time and was the cause of exactly # that incident. Anything at or under 2m0s is a container that will not boot. HTTP_WRITE_TIMEOUT=180s HTTP_IDLE_TIMEOUT=60s HTTP_SHUTDOWN_TIMEOUT=10s # ── Container resources ───────────────────────────────────────────────────── API_CPU_LIMIT=2 API_MEMORY_LIMIT=512M # ── Local database only (docker-compose.local-db.yml) ─────────────────────── POSTGRES_BIND=127.0.0.1 POSTGRES_PORT=5432 POSTGRES_MEMORY_LIMIT=1G