Files
krow_backend/infrastructure/.env.docker.example
Suriyakumarvijayanayagam bd9a8f91fc
Some checks failed
CI / test (push) Failing after 4m40s
CI / fixture (push) Failing after 9s
Make the shipped example envs ones that can actually start
The krow-2 deploy failed on the ANTHROPIC_API_KEY guard, which is the guard
doing its job. Checking what an operator hits *after* fixing it turned up two
older faults in the files they are told to copy — both predating the Groq
switch, both fatal at boot.

HTTP_WRITE_TIMEOUT shipped as 30s in .env.example, .env.docker.example and the
compose default, while validateWriteTimeout refuses anything at or under the
deep tier's 2m deadline. `cp .env.docker.example .env && docker compose up`
could not start. Now 180s. krow-2 never saw this because someone had already
overridden it in that environment.

.env.docker.example carried no model block at all, so a production stack built
from it is refused for a missing MODEL_API_KEY. Added, with the Groq defaults
and the reasoning-effort note (most non-reasoning models reject the request
rather than ignoring the key).

Neither was subtle. Both survived because the examples were prose to every test
in this package: the validator and the file documenting it had no mechanical
connection, so tightening one silently invalidated the other. That connection
is now TestShippedExampleEnvActuallyBoots, which parses each example and runs
Load() on it under the APP_ENV the file itself declares — production for the
docker one, development for the root one, each internally consistent. Verified
by mutation: reverting the timeout, removing the key line, and restoring a
claude-* id each fail it with the message an operator would see.

Go does not treat these files as test inputs, so an example-only edit can be
served a stale pass from the test cache. Noted in the test; use -count=1.

Also documented the upgrade path in handover.md, including the one thing
startup validation cannot catch: renaming ANTHROPIC_API_KEY to MODEL_API_KEY
without replacing the value boots fine and 401s on every run.

gofmt clean, go vet clean, 15/15 packages pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJvibeSc1JYXjatankqM1g
2026-09-07 12:25:20 +05:30

135 lines
7.0 KiB
Plaintext

# ============================================================================
# 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