Files
krow_backend/infrastructure/docker-compose.yml
Suriyakumarvijayanayagam 34fa58a6b9
Some checks failed
CI / test (push) Failing after 4m38s
CI / fixture (push) Failing after 7s
Remove the Anthropic path; the gateway speaks one wire protocol
The platform now runs on Groq by default, through the OpenAI-compatible
chat-completions shape. That shape is not one vendor — Gemini, OpenRouter,
Together, vLLM and a local Ollama serve it too — so moving again stays
configuration rather than code.

Two things in the deleted file were not Anthropic's and would have gone
with it silently:

  withRetry / MaxAttempts / retryBackoff were defined in anthropic.go and
  CALLED BY openai.go. Deleting the file wholesale would have removed the
  retry policy of the provider that survived, and nothing in openai.go
  mentions it, so the loss would have been invisible until the next 429.
  The policy is a property of this platform's runs, not of a vendor's API;
  it now lives in retry.go where no provider can carry it off.

  StreamComplete had the same problem and moves to gateway.go, beside the
  Streamer interface whose comment already referenced it.

Three stale-configuration failures are now refused at startup instead of
being ignored. Each was verified firing through the real config.Load():

  MODEL_PROVIDER=anthropic — named separately from every other wrong value
  because it used to be correct. Ignoring it gives a stack that believes it
  is on Claude while every run goes to Groq and is billed there.

  ANTHROPIC_API_KEY set while MODEL_API_KEY is empty. Ignoring a key an
  operator did set is the worst version of this: they fail every run on a
  missing credential they are looking straight at.

  A leftover claude-* model id, naming the tier that carries it. This is
  the check the previous commit's error-detail work was diagnosing: such an
  id is accepted by this process, rejected by the provider, and 400s on
  EVERY run. "A model is wrong" does not say which of three lines to edit.

Defaults ship as a matched pair. defaultBaseURL and the three tier ids are
one decision, not four: an id is only meaningful against the service that
serves it, and a Groq id on an OpenAI base URL is the same failure from the
other side. The tiers also stop being one model — a tier whose cost does
not differ is a distinction that buys nothing.

Verified end to end against a stub of the wire, driving the real wiring
(config.Load in production mode, gateway.New, StreamComplete): streamed
deltas, tool-call decoding, the loopback credential exemption, and usage
totalling 150 rather than 190 — the cached-prefix subtraction still holds.

gofmt clean, go vet clean, 14/14 non-DB packages pass. httpserver still
needs a reachable database.

NOT verified: the I7 planted-injection eval. Removing this path removed the
only model whose refusal behaviour had been measured against it, so the new
default is unproven there until `make eval-live` runs with a real key. The
Groq model ids should also be confirmed against Groq's current lineup.
Flagged in CLAUDE.md §12 and docs/handover.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJvibeSc1JYXjatankqM1g
2026-09-05 11:52:21 +05:30

179 lines
8.1 KiB
YAML

