# --------------------------------------------------------------------------- # Copy to `.env` and fill in. # # ONLY TWO LINES ARE REQUIRED: POSTGRES_PASSWORD and ALLOWED_HOSTS. # # Everything in the "generated" section below is created on first boot and # persisted to the state volume, and everything in the "set in the browser" # section is configured by the /setup wizard. They are listed here so you know # they exist and can override them — not because you have to fill them in. # # Coolify trap: an env var set to an empty string is treated as UNSET by this # app, on purpose. Delete the line instead of blanking it. # --------------------------------------------------------------------------- # ===== REQUIRED ============================================================= # Password for the bundled Postgres container. Any long random string. POSTGRES_PASSWORD= # The domain(s) this server answers on. Comma-separated, no scheme. # Getting this wrong 400s every request INCLUDING the page that would fix it, # which is exactly why it stays in the environment and not in the UI. ALLOWED_HOSTS=localhost,127.0.0.1 # ===== GENERATED ON FIRST BOOT — leave blank unless you have a reason ======== # # Written once to $BRAIN_STATE_DIR/boot-secrets.json and reused forever. Set # any of them here and yours wins. Back that file up alongside your database: # losing FIELD_ENCRYPTION_KEY makes every stored credential unreadable. # Signs sessions. python -c "import secrets; print(secrets.token_urlsafe(64))" SECRET_KEY= # Encrypts the Claude credential and git credentials at rest in the DB. # python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" FIELD_ENCRYPTION_KEY= # Authenticates the web container to the mcp container. MCP_LOOPBACK_SECRET= # Django's own admin, on a non-guessable path. Bots scan the default one. DJANGO_ADMIN_URL_PATH= # ===== SET IN THE BROWSER (/setup) — env here overrides the UI ============== # The git repo that IS your brain. Start from the brainoutside-template repo. BRAIN_REPO_URL= # Read-only deploy key used for cloning and every routine pull. Set this to # mount your own key file instead of letting the app generate a keypair. BRAIN_GIT_SSH_KEY_PATH= # The WRITE credential — a fine-grained PAT with contents:write on this ONE # repo. Deliberately separate from the read key: a stolen read key leaks, a # stolen write key poisons the mind. Mount it into the worker container ONLY # to keep that split; otherwise set it in the UI and it lives encrypted in # the DB (see docs/SECURITY.md for the tradeoff). BRAIN_GIT_WRITE_PAT_PATH= # Shared secret for GitHub's push webhook (HMAC-verified). Without it the # server still picks up pushes on its periodic pull, just more slowly. GITHUB_WEBHOOK_SECRET= # Author on approval commits, so server writes are distinguishable from yours. BRAIN_COMMIT_NAME= BRAIN_COMMIT_EMAIL= # ===== INFRASTRUCTURE — compose already sets these ========================== # Shown in the browser tab, the landing page and the login screen. This is # only the first-boot value — /ops/settings/ overrides it, and the stored # name wins from then on. APP_NAME=BrainOutside DJANGO_SETTINGS_MODULE=config.settings.prod DEBUG=false PUBLIC_BASE_URL=http://localhost:8000 # Where the ops UI lives. It can approve feeds and read every private note — # put the whole prefix behind a network boundary (Tailscale, Cloudflare # Access, or ADMIN_IP_ALLOWLIST). See docs/SECURITY.md. ADMIN_PANEL_URL_PATH=ops/ # Comma-separated CIDRs allowed to reach the ops prefix. Empty = no IP check. # Behind a proxy this is compared against the address resolved below — set # TRUSTED_PROXY_* FIRST or you will be comparing every caller against your # proxy and lock yourself out. ADMIN_IP_ALLOWLIST= # Real client IP behind a CDN / reverse proxy. Leave both empty for a # direct-to-origin deploy. # # Behind a proxy, REMOTE_ADDR is the proxy, so every per-IP control # (admin-login lockout, honeypot, ADMIN_IP_ALLOWLIST) buckets all callers # together — which INVERTS the login lockout: an attacker's failures lock # out the operator while the attacker rotates edge nodes. # # TRUSTED_PROXY_IP_HEADER the header carrying the caller's address # ("CF-Connecting-IP" for Cloudflare, # "X-Forwarded-For" for a plain reverse proxy). # TRUSTED_PROXY_IPS comma-separated CIDRs the proxy connects FROM, # matched against REMOTE_ADDR. This is the hop # adjacent to this app — behind Coolify that is # the Traefik container on the Docker network, # NOT Cloudflare's ranges. The exposure panel on # /ops/health/ prints the observed value. # # The header is honoured ONLY on requests whose peer is in the list, so a # direct-to-origin caller cannot forge it. Setting one without the other # refuses to boot in prod. TRUSTED_PROXY_IP_HEADER= TRUSTED_PROXY_IPS= # Per-minute cap for callers with no API key and no operator session, # bucketed on the resolved client IP. A backstop for paths nobody sized # explicitly — a genuinely public endpoint should get its own Consumer key # and limit instead of leaning on this. Only meaningful once TRUSTED_PROXY_* # above is set: without it every anonymous caller shares the proxy's bucket, # so one abusive caller throttles everyone. ANONYMOUS_RATE_LIMIT_PER_MIN=20 DATABASE_URL= REDIS_URL=redis://redis:6379/0 # Container paths, tied to the volume mounts in docker-compose.yml. BRAIN_REPO_DIR=/data/brain-repo BRAIN_VIEWS_DIR=/data/brain-views # Generated boot secrets live here. MUST be a persisted volume. BRAIN_STATE_DIR=/data/state # Largest payload `propose-feed` accepts — raise it for long transcripts. FEED_PAYLOAD_MAX_KB=512