Start here: the dashboard’s top bar tells you whether the database is up and the model is ready, and a
failing project shows its error verbatim in the detail view. The logs are
docker compose logs -f.
Startup
The container keeps restarting
The log line above the exit says which process died.
- “PostgreSQL exited during startup” — the PostgreSQL output above it explains why. Usually a data
directory created by another PostgreSQL major version, or a bind-mounted
CONTEXTATOR_PGDATA_PATHwhose permissions could not be fixed. On Docker Desktop, moving the database back to a named volume is the reliable fix. - An invalid configuration — the server prints every problem and exits. Fix
.envand start again.
The database was created with EMBEDDING_DIMENSIONS=… but the current config says …
You changed the embedding dimensions. Either restore the old value, or start once with
RESET_VECTORS=1 (drops every chunk) and re-index everything. See Embedding Models.
The dashboard says “Model loading” for a long time
The first start downloads 470 MB. Watch docker compose logs -f for downloading model file and then
embedding model ready. To download less, set EMBEDDING_DTYPE=q8 (120 MB) before the first start.
On an air-gapped host, pre-populate the models volume and set EMBEDDING_OFFLINE=1.
Until the model is ready, indexing and search fail while the rest of the dashboard works.
Could not load the sharp module
The lockfile was generated on another platform. Regenerate package-lock.json on Linux, or run
npm install --os=linux --cpu=x64 sharp before building the image.
Signing in
The dashboard sends me to /setup
This instance has no account yet. The setup code is in the log — docker compose logs -f — printed in
a box at every start until the first account exists. See
Accounts and Permissions.
That setup code is not the one in the server log
You are reading an older box. A generated code is replaced on every restart, so use the newest one, or
set SETUP_CODE in .env and restart to fix it in place. Case, dashes and spacing do not matter.
Signing in succeeds, then bounces straight back to /login
The browser is dropping the session cookie. On a plain-HTTP address — a LAN IP or hostname without TLS
— set AUTH_COOKIE_SECURE=0 and restart. Behind a proxy that terminates TLS, set it to 1.
Wrong username or password, and I am sure it is right
The message is deliberately the same for an unknown username and a wrong password, so first check the
username exists. If it does, the account may be new with a temporary password that has since been
reset — ask whoever created it. Too many failed attempts; this account is locked for a while means
the lockout is running: it starts at AUTH_LOGIN_WINDOW_MIN minutes and doubles, up to an hour.
Sign-in says too many attempts
Rate limiting, not a lockout on its own account: per IP address, AUTH_LOGIN_MAX_ATTEMPTS failures
inside AUTH_LOGIN_WINDOW_MIN minutes get a 429 with a Retry-After. Wait it out, or raise either
variable.
Nobody can sign in at all
See I forgot my password. Short version: another admin resets it, or
ADMIN_TOKEN does it over the API, or npm run reset-password -- <username> on a source install.
I cannot delete the last root account
By design — the server answers 409 and the dashboard disables the button, so an instance can never
lock itself out of its own user management. Promote somebody else to root first. See
Accounts and Permissions.
A project I know exists answers 404
Your account is not a member of it. That is deliberate — no access and no such project answer the same
way — so ask a root or admin to add you under Members on the project page.
Sources
Directory is outside the allowed document roots
The path must resolve inside ALLOWED_DOC_ROOTS — inside Docker that means /docs/.... Either use such
a path, or mount the folder and add it to ALLOWED_DOC_ROOTS, then restart. See
Local Directory Source.
A git source shows an authentication error
Test connection reports the remote’s own answer. Check that the token has read access to the
repository and has not expired. Leave Username empty for a Bitbucket repository or workspace access
token (the default x-token-auth is for those only); a Bitbucket Cloud API token needs your
Bitbucket username, or x-bitbucket-api-token-auth, in that field; a GitLab deploy token needs its
own generated username, e.g. gitlab+deploy-token-42 — the default oauth2 is refused for one. See
Git Repository Source for the full table by
provider.
Subdirectory "…" does not exist in the repository
The path is relative to the repository root and is checked against the branch you configured. Confirm both.
Branch "…" was not found on the remote
Wrong branch name — master rather than main is the usual cause.
Adding a private git, Notion or Confluence source fails on SECRET_KEY
Set SECRET_KEY (32+ characters, e.g. openssl rand -hex 32) and restart. It is required only once a
source needs to store a token.
A source that used to work now says the token cannot be decrypted
SECRET_KEY changed. If you still have the old key, there is no need to re-enter anything: set
SECRET_KEY_PREVIOUS to it alongside the new SECRET_KEY, restart, run npm run rotate-secret
(docker exec contextator npm run rotate-secret for a Docker install) to re-encrypt every stored token
under the new key, then remove SECRET_KEY_PREVIOUS and restart once it reports nothing left under it.
If the old key is gone entirely, re-enter the token for that source instead — it cannot be recovered.
See Security.
Notion imports nothing
Nothing is shared with the integration yet. In Notion, share the pages or databases with it (… → Connections). A page shared this way includes its children. See Notion.
Notion says no root could be read
The token is wrong or the root ids are not shared with the integration. Contextator deliberately fails here instead of reporting an empty workspace, which would delete every page already imported.
An upload rejected some files
The response lists what was skipped and why: an extension the source does not index, a dot-directory, a name that is not portable across platforms, an entry that would escape the destination, or a size cap. Adjust the source’s file types, or the limits in Configuration.
Indexing
A project is stuck in indexing
Live job state lives in memory, so a restart mid-run can leave the status behind. Press Re-index — the run is incremental and resumes cheaply, and the status is rewritten.
The project is error but most documents are there
One source failed and the rest indexed. The message reads 2/3 sources synced; <name>: <reason>, and
the failing source’s row carries the full error. Fix it and press Sync on that row. Documents from a
source that could not be read are never deleted.
Documents disappeared
Either the files really are gone from the source (a deleted file is removed from the index on the next run), or you changed the file-type selection so they are no longer indexed. A failing sync does not delete documents.
A file is not indexed
Check, in order:
- Its extension is selected on the source (
.mdand.mdxby default;.txt,.html/.htm,.csv,.docxand.pdfare opt-in). - It could be converted at all — there is no OCR, so a scanned or encrypted PDF, or a Word file that is all images, is refused by name on the source’s row rather than half-indexed.
- It is not inside a dot-directory,
node_modules,dist,build,vendoror__pycache__. - It does not match
IGNORE_GLOBS. - It is not empty after the content-type transform — an empty document is skipped.
- It is not reached through a symlink pointing outside the source.
CHUNK_MAX_TOKENS=… exceeds what … reads
The chunking budget is larger than the embedding model reads usefully. The shipped default fits the
shipped model, so seeing this means one of the two was changed on its own. Set CHUNK_MAX_TOKENS to the
value the message suggests and re-index. Running a model this build does not know about? State its
context window in EMBEDDING_MAX_INPUT_TOKENS. See
Configuration.
Indexing is slow
Embedding is CPU-bound and one project is indexed at a time. The first run of a large project is the
expensive one; subsequent runs skip unchanged files. EMBEDDING_DTYPE=q8 or the smaller
Xenova/all-MiniLM-L6-v2 model both speed it up.
Search and clients
The agent cannot connect
- The project name in the URL must match exactly —
Unknown project "…"means it does not. - From another machine, use the server’s address rather than
localhost, and check the firewall. - A browser-based client needs its origin in
ALLOWED_ORIGINS.
search_docs says the project was indexed with another model
The server’s embedding model changed. Re-index the project — the next run is automatically a full one.
The agent finds nothing
- Confirm the project has documents and chunks in the dashboard.
- Try
list_topicsto see what was actually indexed. - Ask a fuller question: search is hybrid — meaning and exact wording at once — and a question carries more signal for the meaning half than two keywords do.
- If nothing at all comes back, the relevance floor may be refusing it:
search_docsanswers no good match belowSEARCH_SCORE_FLOOR(0.82by default, a cosine similarity measured against the default embedding model). If you changedEMBEDDING_MODEL, the startup log says so — re-measure the floor withnpm run evalagainst your own corpus rather than reusing the shipped default, or setSEARCH_SCORE_FLOOR=0to turn every project’s own floor off as well. The server logs every gated query atinfowith the score it saw, and names the projects that carry their own floor in its startup warning. - If the documentation is one enormous file with no headings, chunking has little to work with — add headings.
An agent is told no good match for something that is documented
The floor refused a question it should not have. Compare the score the server logged for that query
against the floor in effect — a project’s own if it set one, else SEARCH_SCORE_FLOOR. A corpus that
scores lower than the rest of the instance (prose more than reference pages) is usually better served by
lowering that one project’s floor in the query-log panel, which previews the change before you commit
it, than by lowering the server-wide setting.
Results are poor
- With the local model, try
CHUNK_MAX_TOKENS=250and force a re-index (Indexing). - Split unrelated bodies of knowledge into separate projects.
- Exclude noise (journals, changelogs, templates) with
IGNORE_GLOBS. - Consider OpenAI embeddings for a large, prose-heavy corpus.
Answers got longer after upgrading
Each excerpt now carries the chunk on either side of it, for more context around the match.
SEARCH_NEIGHBOR_CONTEXT=0 restores the old, single-chunk shape, and SEARCH_MAX_RESULT_CHARS caps the
whole answer regardless.
A webhook returns 401 invalid_signature
The secret in the repository settings is not the one currently stored. Copy it again from the source dialog, or Regenerate and paste the new one. See Push Webhooks.
Scripts and the admin API
403 csrf_blocked from my own script
A cookie-authenticated write must come from this site (Sec-Fetch-Site, falling back to
Origin/Referer) — a script sending the session cookie from another origin trips it. Use
Authorization: Bearer $ADMIN_TOKEN, or an API token, instead; bearer
requests are exempt from the check. See Security.
After upgrading, /api/* answers 401 setup_required
This instance had no ADMIN_TOKEN set and was, on the version it ran before, effectively open on
/api/*. It is now closed by default: open /setup with the code from the log and create the first
account, same as any brand-new instance. Projects, sources and indexes already there are untouched. See
Accounts and Permissions.
MCP connectors
A member reads a project in /mcp/… they are not a member of
Expected while that project is open or token required: a credential that names nobody is judged by
the mode, not by memberships. Switch the project to account required under MCP access and its
endpoint starts following the member list. See
Accounts and Permissions.
An MCP client suddenly answers 401
The project now requires a credential. Mint a token under MCP access and add
--header "Authorization: Bearer …" (or headers in mcp.json) — or, if the project says account
required, reconnect a client that can sign in, because a static token is refused there.
An MCP client answers 403 … not a member of this project
The credential is fine and the account behind it is not on the project. Add it under Members, or connect with an account that already is one.
A browser-based connector cannot connect at all
Either the project is open/token required and the connector has no header to send, or MCP_OAUTH=0
on this instance and there is no OAuth flow for it to use. See
Connecting AI Clients.
A connector says it was disconnected and has to be approved again
Expected after a password change, after its membership was removed, or after the same credential was
presented twice — this server treats that as two parties holding one token and answers by taking the
grant down. A connector simply left unused past MCP_OAUTH_REFRESH_TTL_DAYS is a quieter, different
case: it is asked to authorize again and nothing is revoked. Approve it again either way.
A connector reports invalid_scope
It asked for an OAuth scope. This server issues none — an account-backed credential reaches exactly what its account may read — so the request is refused rather than granted under a scope nobody honours. The server log names the client that asked.
I lost an MCP token
It cannot be recovered — only a hash is stored. Revoke it under MCP access and mint another.
Getting more detail
docker compose logs -f # follow everything
docker compose logs --tail=200 contextator
curl -s http://localhost:3444/api/health | jq
For verbose per-request logging, set LOG_LEVEL=debug and restart. Useful log lines:
embedding model ready, indexing started, indexing finished (with counts),
indexing finished with source errors, source sync failed, mcp session opened / closed.
If you are stuck, open an issue with: the version from /api/health, the relevant log lines, the source
type involved, and what you expected to happen.