Upgrading¶
Upgrading Specivo means moving to a newer specivo/specivo image. The container entrypoint applies
any new database migrations on startup, so an upgrade is usually two commands.
Back up before every upgrade
Migrations can change the database schema and are not always reversible. Take a database dump
and back up SPECIVO_DATA_DIR before you pull a new image. See
Backup & restore. If anything goes wrong, that backup is your way back.
1. Choose the target version¶
Pin the release you want in .env so the upgrade is deliberate:
Using SPECIVO_VERSION=latest always pulls the newest published image. That is fine for trials, but
for a deployment you rely on, pin an explicit tag so you know exactly what is running.
2. Pull and restart¶
docker compose pull
docker compose stop api celery-worker celery-embeddings celery-beat
docker compose up -d
pull fetches the new image; stop stops the app containers so that none is running while the
one-shot init-data service prepares the data directory; up -d recreates the containers. On startup the entrypoint waits for
the database and applies any new migrations automatically — you do not run a migrate step yourself.
Watch the API logs to confirm a clean start:
When the api service reports healthy, the upgrade is done.
Rolling back¶
If the new version misbehaves, roll back by pinning the previous tag and restarting:
# .env
SPECIVO_VERSION=0.1.10 # the version you were on before
docker compose pull
docker compose up -d
A schema migration may require restoring your backup
If the failed upgrade already applied a migration, the database schema may be ahead of the older
image and the rollback alone will not be enough. In that case, restore the database dump and
SPECIVO_DATA_DIR you took before the upgrade, then start the older image. This is exactly why
the pre-upgrade backup is mandatory.
Tips¶
- Upgrade one minor version at a time on production rather than jumping many releases at once.
- Read the release notes for the version you are moving to before you pull.
- After upgrading, do a quick smoke test: sign in, open an issue, run a search.
Upgrading to 0.13.0¶
The btree_gist extension¶
The migration creates the PostgreSQL extension btree_gist, for the AI model
prices. The bundled database needs nothing. On an external database the
migration role needs CREATE on the database (it is a trusted extension, no superuser needed); on
Azure flexible server, add btree_gist to azure.extensions first. See
Configuration.
AI model prices replace the agent cost tables¶
The unused agent_token_logs and model_cost_configs tables are dropped, with their unmounted
API routes. The new AI model prices table is filled with the bundled price
list, valid from the list's date.
Metadata values are validated¶
Metadata schemas now refuse values that do not match them, from the issue page, the REST API and AI
agents. Only what a save changes is checked: an issue that already holds a value that breaks its schema
stays editable, but a write to that field is refused until it matches (for a list, such as commits,
that includes appending to it). What is refused has the
details. Agents that wrote short or malformed values now get an error that names the field.
Nothing has to be fixed before upgrading. Afterwards, list the issues that hold values not matching their schemas, and fix them so people and agents can keep writing to those fields:
docker compose exec api python -m specivo.cli.admin metadata-check
docker compose exec api python -m specivo.cli.admin metadata-check --project ACME
It prints, per project and schema, how many issues fail, which fields, and the issue keys. It never prints values and changes nothing. Run it again after tightening a schema.
Workflow field rules apply¶
Field rules created through the admin API (required or read-only fields per status) are enforced now, on every save of an issue, judging only what the save changes. An instance without rules sees no change. A rule naming an unknown field is refused. See Field rules.
Upgrading to 0.11.0¶
Version 0.11.0 moves the Redmine import out of Specivo. Specivo now only loads import bundles; migration tools write them. Nothing changes for an instance that never ran the direct import.
The direct Redmine import is removed¶
import_data --source redmine, make import-redmine and the importers extra are gone, and so is
the import-source plugin hook. To move from another tracker, see Importing from another tracker; for Redmine use the
Redmine migration tool, which writes a bundle that
python -m specivo.cli.import_data --source bundle loads. Update scripts and scheduled jobs that used the old
options.
Re-key an earlier direct import before loading a bundle¶
If you imported with the old direct import, its rows are keyed by the masked database URL, while a bundle is keyed by the instance its manifest names. Move them to the bundle's key first, so the bundle adds to the earlier import instead of duplicating it:
# Move the scope (leave out --yes first for a dry run that checks and counts)
python -m specivo.cli.admin import-map rename --system redmine \
--from 'postgresql://redmine:***@db.internal/redmine' --to tracker.example.com --yes
# Then load the tool's bundle as a continuation of that import
python -m specivo.cli.import_data --source bundle --path /data/exports/redmine --continue-import
Run import-map list to see the current key. The
bundle format describes
source.system and source.instance.
Upgrading to 0.10.0¶
Version 0.10.0 adds single sign-on and three migrations (0041 to 0043), which the entrypoint applies on startup. Nothing has to be configured to keep working as before: single sign-on is off until you add a provider, and people who are signed in stay signed in. Check the following.
The bundled stack gets a proxy network¶
Take the new docker-compose.yml. It puts nginx and api on a new network, proxy, with nginx at
the fixed address 10.213.47.2, so that Specivo can trust exactly that address when it reads the
client's address from X-Forwarded-For (see
Client addresses behind a proxy).
- If
docker compose up -dstops with "Pool overlaps", the range is already used on your host. SetSPECIVO_PROXY_SUBNETandSPECIVO_NGINX_IPin.envto a free range and address, then run it again. - If you keep your own Compose file, add the network the same way, or set
TRUSTED_PROXIESto the address of your proxy. WithTRUSTED_PROXIESempty, the address of the connection is the client: behind a proxy every person then shares one rate-limit bucket and one address in the audit log. - If another proxy (Caddy, Traefik, a CDN) sits in front of the bundled nginx, set
TRUSTED_PROXY_HOPS=2. Only the proxies you list inTRUSTED_PROXIEScan name the client, andTRUSTED_PROXY_HOPSsays how many there are in a row. - If you replaced
nginx/default.confwith your own, keepproxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;. You can also take over the newspecivo_no_queryaccess log format from the bundled file: the access log no longer contains query strings, which can carry the code and state of a single sign-on sign-in and one-time tokens from links. Delete or rotate old access logs if you want those gone.
Signing out changed¶
- The Sign out button is a form that posts to
/logout/. A bookmark or link to/logout/opens a "Sign out?" page and does not sign anyone out. POST /api/v1/auth/logout/answers200with{"end_session_url": ...}instead of204. The value isnullunless the session came from a single sign-on provider that supports signing out at the provider. A script that checks for exactly204must accept200.- Signing out now ends the session on the server and revokes its access token at once. Before, both stayed valid until they expired.
- A password reset, by email, by an administrator or from the command line, ends every session of the account.
Administrator API keys do less¶
An administrator's API key is refused with 403 and the code api_key_not_allowed for resetting
passwords, unlocking or reactivating users, creating people or administrators (service accounts are
still allowed), creating keys for other users and changing single sign-on settings. Automation that
did any of this with a key must run as a signed-in administrator instead. See
What an administrator's key cannot do.
Deactivated users¶
Administrators can deactivate and reactivate users (Administration → Users, on the user's page). Deactivating ends every session and switches off the user's API keys; reactivating does not switch the keys back on.
If you turn on single sign-on¶
- Set
APP_URLfirst: the redirect and logout addresses are built from it. - Client secrets stored in Specivo are encrypted with a key derived from
SECRET_KEY. If you rotateSECRET_KEY, open each provider and enter its client secret again. Back up.env.localwith the database dump; see Backup & restore. - Register the redirect URI, and the back-channel logout URI if you use it, at your identity provider. See Single sign-on.
- Keep a way back in: Getting back in.
The migrations also remove the unused github_id and google_id columns of the users table.
Upgrading to 0.9.0¶
Version 0.9.0 adds eight migrations (0033 to 0040), which the entrypoint applies on startup like any other. Most of it needs no action, but a few things are worth doing or checking afterwards.
The containers no longer run as root¶
The api and Celery containers, and the commands you run with docker compose exec api ..., now
run as the unprivileged user specivo (UID/GID 10001). Take the new docker-compose.yml: its
init-data service runs once as root before api starts. It gives the app its own subdirectories
of the data directory, attachments/, avatars/, models/, state/ and logs/app/, including
the files an older version wrote there as root, and moves .bootstrap.json into state/ if it is
still there. On the first start this can take a moment on a large attachments folder; later starts
skip directories that already belong to the app.
- Stop the app containers before
docker compose up -d, as in Pull and restart:init-datamust not run while an oldcelery-worker,celery-embeddingsorcelery-beatcontainer is still up. -
It does not enter directories the app already owns. If it reports one that holds files of root, stop all app containers and run
docker compose run --rm init-data fix-permissions --all. -
The data directory itself stays with root (or with you), as do
logs/,postgresql/,redis/andlogs/nginx/: the app cannot change entries at the top of the data directory. - The app's log file moves from
logs/specivo.logtologs/app/specivo.log. The old file is left where it is; delete it when you no longer need it. - If one of the app directories is a symlink, sits on another filesystem or belongs to another
user,
init-dataleaves it alone and logs a warning. Mount the target at that path instead of symlinking, or runsudo chown -R 10001:10001on it yourself. - If you keep your own Compose file, add the
init-dataservice and makeapidepend on it, or prepare the directories yourself before you start the new image:cd <SPECIVO_DATA_DIR> && sudo mkdir -p attachments avatars models state logs/appandsudo chown -R 10001:10001 attachments avatars models state logs/app. Without that the api logs a warning at start and uploads fail. - Other host directories mounted into the app containers (custom theme or error-page folders, a
directory holding files for an import) must be readable by UID
10001. - To run with no root step at all, set
user: "10001:10001"oninit-data,apiand the Celery services indocker-compose.override.yml;init-datathen changes nothing. The image also works with a read-only root filesystem (read_only: trueplus atmpfsfor/tmp). - The image now has its own
HEALTHCHECK, sodocker psshows the health of every Specivo container.
Rebuild the search index¶
Searching for an issue key such as ACME-42 now works in every search mode. Older text has to be
re-indexed before a mention of a key in it is found, so the migration flags the index as needing a
rebuild. Until you run it, an issue is still found by its own key. Rebuild it once:
- in the web UI, under Administration → Settings, with Reindex now; or
- from the command line:
The reindex runs in small batches, so Specivo stays usable while it works. Text edited after the upgrade is indexed as you save it. The notice next to the button reads "Search language changed", which is the same flag the language setting uses; it clears when the reindex finishes.
Comments become searchable, and indexing follows every change¶
Before 0.9.0 a comment could not be found by any search until someone ran the embedding backfill,
and edited issues and wiki pages kept their old text in semantic search. Now every change is
indexed as it is saved: keyword search sees it at once, and a new celery-embeddings container adds
the embeddings for semantic search a few seconds later. See
How content is indexed.
- Migration 0037 makes existing comments searchable by keyword. It follows your
SEARCH_INDEX_COMMENTS,SEARCH_MIN_COMMENT_LENGTHandSEARCH_EXCLUDE_BOT_COMMENTSsettings. Nothing to do. - Take the new
docker-compose.yml: it adds thecelery-embeddingsservice, which runs the embedding jobs in one process so the model is loaded once (EMBEDDING_WORKER_CONCURRENCY, default1). If you keep your own Compose file, add the service, or setEMBEDDING_QUEUE=celerysocelery-workerruns the jobs. It mounts the same data directory, so the model files need no moving. - If you use semantic search, run the backfill once after upgrading so the comments and the edits made before the upgrade get embeddings too. It also indexes every attachment (including those without a description and those on wiki pages) and drops the chunks of comments your settings exclude. Without the model files it writes keyword chunks only and says so, which is harmless:
A scheduled backfill, if you set one up, is no longer needed.
- make download-model, make backfill-embeddings, make reindex-fts and make import-data now
run in the api container of whichever stack the directory has, so they work from a release
bundle too. The model download is now python -m specivo.cli.download_model, which checks
checksums; the release bundle's configure.py moved to scripts/configure.py.
Semantic search gets a vector index¶
Migration 0038 builds an HNSW index on the stored embeddings, so semantic and hybrid search no longer compare the query with every chunk (see Vector index).
- The index is built with
CREATE INDEX CONCURRENTLY: Specivo keeps working while it builds, but the migration, and so the start of theapicontainer, waits until it is done. Expect seconds for a few tens of thousands of chunks (about 30 seconds for 100,000 on a laptop) and minutes for millions. Count yours first withdocker compose exec db psql -U specivo -d specivo -c "SELECT count(*) FROM chunk_embeddings". - Building is much faster when the index fits in PostgreSQL's
maintenance_work_mem(64 MB by default; budget roughly 2 KB per chunk). PostgreSQL logs a notice when it does not fit. On a large install, raise it for the upgrade, for example withALTER SYSTEM SET maintenance_work_mem = '1GB'; SELECT pg_reload_conf();, and set it back after. - It needs pgvector 0.8 or later to be used, which the bundled
pgvector/pgvector:pg18image has. On an external database, check withSELECT extversion FROM pg_extension WHERE extname = 'vector'. - If the build is interrupted, the next start drops the half-built index and builds it again.
Only the bundled embedding model¶
The administrator API can no longer register embedding models: only the bundled local model,
multilingual-e5-small, is supported, and hosted (bring-your-own-key) providers are not. Migration
0039 keeps a single default model; if an older release left more than one, it keeps the local model
as the default. If the default is a model with another provider, semantic search stays off and the
logs say so once; see Changing the model to put the local
model back.
Check time tracking permissions¶
Reading time entries now needs the View time entries permission in the project, and logging time needs Log time. Before 0.9.0, anyone who could open a project could read its time entries. After the upgrade:
- Reporter and the built-in Non member role no longer see any time. Add View time entries to a role if its people should see it.
- Agent is granted View time entries by the migration, so service accounts can read back the time they log. This applies only to the role still named Agent. If you renamed it, or made your own role for agents, grant the permission yourself.
- Manager, which holds every permission, and Developer are unchanged.
- Any custom role that has Log time but not View time entries can log time and will not see it afterwards. Review your custom roles under Administration → Roles.
See Time tracking.
New role and tracker settings¶
Administration → Roles and Administration → Trackers are new. A role now has a Users visibility and a Time entries visibility setting, and a tracker can be Private by default. Every existing role and tracker starts with the values that keep today's behaviour (everyone visible, all time entries visible, not private), so nothing changes until you edit one. See What a role can see and Private by default.
Scripts that use browser cookies need a CSRF token¶
Calls authenticated by the sign-in cookie must now send the X-CSRF-Token header on
change-password, logout, logout-all, session revoke, and any other state-changing request under
/api/v1/auth/, except the four that are meant to work without a session: login, token refresh,
forgot-password and reset-password. The value is the csrf_token cookie Specivo sets on any GET.
A request without it is refused with 403 CSRF validation failed. The web UI sends the header for
you.
Nothing changes for clients that use an API key or an Authorization: Bearer header, which is how
scripts and agents should authenticate anyway.
Longer Markdown is refused¶
An issue description, comment, wiki page, version description or recurring-issue description can now hold at most 262,144 characters. Saving more is refused with a message giving the limit. Content already stored that is longer, such as imported content, is kept as it is and shown as plain text. Text that would take too long to format is also shown as plain text. See Size and complexity limits.
Search paging is capped¶
The REST search endpoint refuses an offset above 1000 with a 422, and the MCP search tool
accepts a limit of 1 to 50. Clients that page deeper than that need to narrow the query instead.
The web search page clamps the offset rather than failing.
Import command renamed¶
python -m specivo.cli.import_redmine became python -m specivo.cli.import_data. Update any script
or scheduled job that calls the old module by name. As of 0.11.0 the direct Redmine import is gone;
see Upgrading to 0.11.0.
The Redmine import also carries over anonymous and non-member access, a role's visibility settings and a tracker's private default. It never turns the instance-wide anonymous access switch on.
Other changes you may notice¶
- A file attached to an issue you cannot see now answers "not found" instead of downloading, and a project's shared versions are hidden from people who cannot open that project.
- In Markdown, a bare filename such as
links to the attachment of that name. See Link to an attachment by filename. - Imported accounts can keep their old passwords. This is off until you list an algorithm in
LEGACY_PASSWORD_ALGORITHMS; see Configuration.