175 lines
9.1 KiB
Plaintext
175 lines
9.1 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
|
|
|
|
# Networks whose X-Forwarded-For header may be believed. Comma-separated CIDR
|
|
# blocks or bare addresses.
|
|
#
|
|
# THIS DEPLOYMENT NEEDS IT SET, AND THE VALUE IS NOT KNOWABLE FROM THIS REPO.
|
|
#
|
|
# The API binds loopback and something else terminates TLS in front of it, so
|
|
# Go sees the proxy's address on every request. Three limits are keyed by that
|
|
# address — failed logins (20 per 15 minutes), OAuth client registration (10
|
|
# per hour) and OAuth authorization before sign-in (20 per hour) — and while it
|
|
# is unset, all three are ONE budget for the whole deployment. The observable
|
|
# symptoms are users rate-limiting each other: a connector that refuses to
|
|
# register because somebody else already did, and sign-in refused platform-wide
|
|
# after twenty bad passwords anywhere.
|
|
#
|
|
# Set it to the address or network the ingress reaches the API from. Find it
|
|
# rather than guess it — on the API host, with the stack running:
|
|
#
|
|
# docker inspect -f '{{range .NetworkSettings.Networks}}{{.Gateway}}{{end}}' krow-api
|
|
#
|
|
# That gateway is the address a proxy on the host arrives as. If the proxy runs
|
|
# in a container on a shared Docker network, use that network's subnet instead:
|
|
#
|
|
# docker network inspect -f '{{range .IPAM.Config}}{{.Subnet}}{{end}}' <network>
|
|
#
|
|
# Confirm before committing to it: with LOG_LEVEL=debug, one request's logged
|
|
# address should be the real client's, not the proxy's.
|
|
#
|
|
# NEVER 0.0.0.0/0. That trusts every caller's own header — not a weaker limit
|
|
# but no limit at all, since anyone could then mint a budget per request.
|
|
#
|
|
# Left unset here deliberately. An operator supplying a wrong value gets the
|
|
# shared bucket back; an operator supplying 0.0.0.0/0 gets no protection at all,
|
|
# so this file ships no value rather than a plausible-looking one to copy.
|
|
HTTP_TRUSTED_PROXIES=
|
|
|
|
# ── 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=openai/gpt-oss-20b
|
|
MODEL_BALANCED=openai/gpt-oss-120b
|
|
MODEL_DEEP=openai/gpt-oss-120b
|
|
|
|
# Safe to turn on with the gpt-oss ids above: both accept reasoning_effort at
|
|
# low, medium and high, which is exactly the scale the gateway maps its three
|
|
# efforts onto. VERIFIED against the live Groq API, not assumed.
|
|
#
|
|
# It is NOT portable. qwen/qwen3.6-27b on the same account rejects all three
|
|
# ("must be one of `none` or `default`") and fails the whole request rather than
|
|
# ignoring the key, so switching model id and leaving this on breaks every run.
|
|
# Re-check it whenever MODEL_* changes.
|
|
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
|