# ============================================================================
# Krow API — production stack
#
# cd infrastructure
# cp .env.docker.example .env
# docker compose up -d --build
#
# This file assumes a MANAGED PostgreSQL (RDS, Cloud SQL, Neon, a DBA's
# cluster) reachable from wherever these containers run. That is deliberate:
# a production database wants backups, point-in-time recovery, failover and a
# patch schedule, none of which a container in this file provides.
#
# To run PostgreSQL alongside the API — for staging, or to test the stack
# end-to-end on one host — layer the override on top:
#
# docker compose -f docker-compose.yml -f docker-compose.local-db.yml up -d
#
# ── ORDER OF OPERATIONS ─────────────────────────────────────────────────────
#
# `migrate` runs to completion BEFORE `api` starts, and `api` will not start if
# it fails. That is the expand/migrate/contract discipline from README.md
# expressed in the dependency graph rather than in a runbook: schema first,
# binary second, and every migration backwards-compatible with the version
# still running.
#
# ── TLS IS NOT OPTIONAL HERE ────────────────────────────────────────────────
#
# internal/config rejects DATABASE_SSLMODE=disable when APP_ENV=production, so
# this stack will refuse to start against a database that cannot do TLS. That
# is the intended behaviour and not something to work around by dropping to
# APP_ENV=staging — a staging flag on a production deployment is a lie the next
# person has to discover.
# ============================================================================
name: krow
# The database settings the API and the migration runner must agree on.
x-database-env: &database-env
DATABASE_HOST: ${DATABASE_HOST:?DATABASE_HOST is required}
DATABASE_PORT: ${DATABASE_PORT:-5432}
DATABASE_NAME: ${DATABASE_NAME:?DATABASE_NAME is required}
DATABASE_USER: ${DATABASE_USER:?DATABASE_USER is required}
DATABASE_PASSWORD: ${DATABASE_PASSWORD:?DATABASE_PASSWORD is required}
DATABASE_SCHEMA: ${DATABASE_SCHEMA:-public}
DATABASE_SSLMODE: ${DATABASE_SSLMODE:-require}
x-logging: &logging
driver: json-file
options:
max-size: "10m"
max-file: "5"
services:
# ── Schema ────────────────────────────────────────────────────────────────
# One-shot. Exits 0 when the schema is current, including when there was
# nothing to apply. `api` waits on that exit code.
#
# DATABASE_URL is a single pre-built string rather than assembled from the
# parts above because golang-migrate takes a URL, and a password containing
# @ : / ? or # must be percent-encoded in one. Compose cannot encode it, so
# the encoding is the operator's job — see .env.docker.example.
migrate:
image: migrate/migrate:v4.19.0
container_name: krow-migrate
volumes:
- ../migrations:/migrations:ro
command:
- -path=/migrations
- -database=${DATABASE_URL:?DATABASE_URL is required — see .env.docker.example}
- up
restart: "no"
logging: *logging
networks: [krow]
# ── API ───────────────────────────────────────────────────────────────────
api:
build:
context: ..
dockerfile: infrastructure/Dockerfile.api
args:
VERSION: ${VERSION:-dev}
image: ${IMAGE:-doormile/krowbackend:latest}
container_name: krow-api
depends_on:
migrate:
condition: service_completed_successfully
environment:
<<: *database-env
APP_ENV: ${APP_ENV:-production}
LOG_LEVEL: ${LOG_LEVEL:-info}
# The agent run routes are not registered without this, so a deployment
# without it answers 404 on /agents/{id}/runs and reports three fewer
# endpoints on /version. Empty by default: absent is a working API
# without Owliver, which is a legitimate way to run this.
# Provider selection. Empty MODEL_PROVIDER means openai — the only
# implementation — and empty MODEL_BASE_URL means Groq.
#
# ANTHROPIC_API_KEY is passed through DELIBERATELY even though nothing
# reads it: a host that still exports it gets a loud startup failure
# telling it to rename the variable, instead of a container that silently
# boots with no credential and fails every agent run.
MODEL_PROVIDER: ${MODEL_PROVIDER:-}
MODEL_BASE_URL: ${MODEL_BASE_URL:-}
MODEL_API_KEY: ${MODEL_API_KEY:-}
MODEL_REASONING_EFFORT: ${MODEL_REASONING_EFFORT:-}
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
MODEL_FAST: ${MODEL_FAST:-}
MODEL_BALANCED: ${MODEL_BALANCED:-}
MODEL_DEEP: ${MODEL_DEEP:-}
# voyage | ollama | lexical. Unset means ingest stores chunks that are
# keyword-searchable only — see the ingest output.
EMBED_PROVIDER: ${EMBED_PROVIDER:-}
EMBED_MODEL: ${EMBED_MODEL:-}
EMBED_DIMENSIONS: ${EMBED_DIMENSIONS:-}
# Ollama runs on the HOST, so "localhost" here would be this container.
# host.docker.internal is what reaches the host from inside it.
EMBED_BASE_URL: ${EMBED_BASE_URL:-}
VOYAGE_API_KEY: ${VOYAGE_API_KEY:-}
# 0.0.0.0 so the published port actually reaches the process. The binary
# defaults to 127.0.0.1, which inside a container means "nobody".
HTTP_HOST: 0.0.0.0
HTTP_PORT: "8080"
HTTP_READ_TIMEOUT: ${HTTP_READ_TIMEOUT:-15s}
HTTP_WRITE_TIMEOUT: ${HTTP_WRITE_TIMEOUT:-30s}
HTTP_IDLE_TIMEOUT: ${HTTP_IDLE_TIMEOUT:-60s}
HTTP_SHUTDOWN_TIMEOUT: ${HTTP_SHUTDOWN_TIMEOUT:-10s}
# Browser origins allowed to call this API. Empty means same-origin only,
# and the CORS middleware is then not installed at all. Cookies are the
# auth mechanism, so any origin listed here can hold a session.
#
# "*" is refused at startup: browsers reject Allow-Origin "*" together
# with credentials, so it would break authenticated calls rather than
# loosen anything.
HTTP_CORS_ORIGINS: ${HTTP_CORS_ORIGINS:-}
# lax | none | strict, or unset to let the server choose.
#
# "none" is required when the frontend is on a different registrable
# DOMAIN from the API — otherwise the browser withholds the cookie
# however correct the CORS headers are. A different ORIGIN on the same
# domain (platform.krowforce.com → mcp.krowforce.com) needs CORS but not
# this: a Lax cookie already travels between them, and "none" would give
# up the only CSRF protection this API has.
#
# Unset, the server derives it from HTTP_CORS_ORIGINS. The default below
# is deliberate: a compose deployment keeps Lax unless told otherwise.
HTTP_COOKIE_SAMESITE: ${HTTP_COOKIE_SAMESITE:-lax}
DATABASE_MAX_OPEN_CONNS: ${DATABASE_MAX_OPEN_CONNS:-25}
DATABASE_MIN_IDLE_CONNS: ${DATABASE_MIN_IDLE_CONNS:-2}
DATABASE_CONN_MAX_LIFETIME: ${DATABASE_CONN_MAX_LIFETIME:-30m}
DATABASE_CONNECT_TIMEOUT: ${DATABASE_CONNECT_TIMEOUT:-10s}
DATABASE_STATEMENT_TIMEOUT: ${DATABASE_STATEMENT_TIMEOUT:-10s}
ports:
# Bound to loopback by default. A reverse proxy in front of this is what
# terminates TLS; publishing 0.0.0.0:8080 would put a plain-HTTP service
# carrying session cookies straight onto the network.
- "${API_BIND:-127.0.0.1}:${API_PORT:-8080}:8080"
restart: unless-stopped
stop_grace_period: 30s
read_only: true
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
tmpfs:
- /tmp:size=32m
deploy:
resources:
limits:
cpus: "${API_CPU_LIMIT:-2}"
memory: ${API_MEMORY_LIMIT:-512M}
reservations:
memory: 128M
logging: *logging
networks: [krow]
networks:
krow:
driver: bridge