Files
krow_backend/infrastructure/.env.docker.example
Aravind f2aa3b3ad8
Some checks failed
CI / fixture (push) Has been cancelled
CI / test (push) Has been cancelled
mcp connection
2026-09-22 10:58:02 +05:30

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