Configuration¶
Specivo is configured with environment variables. The Docker stack reads two files from the repo root, and neither is required to start — sensible defaults apply until you override them.
.env vs .env.local¶
.envholds settings: ports, the image version, the data directory, registration mode. The oneconfigure.pywrites also holds the database URL and password, so treat a generated.envas a secret too..env.localholds secrets — most importantlySECRET_KEY. Keep it out of version control.
Start from the example file:
Or let the helper generate both files (including a strong SECRET_KEY):
Run configure.py and Docker Compose as the same user
configure.py creates .env and .env.local readable and writable by their owner only
(mode 0600): .env gets the database password and .env.local the SECRET_KEY. Docker
Compose reads both files as the user who runs it, so run configure.py and docker compose
(or make up) as the same user, or both with sudo. If you created the files as one user and
start the stack as another, docker compose cannot read them; change their owner with
sudo chown <user> .env .env.local rather than widening the mode.
After changing either file, apply it with:
Key variables¶
| Variable | Purpose / default |
|---|---|
COMPOSE_PROJECT_NAME |
Isolates an instance (containers, networks, volumes) so you can run several on one host. Default specivo. |
SPECIVO_PORT |
External nginx port. Default 9933. |
SPECIVO_DATA_DIR |
Persistent data directory: database files, logs, attachments, themes. Default ./specivo-data. |
SPECIVO_VERSION |
Image tag — latest, or a pinned release like 0.12.0. |
SPECIVO_IMAGE |
Where the image is pulled from. Default specivo/specivo (Docker Hub). Set ghcr.io/specivo/specivo to pull the same image from the GitHub Container Registry, for example where Docker Hub is blocked. |
DATABASE_URL |
postgresql+asyncpg://specivo:specivo@db:5432/specivo. Point it at an external database to use your own. |
POSTGRES_DB / POSTGRES_USER / POSTGRES_PASSWORD |
Credentials for the bundled Postgres. |
REDIS_URL |
redis://redis:6379/0. Point it at an external Redis if you prefer. |
APP_URL |
The public address of Specivo, for example https://specivo.example.com. Links in emails, repository webhook URLs and single sign-on addresses are built from it. See Public address (APP_URL). |
SECRET_KEY |
Signs sessions and tokens. Must be a strong, unique value in production. Keep in .env.local. |
DEBUG |
false in production. |
REGISTRATION_MODE |
open lets anyone self-register. Use a closed mode to require invites. |
CAPTCHA_ENABLED |
Captcha on the registration form. |
SEARCH_ENGINE_INDEXING |
Let search engines index the pages anyone can read without an account. Default false. The Administration → Settings → Anonymous access checkbox overrides it. See Search engines. |
ROBOTS_TXT |
A fixed robots.txt, replacing the one Specivo serves. Unset by default. The admin setting robots_txt overrides it. See Search engines. |
CORS_ORIGINS |
Allowed browser origins, e.g. ["https://specivo.example.com"]. |
AUDIT_LOG_RETENTION_MONTHS |
Whole months of the security audit log kept before the current month; older months are dropped daily. Default 12. 0 keeps everything. |
TRUSTED_PROXIES |
Address of the reverse proxy allowed to name the client in X-Forwarded-For, as a JSON list of addresses or networks. Default [] (the connection's address is the client); the bundled stack sets the bundled nginx, ["10.213.47.2/32"]. See Client addresses behind a proxy. |
TRUSTED_PROXY_HOPS |
How many proxies in a row stand in front of Specivo. Default 1. |
SMTP_HOST / SMTP_PORT / SMTP_TLS / SMTP_USER / SMTP_PASSWORD |
Outbound email (optional). |
SMTP_MESSAGE_ID_DOMAIN |
Domain in the Message-ID and List-Id of outgoing mail: a plain host name such as mail.example.com; any other value stops Specivo from starting. Default: the domain of SMTP_FROM. |
MARKDOWN_RENDER_DEADLINE_SECONDS |
CPU seconds one Markdown field (a description, a comment, a wiki page) may use to render before it is shown as plain text instead. It also may not take more than 4 times this in elapsed time. A backstop: text that would render slowly is caught before rendering. Counts CPU time, so a busy host does not trigger it. Default 2, scaled by MARKDOWN_RENDER_SPEED_FACTOR; the largest realistic pages need under half a second on a fast machine. See Size and complexity limits. |
MARKDOWN_RENDER_REQUEST_BUDGET_SECONDS |
CPU seconds all the Markdown fields on one page or API response may use together (a description, its comments and its history). Once spent, the remaining fields are shown as plain text. Default 5, scaled by MARKDOWN_RENDER_SPEED_FACTOR. |
MARKDOWN_RENDER_SPEED_FACTOR |
The two settings above are for a fast current machine. Specivo measures how much slower this host renders Markdown, once per process at first use (logged at INFO as "Markdown render speed factor"), and multiplies both limits by that factor, between 1 and 20, so large pages still render on slow or low-power hardware. Set a fixed value to override the measurement, e.g. 1 to turn scaling off. Default: measured. |
Secrets (.env.local)¶
Set a strong SECRET_KEY and review registration
The default SECRET_KEY is insecure and exists only so the stack starts on first run. Before
you expose Specivo to anyone, put a strong, unique value in .env.local:
Also review REGISTRATION_MODE. With open, anyone who can reach the URL can create an
account. For a private deployment, switch to a closed mode and invite people instead.
External database and Redis¶
The bundled db and redis services are convenient but optional. To use managed services instead,
point the URLs at them:
# .env
DATABASE_URL=postgresql+asyncpg://user:[email protected]:5432/specivo
REDIS_URL=redis://cache.example.com:6379/0
Your PostgreSQL must have the pgvector extension available, since search relies on it. Use
pgvector 0.8 or later: semantic search only uses its vector index with 0.8's iterative index
scans, and compares every embedding on older releases (the bundled pgvector/pgvector:pg18 image
ships 0.8). Once you
point the URLs elsewhere, you can remove the db and redis services from docker-compose.yml —
they are declared as optional dependencies, so the app starts without them.
It also needs the pg_trgm extension, which the similar issue suggestions use, and btree_gist, which keeps the periods of an AI model price from overlapping. Both ship with PostgreSQL itself. The database migrations create them, as they do pgvector, and stop if they cannot:
- Both are trusted extensions (PostgreSQL 13 and later), so a database role with
CREATEon the database can install them; no superuser is needed. - AWS RDS lists both among its supported extensions.
- On Google Cloud SQL, the role needs
cloudsqlsuperuser. - On Azure Database for PostgreSQL flexible server, an extension must first be added to the
azure.extensionsserver parameter; check that list forpg_trgmandbtree_gistbefore you migrate.
The data directory¶
Everything that must survive a container restart lives under SPECIVO_DATA_DIR (default
./specivo-data):
postgresql/— the bundled database files- attachments uploaded to issues, wiki pages, and comments
logs/— application and nginx logs- themes and the embedding model, if you fetch it
Back this directory up alongside a database dump. See Backup & restore.
Email (SMTP)¶
Specivo can send notifications (mentions, watcher updates, password resets) if you configure SMTP. It works fine without email — those notifications simply stay in-app.
# .env
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_TLS=true
SMTP_USER=[email protected]
SMTP_PASSWORD=... # put the password in .env.local
All mail about one issue has the same subject, [KEY-123] issue subject, and refers to the same
thread, so mail clients group it into one conversation; the first line of each mail says what
happened ("Assigned to you by …", "… commented"). Every mail carries a Message-ID, a Date and
Auto-Submitted: auto-generated, so out-of-office replies are not sent back. Mail about an issue
also carries X-Specivo-Issue, X-Specivo-Project and X-Specivo-Event (assignment,
updated, comment or mention), and a List-Id per project, for filtering rules.
Each person's mail is written in their own language, the one under Preferences, or the
instance's default language if theirs is not available, and its time is shown in their timezone.
It ends with a link to the issue, or to the comment, built from APP_URL.
If APP_URL is empty, the link is left out. If it still points at localhost, so does the link,
and Specivo logs a warning at start-up.
Mail goes only to people who can open the issue, so the subject of a private issue never reaches
anyone else. Someone mentioned with @username gets a mail saying so; if they also watch the issue
or are assigned to it, that is the only mail they get about the comment.
Message-IDs end in the domain of SMTP_FROM. To use another one, for example when the from
address is on a shared mail provider's domain, set it:
The value must be a plain host name (letters, digits, hyphens and dots); Specivo refuses to start
with anything else. If it is not set and the domain of SMTP_FROM is not a plain host name, the
host of APP_URL is used.
Public address (APP_URL)¶
Specivo builds every address that leaves the instance from APP_URL: the links in emails, the
webhook URL of a repository integration, the redirect and
back-channel logout URIs of a single sign-on provider and its
SCIM base URL. It never takes them from the address the browser used,
which a client can forge. Set it to the scheme, host and port (if not the default) that people and
other systems use to reach Specivo, without the STEALTH_PREFIX, and restart:
The default, http://localhost:8030, only works on the machine Specivo runs on. While APP_URL
is empty or names this machine (localhost or a *.localhost name, 127.0.0.1 or another
loopback address, ::1, 0.0.0.0):
- the pages that show these URLs for copying (project Settings → Repositories, and the Connection and Provisioning (SCIM) sections of a single sign-on provider) show a warning with the current value;
- with
DEBUG=false, the web app and the background worker each log one warning when they start.
When APP_URL is a real address but you opened the page under another host name, the same pages
say so: the URLs you copy carry the APP_URL host, which may not be the one that reaches this
instance.
Search engines¶
Search engines index nothing by default. Every response carries
X-Robots-Tag: noindex, nofollow, noarchive, /robots.txt is Disallow: /, and
/sitemap.xml answers 404.
Indexing is opt-in, and it only ever covers pages a visitor without an account can already read. A page becomes indexable when all of these hold:
- Search engine indexing is on: the checkbox Let search engines index public pages in
Administration → Settings → Anonymous access, or
SEARCH_ENGINE_INDEXING=true. The checkbox wins over the variable. - Anonymous access is on (the switch in the same card).
- The project is public, active and opted in to anonymous access for issues, wiki or both.
- The page is served without an account: the project list, the project page, its issue list, issue pages and wiki pages.
What stays noindex whatever the setting: private projects, projects not opted in, archived
projects, private issues and comments, anything a signed-in visitor sees, sign-in and password
pages, error pages and redirects, administration, the JSON API and MCP. Search result pages, and
any public page opened with a query string (filters, sorting, paging), are sent with
noindex, follow: crawlers may follow their links but do not index them. Indexable pages keep
noarchive and Cache-Control: no-store, and carry a <link rel="canonical"> built from
APP_URL; issue and wiki pages also get a short
<meta name="description"> from their text.
While indexing applies and at least one project is readable:
/robots.txtallows/projects/, the readable projects' pages (/projects/KEY/) and issues (/issue/KEY-), disallows everything else and names the sitemap./sitemap.xmlis a sitemap index. It points at/sitemaps/projects.xml(the project list, project pages and issue lists) and at per-project sitemaps of issues and wiki pages, at most 5,000 URLs each (/sitemaps/KEY/issues-N.xml,/sitemaps/KEY/wiki-N.xml). They list only the issues and wiki pages a visitor without an account can open, with their last change. Links are built fromAPP_URL, so set it to the public address first.
Changes apply on the next request: turning anonymous access or indexing off, or opting a project
out, removes it from robots.txt and the sitemap and restores noindex at once. Search engines
drop pages they already indexed on their own schedule.
An instance with a STEALTH_PREFIX is never indexed: robots.txt and the sitemap are served at
the root and would reveal the prefix.
Keeping one public project out of search engines. Indexing is instance-wide. To exclude a
project while leaving it readable without an account, write your own robots.txt in the admin
setting robots_txt (or ROBOTS_TXT), which replaces the generated one, for example:
User-agent: *
Allow: /projects/$
Allow: /projects/ACME/
Allow: /issue/ACME-
Disallow: /
Sitemap: https://specivo.example.com/sitemap.xml
The sitemap still lists every readable project; leave the Sitemap: line out if that matters.
Repository activity. Issue pages of a public project can show repository activity: commit messages, branch names and pull request titles. Once the project is indexable, search engines can pick that text up too. Decide before making a project public and turning indexing on.
Search rate limits¶
Every search runs the query through the database, and a hybrid or semantic search also runs the
embedding model on the server's CPU. So searches are limited per client
address, on the search page and on the REST search endpoint (GET /api/v1/search/), in every mode,
for people with and without an account. People without an account get the same search as everyone
else, hybrid by default, but only over what they may read without signing in.
| Variable | Purpose / default |
|---|---|
SEARCH_RATE_LIMIT_ANONYMOUS_PER_MINUTE |
Searches per minute one client address may run without signing in. Default 10. |
SEARCH_RATE_LIMIT_PER_MINUTE |
Searches per minute one client address may run while signed in. Default 60. It is higher because a whole office behind one address shares it. |
SEARCH_QUERY_EMBEDDING_CACHE_SECONDS |
Seconds a query's embedding is kept in Redis, so a repeated query, or the next page of results, does not run the model again. Default 300; 0 turns the cache off. |
The two budgets are separate, and the search page and the REST endpoint share them: moving from one to the other does not start a new count. An IPv6 client is counted by its /64, like the other per-address limits. Opening the search page without searching does not count.
Over a limit, the search page shows a short page saying that searching is paused and when the
visitor can try again, with status 429 and a Retry-After header (in seconds). The REST API
answers 429 with its usual JSON error, code rate_limit_exceeded, and the same header. Every
other rate-limited web page answers the same way, with general wording.
Behind a reverse proxy, set TRUSTED_PROXIES, or every visitor
shares the proxy's budget. If Redis is unavailable, searches are not limited, as with every other
rate limit, and RATE_LIMIT_ENABLED=false turns all rate limits off.
AI report rate limits¶
AI usage charts and spend reports group a person's or a project's whole AI usage and convert its
amounts, so each person has a budget per minute for them. It is shared by My AI usage (its charts),
the project and instance AI spend report pages, the report API (GET /api/v1/projects/{key}/ai-usage/report/
and GET /api/v1/ai-usage/report/, JSON and CSV) and the MCP report tool.
| Variable | Purpose / default |
|---|---|
AI_REPORT_RATE_LIMIT_PER_MINUTE |
Charts and reports per minute one person may load. Default 30. |
AI_REPORT_RATE_LIMIT_ADMIN_PER_MINUTE |
The same for administrators, who also read the instance report. Default 120. |
AI_USAGE_CHART_CACHE_SECONDS |
Seconds the My AI usage chart data is kept in Redis for the same person and filters. A new, edited or deleted entry shows at once; exchange rates, prices and plans changed meanwhile show within this time. Default 60; 0 turns the cache off. |
Over the limit, a page shows the rate-limit page and the API answers 429 with code
rate_limit_exceeded, both with Retry-After. Whatever is asked for, a chart has at most 120
points (days, weeks, months, quarters or years, by the range of the entries shown), and a spend
report grouped by day or week over a long range is grouped by week or month instead.
Client addresses behind a proxy¶
Specivo uses the client's IP address for the per-address rate limits (sign-in, single sign-on,
anonymous reads, search), for the address shown on each session, for API key IP allowlists and for the
security audit log. Behind a reverse proxy, the connection comes from the proxy. Each proxy appends
the address it received the request from to the X-Forwarded-For header, so the client's address
is in that header. Two settings decide what Specivo believes:
TRUSTED_PROXIES: the address of the proxy that connects to Specivo. Only a connection from one of these addresses may name the client. A connection from anywhere else is the client, whatever header it sends. Default[]: nothing is trusted.TRUSTED_PROXY_HOPS: how many proxies in a row stand in front of Specivo. The client is the entry that many places from the right ofX-Forwarded-For. Everything to the left of it was written by the client and is ignored, so nobody can choose their own address by sending the header. Default1.
List the proxy's exact address, not a whole private range: anything in a trusted range could name any client.
The bundled stack¶
docker-compose.yml puts nginx and the api on their own network, proxy
(10.213.47.0/29), with nginx at the fixed address 10.213.47.2, and sets
TRUSTED_PROXIES=["10.213.47.2/32"]. Docker hands out automatic addresses, such as the api's,
only from 10.213.47.4/30, so they never collide with nginx's. Nothing else is on that network,
the api publishes no port, and nginx appends the address it saw, so it works without changes. If
that range is already used on your host (docker compose up fails with "Pool overlaps"), pick
another one in .env. Change all three together, and keep the nginx address inside the subnet
but outside the range:
# .env
SPECIVO_PROXY_SUBNET=10.99.0.0/29
SPECIVO_PROXY_IP_RANGE=10.99.0.4/30
SPECIVO_NGINX_IP=10.99.0.2
TRUSTED_PROXIES follows SPECIVO_NGINX_IP unless you set it yourself. Set these in .env, not
.env.local: the compose file reads them from .env.
Another proxy in front of nginx¶
When an HTTPS terminator or a CDN forwards to the bundled nginx, there are two proxies in a row. The outer one writes the client's address and nginx appends the outer proxy's address, so set:
- Caddy:
reverse_proxysetsX-Forwarded-Forto the client's address and does not pass on a header the client sent, unless you configure Caddy's owntrusted_proxies. - Traefik: appends the client's address. Leave
forwardedHeaders.insecureoff on the entry point, so a header the client sent is not passed through as if Traefik had written it. - Cloudflare: appends the visitor's address. Nginx must then only accept connections from Cloudflare (firewall the published port to Cloudflare's address ranges), because a request that goes around Cloudflare can put anything in the extra entry.
With TRUSTED_PROXY_HOPS=1 and an outer proxy, every client appears as the outer proxy's address:
safe, but everyone shares one rate limit bucket. With a hop count higher than the real number of
proxies, clients can choose their address. Count exactly.
Without the bundled nginx¶
If your own proxy (nginx, Caddy, Traefik) connects straight to the api, set TRUSTED_PROXIES to
that proxy's address and keep TRUSTED_PROXY_HOPS=1. For nginx, the usual
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; is right: it appends the client's
address.
MCP sessions¶
An AI agent connected over MCP (Streamable HTTP, at /mcp/) holds a session that it names in the
Mcp-Session-Id header of every request. Specivo keeps each session in Redis, so it survives the
agent's computer going to sleep, a dropped network connection and a restart of Specivo: when the
agent comes back with its session id, the session works as before.
| Variable | Purpose |
|---|---|
MCP_SESSION_TTL_SECONDS |
How long a session may go unused before it expires. Every request starts the period again, so a session in use never expires. Default 604800 (7 days). After it, the agent's next request is answered with HTTP 404, and an MCP client then opens a new session by itself. |
MCP_SESSION_IDLE_TIMEOUT_SECONDS |
How long an unused session is kept in memory. Only frees memory: the next request rebuilds the session from Redis. Default 3600 (1 hour). |
Sessions survive a restart of Redis only when Redis saves its data to disk. The bundled Redis does (append-only file); Specivo logs a warning at startup when an external Redis does not. Without it, agents connected when Redis restarts start new sessions.
The legacy SSE transport (/mcp/sse/) keeps no sessions: an agent whose event stream drops
reconnects and carries on, but a request it sends while the stream is down fails once. Configure
agents for Streamable HTTP ("type": "http", URL ending in /mcp/) where they support it.
Proxies in front of Specivo¶
MCP responses are streamed. The bundled nginx is already set up for this. If another proxy (Caddy,
Traefik, a load balancer, Cloudflare) stands in front of Specivo, make sure that for /mcp/ it:
- does not buffer responses. nginx:
proxy_buffering off;. Specivo also sendsX-Accel-Buffering: no, which nginx-based proxies obey. - allows long-lived responses. An agent keeps a stream open for as long as it is connected,
and Specivo sends a keep-alive on it every 15 seconds. A read timeout of at least a minute is
enough; the bundled nginx uses
proxy_read_timeout 24h;. - uses HTTP/1.1 or later to Specivo with keep-alive. nginx:
proxy_http_version 1.1;andproxy_set_header Connection "";.
A proxy that cuts a stream does not end the session: the agent reconnects and carries on.
Repository integrations and webhooks¶
Repository integrations receive webhooks from GitHub, GitLab,
Gitea and Forgejo at APP_URL followed by /hooks/... (after the STEALTH_PREFIX, if set), so
APP_URL must be the address the forge can reach. Both they and
outgoing webhooks do their work in the background containers
(celery-worker, with celery-beat to pick up anything that was missed): while those are down, incoming deliveries wait
as Accepted and outgoing ones as Pending, and both are picked up once they run again.
| Variable | Purpose / default |
|---|---|
GIT_HOOK_MAX_BODY_BYTES |
Largest webhook body a repository integration accepts; larger ones are answered 413. Default 5242880 (5 MB), at least 1024. |
GIT_HOOK_RATE_LIMIT_PER_MINUTE |
Webhook requests per minute one repository integration may receive before Specivo answers 429. Default 120. |
GIT_HOOK_UNKNOWN_RATE_LIMIT_PER_MINUTE |
Requests per minute one client address may send to hook URLs that match no integration, or with a bad signature. Default 30. |
AI_USAGE_INGEST_RATE_LIMIT_PER_MINUTE |
Batches per minute one account may send to the AI usage ingest API before Specivo answers 429. Default 60. |
WEBHOOK_TIMEOUT |
Seconds an outgoing webhook may take for each step of a request (connecting, sending, reading the answer). Default 30; a whole attempt is always cut off after 60 seconds. |
Single sign-on: getting back in¶
Identity providers are configured in Administration → Single sign-on, and the login page settings in Administration → Settings → Authentication. Those settings can hide the password form, send the login page straight to one provider, and limit password sign-in to administrators. None of them can lock an administrator out:
- With the password form hidden, the login page still shows a small Sign in with a local
account link. It opens
/login/?local=1, which always shows the form and never redirects. - If the identity provider is down, the error on the login page links to the same form.
- If the SSO settings themselves are wrong (a broken provider, every admin refused), turn on the environment switch below. It overrides all three settings until you turn it off again:
Specivo logs a warning at startup while it is on. Fix the settings, then remove the line and
recreate the containers again.
- If no administrator knows a password, set one from the host with
make reset-password login=... password=..., or create a new administrator with
make create-admin login=... email=... password=....
| Variable | Purpose / default |
|---|---|
SSO_FORCE_LOCAL_LOGIN |
Break-glass switch: always show the password form, never redirect the login page to a provider, and let every user sign in with a password. Default false. |
SSO_IDP_RECHECK_MINUTES |
How often, at most, renewing an SSO session asks the identity provider whether the person may still sign in (per session). Default: the access-token lifetime, ACCESS_TOKEN_EXPIRE_MINUTES (15). 0 asks at every renewal. See The session check. |
The other SSO variables are listed in Single sign-on.
Imported passwords¶
These settings matter only if you import accounts from another system and let them keep their passwords. A bundle can carry the old password hashes (see Importing from another tracker). See also Writing a password hasher plugin.
| Variable | Purpose / default |
|---|---|
LEGACY_PASSWORD_ALGORITHMS |
Imported password formats allowed to sign in, e.g. ["redmine_sha1"]. Default []: none. |
LEGACY_PASSWORD_HASHES_EXPIRE_AT |
After this moment no imported password works and those people use password recovery, e.g. 2027-03-31T00:00:00Z. Default unset. |
PASSWORD_HASHERS |
Extra hasher classes by dotted path, e.g. ["acme_hashers.AcmeTrackerMd5"]. |
INSTALLED_PLUGINS |
Plugins by dotted path; a plugin can provide password hashers. |
Exchange rate feed¶
| Variable | Meaning |
|---|---|
EXCHANGE_RATE_FEED_ALLOW_PRIVATE_NETWORKS |
Let the optional daily exchange rate fetch reach loopback and private addresses, for a self-hosted Frankfurter on your internal network. Default false: only public addresses. Link-local and cloud metadata addresses are refused either way. See Exchange rates. |
EXCHANGE_RATE_FEED_ALLOW_UNRESOLVED_VIA_PROXY |
Behind a proxy, let the fetch use a feed host that does not resolve on this server (names resolve only through the proxy). Its address then cannot be checked here, so your proxy must enforce egress rules to private and link-local ranges. Default false: such a host is refused. |
The fetch itself is switched on by an administrator on Admin → Exchange rates; it honours
HTTPS_PROXY and NO_PROXY.
Semantic search model¶
Hybrid and semantic search match by meaning, which needs an embedding model. Specivo runs one on
your own CPU, multilingual-e5-small (about 487 MB, 384-dimensional vectors, about 100 languages).
It is not part of the Docker image and is not downloaded automatically. You fetch it once, with the
stack running:
It saves into SPECIVO_DATA_DIR/models and checks each file against its published checksum. See
Semantic search and embeddings.
There is no setting that turns it on: when the model files are in SPECIVO_DATA_DIR/models, it is
on. Without them, keyword search and everything else work normally and hybrid search behaves like
keyword search. Only this bundled local model is supported: there are no hosted embedding providers,
no bring-your-own-key (BYOK) option and no way to switch to another model (see
Changing the model).
| Variable | Purpose / default |
|---|---|
EMBEDDING_ONNX_THREADS |
CPU threads the model may use. Default 2; 0 uses every core. |
EMBEDDING_WORKER_CONCURRENCY |
Processes in the celery-embeddings container. Default 1; each one loads its own copy of the model. |
EMBEDDING_QUEUE |
Celery queue for embedding jobs. Default embeddings, read by celery-embeddings; celery runs them in celery-worker instead. |
SEARCH_INDEX_COMMENTS |
Index comments for search. Default true. |
SEARCH_MIN_COMMENT_LENGTH |
Comments shorter than this many characters are not indexed. Default 20. |
SEARCH_EXCLUDE_BOT_COMMENTS |
Skip comments made through an API key or by a service account. Default true. |
SEARCH_HNSW_EF_SEARCH |
Size of pgvector's candidate list for each vector index scan (hnsw.ef_search, 10 to 1000). Higher finds the true nearest matches more reliably and is a little slower. Default 100. |
SEARCH_SEMANTIC_OVERFETCH |
Nearest chunks read from the vector index per wanted result, before results the reader may not see are removed (1 to 50). Default 4. See Vector index. |
SEARCH_SEMANTIC_MIN_SIMILARITY |
Minimum cosine similarity (-1 to 1) a chunk needs to count as a semantic match; in hybrid search it applies to matches found by meaning alone. Unset by default, which uses the model's own minimum (0.82 for multilingual-e5-small); -1 turns it off. See Minimum similarity. |
See Semantic search and embeddings for what it is used for, how existing content is indexed, how to check it works, and troubleshooting. For an offline host, see Air-gapped install.