Running Hindsight on Coolify
I wrote about why I keep agent memory in Hindsight. This is the other half: how it actually runs. Self-hosted, on Coolify, behind Caddy, with one bank per repo.
The compose file is the easy part. Everything below it is what I got wrong first.
What you need
A Coolify instance with a worker you are allowed to deploy on. A small box — 2 vCPU and 4 GB is enough, with one condition I will come back to. A domain you can point at it. And an LLM key, because Hindsight extracts facts with a model; it will not run on vibes.
The compose
Create a service in Coolify from a custom compose. Two containers, one volume.
services:
db:
image: 'pgvector/pgvector:pg18'
restart: unless-stopped
shm_size: 1gb
environment:
- POSTGRES_USER=${HINDSIGHT_DB_USER}
- POSTGRES_PASSWORD=${HINDSIGHT_DB_PASSWORD}
- POSTGRES_DB=${HINDSIGHT_DB_NAME}
volumes:
- 'pg-data:/var/lib/postgresql/18/docker'
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}']
interval: 5s
timeout: 20s
retries: 20
hindsight:
image: 'ghcr.io/vectorize-io/hindsight:latest'
restart: unless-stopped
expose:
- 8888
- 9999
environment:
- SERVICE_FQDN_HINDSIGHT_8888
- SERVICE_FQDN_HINDSIGHT_9999
- HINDSIGHT_API_DATABASE_URL=postgresql://${HINDSIGHT_DB_USER}:${HINDSIGHT_DB_PASSWORD}@db:5432/${HINDSIGHT_DB_NAME}
- HINDSIGHT_API_WORKER_ID=${HINDSIGHT_API_WORKER_ID}
- HINDSIGHT_API_LLM_PROVIDER=${HINDSIGHT_API_LLM_PROVIDER}
- HINDSIGHT_API_LLM_MODEL=${HINDSIGHT_API_LLM_MODEL}
- HINDSIGHT_API_TENANT_EXTENSION=${HINDSIGHT_API_TENANT_EXTENSION}
- HINDSIGHT_API_TENANT_API_KEY=${HINDSIGHT_API_TENANT_API_KEY}
- HINDSIGHT_CP_ACCESS_KEY=${HINDSIGHT_CP_ACCESS_KEY}
- HINDSIGHT_CP_DATAPLANE_API_KEY=${HINDSIGHT_CP_DATAPLANE_API_KEY}
depends_on:
db:
condition: service_healthy
volumes:
pg-data:
Two ports because Hindsight is two things: the API on 8888 and a control plane UI on 9999.
shm_size: 1gb is not decoration. Postgres defaults to 64 MB of shared memory in Docker, and the document transfer endpoint dies on it once you move real volume through. I found that out by importing four thousand documents and watching it fail somewhere in the middle.
Auth
Three variables, and the third one is the one people miss.
HINDSIGHT_API_TENANT_EXTENSION set to the API-key tenant extension, plus HINDSIGHT_API_TENANT_API_KEY, gets you an API that returns 401 without a key. /health stays open, which is what you want for a healthcheck.
HINDSIGHT_CP_ACCESS_KEY protects the UI. And HINDSIGHT_CP_DATAPLANE_API_KEY has to be set to the same value as the tenant key — otherwise the control plane cannot talk to its own API and you get a UI that loads and then shows nothing.
Four Coolify-specific traps
Do not write your own caddy_* labels. Coolify regenerates proxy labels from the fqdn field and throws yours away. Worse, in my case the deploy then failed silently — containers just vanished, no error anywhere. Declare the ports as bare SERVICE_FQDN_<SERVICE>_<PORT> entries and let Coolify produce caddy_0 and caddy_1.
Service FQDNs cannot be set through the API. Applications accept a domains field on PATCH. Services do not, still not in 4.3.5. You set service_applications.fqdn in Coolify’s own Postgres, format https://a.example:8888,https://b.example:9999, then redeploy.
Env changes only reach the server on a full deploy. A restart leaves the old .env in /data/coolify/services/<uuid>/ untouched, and you debug a value that is not there.
When Coolify goes quiet, ask Docker. cd /data/coolify/services/<uuid> && docker compose up -d on the box prints the real error. Also: after stop and start, Coolify re-pulls :latest, which takes minutes. Checking too early shows “no containers” and looks exactly like a crash.
Do DNS before domains
Set the A record first, then put the domain into Coolify. Not the other way around.
If the hostname is configured before DNS resolves, Caddy starts asking Let’s Encrypt for a certificate it cannot validate. Five failed authorizations per hostname per hour and you are rate-limited, sitting there watching a service that is running fine serve no TLS. I burned that limit on several hostnames in one afternoon. With DNS in place first, the certificate arrives on the first try.
The model has to be boring
gpt-5-mini does not work. Reasoning models only accept temperature=1, Hindsight sends 0.1, and you get a BadRequestError that says nothing about why. A normal chat model is fine — I run gpt-5.4-mini through litellm against Azure.
You can also point retain, reflect and consolidation at different models with HINDSIGHT_API_{RETAIN,REFLECT,CONSOLIDATION}_LLM_MODEL. I have not needed to.
The reranker is the whole ballgame
This is the part I would put first if I were writing this for myself a week ago.
Out of the box, Hindsight reranks with a neural cross-encoder. On a small CPU box that means recall takes nine seconds, idle, every time. I nearly threw the whole thing out over it.
What saved it was setting trace: true on a recall call and reading the phase breakdown. Total 9.82 s. Reranking: 8.87 s of that. One phase, scoring 300 candidates at about 30 ms each, on a CPU with no business doing it.
Moving the reranker to a hosted endpoint took it to 1.1–1.7 seconds total, 0.24 s for reranking, with the same candidate count. Nothing else changed.
- HINDSIGHT_API_RERANKER_PROVIDER=${HINDSIGHT_API_RERANKER_PROVIDER}
- HINDSIGHT_API_RERANKER_COHERE_BASE_URL=${HINDSIGHT_API_RERANKER_COHERE_BASE_URL}
- HINDSIGHT_API_RERANKER_COHERE_MODEL=${HINDSIGHT_API_RERANKER_COHERE_MODEL}
- HINDSIGHT_API_RERANKER_COHERE_API_KEY=${HINDSIGHT_API_RERANKER_COHERE_API_KEY}
Any Cohere-compatible rerank endpoint works. I use Azure.
So: measure before you judge. And that 2 vCPU sizing from the top only holds because the reranker is remote. If you want it local, buy cores.
Wiring the clients
The Claude Code plugin reads ~/.hindsight/claude-code.json:
{
"hindsightApiUrl": "https://memory.example.com",
"hindsightApiToken": "…",
"dynamicBankId": true,
"dynamicBankGranularity": ["project"],
"autoRecall": true,
"autoRetain": true
}
dynamicBankGranularity is ["project"] deliberately, without agent. Add agent and Claude Code and Codex write into separate banks for the same repo, which defeats the point. Git worktrees resolve to the main repo name on their own.
For anything that is not Claude Code — Grok, Cursor — a five-line wrapper gets you the same MCP server reading the same config:
#!/usr/bin/env bash
set -e
export CLAUDE_PLUGIN_ROOT="$HOME/.claude/plugins/cache/hindsight/hindsight-memory/<version>"
export CLAUDE_PLUGIN_DATA="$HOME/.claude/plugins/data/hindsight-memory"
CFG="$HOME/.hindsight/claude-code.json"
export HINDSIGHT_API_URL="$(jq -r .hindsightApiUrl "$CFG")"
export HINDSIGHT_API_TOKEN="$(jq -r .hindsightApiToken "$CFG")"
exec "$CLAUDE_PLUGIN_ROOT/scripts/run_mcp.sh" "$@"
One config file, one server, every agent. Change the URL once and all of them follow.
Checking it works
The API paths are prefixed and not guessable. /banks is a 404. What you want is /v1/default/banks and /v1/default/banks/<bank>/memories/recall. /openapi.json is served and is the fastest way to find the rest.
The real test is not /health. It is whether autoRetain writes during actual sessions. Query the database directly rather than trusting the UI:
select bank_id, count(*), max(created_at)
from documents
where created_at >= '<date>'
group by 1 order by 2 desc;
If the counts climb across several repos over a few days, it is working. Mine went to forty-four new documents across seven banks in the first two days, which is when I stopped worrying about it.
Worth it?
Setup was an evening, and most of that was the reranker detour. It has been running since, at about 1.2 GB of RAM, on its own small box away from anything I deploy to — because the one thing I do not want is the memory dying with the machine I was debugging.