Upgrading an existing stack¶
A generated stack belongs to you. Regenerating it would throw away your rooms, prompts and configuration, and nothing yet updates one in place, so bringing an older stack up to the current template is a by-hand job. This page lists the changes in order, grouped by the template release that introduced them: find the section your stack is at, and work forward from there.
Where do I start?¶
| Your stack | What its files show | Skill release to walk with | Start at |
|---|---|---|---|
| soliplex 0.84 or earlier | backend/constraints.txt pins below 0.85 |
template-skill-2026.10.10-37f7a2d |
1 |
| soliplex 0.85, built from PyPI | backend/Dockerfile runs uv add … soliplex |
template-skill-2026.10.14-9a17a20 |
2 |
| on the 0.85 release images | backend/Dockerfile builds FROM ghcr.io/soliplex/soliplex-postgres:0.85 |
template-skill-2026.10.24-932e134 |
3 |
The [tool.soliplex-template] manifest at the end of the stack's
pyproject.toml records the skill commit (skill_source_commit) and date
(skill_generated) that generated it, and the parameters it was generated
with. (skill_version may be empty.)
Compare against a fresh stack
Each step names what the current template has. To see it in place, generate a throwaway stack with the skill release the rung walks with (see Before you start #1) and your own parameters, and diff the two; see Checking the whole stack against a reference.
How an upgrade runs¶
Every upgrade on this page should follow this phased process:
- Prepare — "Before you start": the new skill, a clean branch, the stack stopped, the state recorded and backed up.
- Run the rung — the numbered section Where do I start? named for your stack's state.
- Check it — the shared verification checklist: the audit's
database lines and a room's
ask. - Afterwards — restore dev mode, refresh the stack's own agent-facing docs. Still on the rung's branch.
- Roll out — merge the branch to
mainand bring the live stack up on the merged state.maincarries the files; the migration steps the rung rehearsed (the queue migration, the LanceDB store upgrades, the Alembic head) run live, against the deployment's own stores and databases, in the rollout's stop-apply-restart order.
Each phase completes before the next begins: a rung is not done until it has passed Check it and its commit exists, and no rung starts with the previous one unfinished.
Before you start¶
- Upgrade the
soliplex-templateskill in your harness, then pick the release that matches the rung being walked: the rung's steps run the skill's scripts (src_projects.py), and its generator renders the reference stack the rung compares against. The release this page describes is the right one for the newest rung (3); earlier rungs walk with the release that generated the shape the rung lands on — the stack's own manifest (skill_source_commitin[tool.soliplex-template]) names the release that generated it, the one the rung moving from it needs.
For instance, upgrade rung #1 above specifies the
template-skill-2026.10.10-37f7a2d skill release. If that is your
rung, ask your agent, e.g.:
```text
Set the installed soliplex-template skill to the `template-skill-2026.10.10-37f7a2d` release.
```
or run its helper yourself (`list` shows the published releases,
`diff` what a move would change; both `upgrade` and `downgrade`
land on the target from whatever is installed — no need to pass
through the newest release first):
```bash
uv run <skill>/scripts/skill_versions.py list
uv run <skill>/scripts/skill_versions.py downgrade \
template-skill-2026.10.10-37f7a2d # e.g. the rung-1 cut
```
The rung's scripts come from the installed release, so walking a
rung with a mismatched skill runs older scripts and generates an
old-template reference: a comparison against it shows nothing
to change. To check that the installed skill matches *this
page*, ask its generator for its defaults: this page's release
names the image tag, where an older one names the PyPI
constraint instead:
```bash
uv run <skill>/scripts/generate_soliplex_project.py --print-defaults \
| grep -E 'soliplex_(image_tag|backend_constraint)'
# "soliplex_image_tag": "0.85",
```
-
Start from a clean git tree, on a branch of its own, so each step below is a commit you can review, and switching back to your usual branch undoes the lot. Tag the starting point too; section 2 compares against it:
git status --short # should list nothing git switch -c upgrade-soliplex-0.85 git tag pre-upgrade-soliplex-0.85A checkout cloned under
src/(e.g. a dev-modesrc/soliplex) is its own repository, which the stack's.gitignorehides fromgit status, so check it separately: -
Stop the stack with
docker compose down, and check that nothing is left running (a one-offdocker compose runcontainer, say):Never
down -vhere: that drops the Postgres volume holding the databases this upgrade migrates. (From step 5 on,docker compose ps -aalso lists a stoppedpostgrescontainer, which step 5 starts and stops again; that one is expected.) -
Record the state of each RAG store, to compare against after the upgrade. A stack whose
docker-compose.ymlhas the one-shothaiku-ragservice runs the CLI on it:An older stack, without that service, runs it on the ingester instead.
--no-depskeeps the ingester's dependencies (Postgres, docling-serve) stopped, and-esets the queue password the config interpolates, whichrunwould otherwise lose along with the ingester's own command;infonever opens the queue, so any value will do:docker compose run --rm --no-deps -e INGESTER_DB_PASSWORD=unused \ haiku-ingester haiku-rag info --db /data/haiku.rag.lancedbRun it once per store,
rag/db/<stem>.lancedbappearing in the container as/data/<stem>.lancedb. Note thehaiku.rag version (db)and the document and chunk counts. -
Back up the data the migrations touch, into a new directory under
tmp/, which the stack's.gitignorealready ignores. The directory is named to the second, so a second run makes a second backup:cp -ainto a directory that already exists would nest the copy inside it instead. The Postgres dumps need Postgres running, and only Postgres:BACKUP=tmp/backup-$(date +%Y%m%dT%H%M%S) mkdir "$BACKUP" docker compose up -d --wait postgres for db in soliplex_agui soliplex_authz; do docker compose exec -T postgres sh -c \ 'PGPASSWORD=$(cat /run/secrets/postgres_password) \ pg_dump -h localhost -U postgres -Fc "$0"' "$db" \ > "$BACKUP/$db.dump" done docker compose stop postgres cp -a rag/db "$BACKUP/rag-db"Then check the dumps: a failed
pg_dumpstill leaves its output file behind, empty. Each should be non-empty and start withPGDMP:Use your own database names if they differ (the manifest's
agui_dbandauthz_db). -
Take a dev-mode checkout off the path. If the backend's
PYTHONPATHindocker-compose.ymllists a checkout providing thesoliplexpackage (e.g./app/src/soliplex/src), take it off for the upgrade:Ignore its closing advice to
docker compose up -d backend: the stack stays down until the end of the section.removealso takes the checkout off the backend's watch list (its--reload-dirs), and settles an older reload mode:--reload=bothbecomes--reload=config, while--reload=pythonstays, being what turns reload on without watching the config (--reload=configwould work as well).The checkout's code would otherwise override the soliplex the rebuilt image installs, and the migrations would ask the wrong release. Putting it back is the last step, Restore dev mode.
-
Fetch the migration scripts if the stack's
scripts/lacksmigrate_soliplex_dbs.pyandmigrate_rag_dbs.py(stacks generated before soliplex-template 0.17):for s in migrate_soliplex_dbs migrate_rag_dbs; do curl -fsSL -o "scripts/$s.py" \ "https://raw.githubusercontent.com/soliplex/soliplex-template/main/scripts/$s.py" doneEach is a PEP 723 shim:
uv runprovisions thesoliplex-templatepackage from PyPI, which holds the migration code itself. -
Read the scripts' advice with the upgrade in mind.
src_projects.pyand both migration scripts end by telling you to start or restart a service. Mid-upgrade, ignore that: each section says when to bring the stack up.
<skill> is wherever the soliplex-template skill is installed, e.g.
~/.claude/skills/soliplex-template. The steps below edit the manifest in
a few places, although its header says not to: they are the exception, so
that it describes the stack the upgrade leaves.
Follow the upgrade steps for your rung¶
Follow the steps listed for your upgrade rung in order. After each step, check the status of your stack's files:
If there are changes, commit them with a short checkpoint commit message:
Restore Dev mode¶
A stack that ran a soliplex checkout in dev mode can put it back now;
add puts it on the path and watches it, like any other project.
Check out the release the image runs first: the checkout supplies only
soliplex's code, and its dependencies still come from the image, so a
checkout far from the image's release fails at import time.
docker compose run --rm --no-deps backend soliplex-cli --version
git -C src/soliplex checkout v<the version just reported>
uv run <skill>/scripts/src_projects.py add soliplex
docker compose up -d backend
Check it¶
docker compose ps # every service running, backend healthy
docker compose run --rm --no-deps backend soliplex-cli audit /environment
ls backend/environment/rooms # room ids, for the 'ask' below
docker compose run --rm --no-deps backend \
soliplex-cli ask /environment <room-id> "Say hello"
For the upgrade, what matters in the audit is its database lines: both
databases OK at the same Alembic revision. It can still exit non-zero for
reasons of its own, such as models missing on the Ollama server
(soliplex-cli ollama pull fetches them). The ask should return a reply
and persist its thread. Open the frontend (http://localhost:9000 by
default) for a last look.
Afterwards¶
The stack's own agent guide (AGENTS.md, or CLAUDE.md in older
stacks) and its docs/ were written for the stack as generated, and
the upgrade leaves them describing the old setup:
backend/constraints.txt, the old image tags. Refresh them from a
reference stack generated with your parameters (see the tip at the
top), or at least treat them as out of date.
Wrap the run into main: merge the rung's branch and bring the
live stack up on the merged state. What merges is the files; what
rolls out is the state: the migration steps the rung rehearsed run
live, against the deployment's own databases and stores.
- In the live stack's clone, the merge is direct, and the rollout follows it in the same working tree.
- From a separate upgrade-only clone, the clone is the rehearsal: it proved the rung's steps against copied stores and databases; push the branch and open the PR for the files, and roll out live afterwards by re-running the rung's migration steps for real.
Upgrade rungs¶
1. Move to soliplex 0.85¶
Walk this rung with the template-skill-2026.10.10-37f7a2d skill release — its generator produces the rung's end state (Soliplex 0.85 built from PyPI; uv add-shaped). The stack being upgraded names the release that generated it in its own [tool.soliplex-template] manifest — use that
one when climbing from an older shape.
```bash
uv run <skill>/scripts/skill_versions.py downgrade template-skill-2026.10.10-37f7a2d
```
This section keeps the stack's PyPI-built backend/Dockerfile; only versions
and configuration shapes change. Skip the steps that do not apply.
-
Raise the soliplex pin, everywhere the stack has one:
-
backend/constraints.txt, andtui/constraints.txtif the stack has the TUI: -
the
soliplexentry in the rootpyproject.toml'sdependencies("soliplex >= 0.85, < 0.86"); - the
soliplexrequirement insrc/<package>/pyproject.toml. That is your own library, so check rather than overwrite: if the range it gives does not admit0.85, raise it to>= 0.85, < 0.86.
Then refresh both lock files and the host environment, so host-side work runs 0.85 from here on:
Expect large lock diffs: the new soliplex pulls in a newer dependency tree.
-
-
Add
bubblewrapto theapt-get installlist inbackend/Dockerfile, whether or not any room uses the sandbox skill yet: soliplex 0.84 and later refuse a room configuring that skill unlessbwrapcan run, and the current template always installs it. (A stack going on to section 2 replaces this Dockerfile there, so it may skip this step.) -
Bump the haiku.rag image to
ghcr.io/ggozad/haiku.rag-slim:0.96.1, in both places it appears: theFROMline ofhaiku.rag/Dockerfile, and thehaiku-ingesterservice'simage:indocker-compose.yml(in newer stacks that line also carries an&haiku_rag_imageanchor; keep it). soliplex 0.85 requireshaiku.rag-slim >= 0.96.1, < 0.97, and the ingester writes the stores the backend reads. Record the new version in the manifest:The template's two
haiku.rag.yamlfiles need no changes between haiku.rag 0.82 and 0.96.1. For a later bump, check haiku.rag's release notes for configuration changes. -
Bump docling-serve to
v1.32.0, indocling-serve/Dockerfileand thedocling-serveservice'simage:(and the commented GPU alternatives beside each). It goes with the haiku.rag bump:v1.32.0carries the docling stack haiku.rag 0.83 and later pin. It changes no stored data, but it does change how documents re-ingested from now on are chunked, and so their chunk ids; see Converter bumps. -
Rename the database settings (stacks from before soliplex 0.82, whose
installation.yamlstill hasthread_persistence_dburi:). Each section gets a new name, and its keys a_dburisuffix:thread_persistence_db: sync_dburi: "postgresql+psycopg://soliplex_agui:secret:AGUI_DB_PASSWORD@postgres/soliplex_agui" async_dburi: "postgresql+asyncpg://soliplex_agui:secret:AGUI_DB_PASSWORD@postgres/soliplex_agui" authorization_db: sync_dburi: "postgresql+psycopg://soliplex_authz:secret:AUTHZ_DB_PASSWORD@postgres/soliplex_authz" async_dburi: "postgresql+asyncpg://soliplex_authz:secret:AUTHZ_DB_PASSWORD@postgres/soliplex_authz"Rename the commented SQLite alternatives (
#sync:,#async:) the same way, so uncommenting one later does not bring the old spelling back. Keep your own role and database names if they differ. (Section 2 later switches theasync_dburidriver topsycopg.) -
Update sandbox rooms. List the rooms that configure the sandbox skill:
soliplex 0.84 flattened the skill's settings, and rejects the old keys, so in each room listed:
Before After default_environment: "bare"environment: "bare"allowed_environments: [...](gone: the room runs one environment, and the model does not choose it) sandbox_config:→execution_timeout_seconds: 30execution_timeout_seconds: 30.0(top level)sandbox_config:→max_output_chars: 100000max_output_chars: 10000(top level) -
Add the sandbox
security_optblock to thebackendservice indocker-compose.yml, after itsuser:line (where the template has it), andsecurity_opt: *sandbox_security_opttosoliplex-devif the stack has one. Add it live if a room from step 6 is to run code:security_opt: &sandbox_security_opt - seccomp=unconfined - apparmor=unconfined - systempaths=unconfinedand commented out otherwise, as a stack generated today without the sandbox carries it, so opting in later means uncommenting it:
#security_opt: &sandbox_security_opt # - seccomp=unconfined # - apparmor=unconfined # - systempaths=unconfinedadd_room.pychecks for the live block, and refuses to add a sandbox room to a stack without it. Record the choice in the manifest's[tool.soliplex-template.params]table, besideinclude_tui:Configuration explains what the block lifts and why.
-
Add the one-shot
haiku-ragservice ifdocker-compose.ymllacks it:migrate_rag_dbs.pyruns on it. It shares the ingester's settings through YAML anchors, so first name four of thehaiku-ingesterservice's own keys, the ones directly underhaiku-ingester:at its four-space indent (other services have keys of the same names; leave those alone):haiku-ingester: image: &haiku_rag_image ghcr.io/ggozad/haiku.rag-slim:0.96.1 # ... volumes: &haiku_rag_volumes # ... environment: &haiku_rag_environment # ... ulimits: &haiku_rag_ulimitsthen add the service after it:
haiku-rag: profiles: ["tools"] image: *haiku_rag_image build: context: haiku.rag dockerfile: Dockerfile entrypoint: - sh - -c - | export INGESTER_DB_PASSWORD="$$(cat /run/secrets/ingester_db_password)" exec haiku-rag --config /app/haiku.rag.yaml "$$@" - haiku-rag volumes: *haiku_rag_volumes environment: *haiku_rag_environment user: "${PUID:-1000}:${PGID:-1000}" secrets: - ingester_db_password ulimits: *haiku_rag_ulimitsCheck it by running the CLI on it. It is held out of
docker compose upby its profile, so plaindocker compose configdoes not list it, butrunenables the profile itself; against the new haiku.rag image this also reports any store migrations step 11 will apply: -
Tag the current backend image, then rebuild. The tag keeps the old image to return to if the upgrade goes wrong;
<project>is thename:at the top ofdocker-compose.yml:docker tag <project>-backend:latest <project>-backend:pre-soliplex-0.85 docker compose build --no-cache backend docker compose build haiku-ingester docling-serve(and tag and rebuild
tuithe same way, if the stack has it).--no-cachematters: the backend resolves soliplex from PyPI in a cached layer, and without it Docker can reuse a layer an earlier build resolved, silently installing an older0.85.xthan the newest. To roll back, switch back to your usual branch and tag the old image aslatestagain; a database already migrated (step 10) also needs its backup restored. -
Migrate the Soliplex databases, with only Postgres running (the script runs the rebuilt backend image in a throwaway container, and refuses while the backend service is up). Databases created by soliplex 0.81 or earlier carry no Alembic stamp, and the backend refuses to start on them until stamped:
docker compose up -d --wait postgres uv run scripts/migrate_soliplex_dbs.py --check uv run scripts/migrate_soliplex_dbs.pyStamping prints the bootstrap script's own advice to run
soliplex-cli database upgrade; the script then runs it itself, so there is nothing to do by hand. A second--checkshould report both databases at head.Do this before section 2. Releases of the script up to soliplex-template 0.17.x fetch soliplex's stamping step with
curlinside the backend image, which this PyPI-built image has and the release images do not. -
Migrate the RAG stores, with the ingester still stopped:
haiku.rag prints a warning that it is outdated, naming a newer release; ignore it, since soliplex 0.85 requires a release below
0.97.rag/db/is ignored by git, so this step leaves nothing to commit. -
Bring the stack up (
docker compose up -d), check it, and commit.
Database migrations covers both migration scripts, and what their reports mean, in full.
2. Build on the soliplex release images¶
Walk this rung with the template-skill-2026.10.20-a2c13ae skill release — its generator produces the rung's end state (the 0.85 release-image shape). The stack being upgraded names the release that generated it in its own [tool.soliplex-template] manifest — use that
one when climbing from an older shape.
```bash
uv run <skill>/scripts/skill_versions.py downgrade template-skill-2026.10.20-a2c13ae
```
From here the backend no longer resolves soliplex from PyPI: it builds on the
published ghcr.io/soliplex/soliplex-postgres image, which already holds
soliplex and its PostgreSQL driver. See
Backend image & dependencies for how the
result works.
Stop the stack first (docker compose down). The backend reloads
backend/environment/ as soon as a file there changes, so a running stack
would pick up step 5's edit on the old image.
-
Note your extras. The old
backend/Dockerfileinstalls packages withuv add. Anything on that line other thansoliplex,psycopg[binary],asyncpg,textual,textual-fspickerandtextual-serveis yours, and so is any pin inbackend/constraints.txtother thansoliplex's. Your own edits to the two files show in their history from generation to thepre-upgradetag, which leaves out section 1's changes:git diff $(git rev-list --max-parents=0 HEAD) pre-upgrade \ -- backend/Dockerfile backend/constraints.txtThat takes the stack's first commit as "as generated", which holds when the generator created the repository (its default). For a stack generated with
--no-git, or whose history was rewritten, compare against a reference stack instead (see the tip at the top).The
textualpackages go: the backend image no longer carries the TUI's dependencies, so a TUI run inside the backend container needs the TUI service instead (include_tui). -
Create
backend/requirements.txtwith your extras, one requirement per line: anhttpxon theuv addline with anhttpx >= 0.28pin inconstraints.txtbecomes the single linehttpx >= 0.28. Drop pins on packages the soliplex release already holds: the build freezes the image's own versions as constraints, so a pin that disagrees fails the build rather than moving them. With no extras, the file holds only its header:# Extra PyPI dependencies for this stack's backend image, one requirement per # line (pip requirements syntax), e.g.: # # cowsay # httpx-retries >= 0.4 # # backend/Dockerfile installs them into the soliplex image's '/app/.venv', # with every package the image already holds frozen as a constraint: an extra # may add packages, but one that needs a different version of something the # soliplex release pinned fails the build rather than moving it. # # Rebuild after editing: 'docker compose build backend' (add 'soliplex-dev' # if you use dev mode, which builds its own copy of this image). # # Local projects under src/ are not installed from here; they are mounted # and put on PYTHONPATH (see docs/users/custom-package.md). -
Replace
backend/Dockerfilewith the release-image one below. Carry over any local edits from step 1 (e.g. aCOPYofvendor/).# syntax=docker/dockerfile:1 # # Built on the soliplex release image with PostgreSQL support: '/app/.venv' # holds soliplex and its 'postgres' extra (pure-Python 'psycopg' over the # image's system 'libpq5'). Its tag picks the release: a moving # '<major>.<minor>' tag follows patch releases on # 'docker compose build --pull backend'; an exact '<major>.<minor>.<patch>' # pins one. Only what is particular to this stack is layered on below. # # The release image carries neither 'uv' nor 'bwrap': 'uv' is mounted for the # build steps that need it (never copied into the image, as upstream does), # and 'bwrap' is installed for the 'bubble-sandbox' skill. FROM ghcr.io/astral-sh/uv:0.12.18 AS uv FROM ghcr.io/soliplex/soliplex-postgres:0.86.1 WORKDIR /app # Enable bytecode compilation for faster startup ENV UV_COMPILE_BYTECODE=1 \ UV_LINK_MODE=copy # 'bwrap' for the 'bubble-sandbox' skill. Running code also needs the # backend's 'security_opt' in docker-compose.yml (generator: enable_sandbox). RUN \ apt-get update && \ apt-get install -y --no-install-recommends \ bubblewrap && \ rm -rf /var/lib/apt/lists/* # Extra PyPI dependencies for this stack, one requirement per line (see # requirements.txt). The soliplex image's own versions are frozen as # constraints, so an extra can add packages but never silently move one the # release pinned; an extra that needs a different version fails the build # instead. Skipped while the file holds only comments and blank lines. COPY --link requirements.txt /tmp/requirements.txt RUN --mount=from=uv,source=/uv,target=/usr/local/bin/uv \ --mount=type=cache,target=/root/.cache/uv \ if grep -qv '^[[:space:]]*\(#\|$\)' /tmp/requirements.txt; then \ uv pip freeze --python /app/.venv/bin/python \ > /tmp/base-constraints.txt && \ uv pip install --python /app/.venv/bin/python \ --constraints /tmp/base-constraints.txt \ --requirements /tmp/requirements.txt; \ fi # Bootstrap sandbox environments COPY --link sandbox/environments /sandbox/environments RUN --mount=from=uv,source=/uv,target=/usr/local/bin/uv \ --mount=type=cache,target=/root/.cache/uv \ for env_dir in /sandbox/environments/*/; do \ if [ -f "$env_dir/pyproject.toml" ]; then \ uv --directory "$env_dir" sync --frozen; \ fi; \ done # UID/GID the service runs as. Passed from docker-compose.yml (build.args), # which sources PUID/PGID from .env; defaults match the historical 1000:1000. # Aligning the image user with the host operator lets host-owned bind mounts # and 0600 Docker secrets be read/written without a uid mismatch. (The # soliplex image deliberately creates no user of its own.) ARG PUID=1000 ARG PGID=1000 RUN groupadd -g ${PGID} soliplex && useradd -m -u ${PUID} -g ${PGID} soliplex USER soliplex ENV PATH="/app/.venv/bin:$PATH" CMD ["/app/.venv/bin/soliplex-cli", "serve", "--host=0.0.0.0", "--proxy-headers", "--forwarded-allow-ips=*", "/environment"] -
Delete
backend/constraints.txt. -
Switch the async driver in
backend/environment/installation.yaml: bothasync_dburivalues changepostgresql+asyncpg://topostgresql+psycopg://. The image ships psycopg, not asyncpg. Leavehaiku.rag/haiku.rag.yamlalone: the haiku.rag image has its own driver, as the comment above itsdburisays. -
Replace the backend's healthcheck test in
docker-compose.yml. The image has nocurl, so thetest:entry under the backend'shealthcheck:uses the venv's Python instead; keep itsinterval,timeoutandretries:healthcheck: # The soliplex image has no curl; use the venv's stdlib instead. test: - "CMD" - "python" - "-c" - "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://localhost:8000/api/ok', timeout=5).status == 200 else 1)" interval: 30s timeout: 10s retries: 4It takes effect when step 13 recreates the container.
-
The TUI, if
tui/Dockerfileexists: replace it with the one below, builtFROM ghcr.io/soliplex/soliplex-tuiat the same tag as the backend, and deletetui/constraints.txt:# syntax=docker/dockerfile:1 # # Built on the soliplex-tui release image: soliplex plus the TUI's # dependencies (textual, textual-serve, textual-fspicker) in '/app/.venv'. It # talks to the backend over HTTP, so it needs no PostgreSQL driver. It shares # its tag with the backend's soliplex-postgres image, so the two always run # the same soliplex release. FROM ghcr.io/soliplex/soliplex-tui:0.86.1 WORKDIR /app # UID/GID the service runs as (build.args from docker-compose.yml, sourced from # PUID/PGID in .env; defaults to the historical 1000:1000). The soliplex-tui # image deliberately creates no user of its own. ARG PUID=1000 ARG PGID=1000 RUN groupadd -g ${PGID} soliplex && useradd -m -u ${PUID} -g ${PGID} soliplex USER soliplex ENV PATH="/app/.venv/bin:$PATH" EXPOSE 8002 CMD ["/app/.venv/bin/soliplex-tui-serve"] -
Update the stack's host-side dependencies in the root
pyproject.toml. Thesoliplex,psycopg[binary]andasyncpgentries become one:then refresh the lock and the environment (
uv lock && uv sync). Section 1 already moved the lock to 0.85, so the diff is small, mostlyasyncpgdropping out.src/<package>/pyproject.tomlalready admits 0.85 (section 1, step 1). -
Update the manifest's parameters in the
[tool.soliplex-template.params]table. The version parameters are renamed,soliplex_backend_constraintandsoliplex_tui_constraintgiving way to:and, if section 1 did not already set them,
haiku_rag_version = "0.96.1"andenable_sandbox(section 1, steps 3 and 7). Finally, so the manifest names the template the stack now matches, set itsskill_source_commitandskill_generatedto the upgraded skill's ownsource_commitandgenerated, from themetadata:at the top of itsSKILL.md. Only a built release carries that block: the skill as it sits in the template repository has none. A skill built from a checkout withscripts/build_skill.pygets one, stamped with the checkout'sHEAD, which does not cover changes not yet committed there. -
Watch your own package. A stack generated before the backend watched its package carries no
--reload-dirs, so an edit undersrc/<package>/needs a restart. Have it watched, as a newly generated stack's is:Its
PYTHONPATHentry is already there (unchanged);addadds the matching--reload-dirs=/app/src/<package>/srcto the serve command. Ignore its advice to bring the backend up: step 13 does that. -
Give
soliplex-deva build of its own (stacks whosedocker-compose.ymlhas asoliplex-devservice). It now builds on the backend's image plus everysrc/project'sdevdependency group, so a project's tests run there against the backend's versions, without the backend image carrying them. Make asoliplex-dev/directory, save the template's Dockerfile into it assoliplex-dev/Dockerfile:# syntax=docker/dockerfile:1 # # The dev-mode 'soliplex-dev' service: the backend's own image (compose passes # it as the 'backend' build context, 'service:backend'), plus the 'dev' # dependency group of every project under src/ -- pytest and the like -- so a # project's tests run against the very versions the backend imports it with. # # Those are listed in requirements-dev.txt, which src_projects.py generates # from the projects' pyproject.toml files. They are installed with the backend # image's own versions frozen as constraints, as backend/requirements.txt is: a # dev requirement that needs a different version fails this build. Skipped # while the file holds only comments and blank lines. # # The 'uv' tag is the one backend/Dockerfile mounts; keep the two in step. FROM ghcr.io/astral-sh/uv:0.12.18 AS uv FROM backend # The backend image runs as the 'soliplex' user; installing needs root. USER root COPY --link requirements-dev.txt /tmp/requirements-dev.txt RUN --mount=from=uv,source=/uv,target=/usr/local/bin/uv \ --mount=type=cache,target=/root/.cache/uv \ if grep -qv '^[[:space:]]*\(#\|$\)' /tmp/requirements-dev.txt; then \ uv pip freeze --python /app/.venv/bin/python \ > /tmp/base-constraints.txt && \ uv pip install --python /app/.venv/bin/python \ --constraints /tmp/base-constraints.txt \ --requirements /tmp/requirements-dev.txt; \ fi USER soliplexthen generate its requirements file from the projects' dev groups:
and point the service's
build:at it, in place of itscontext: backend,dockerfile: Dockerfileandargs::Commit both files. Step 13 builds it.
-
Build nginx on the published web client. Instead of building the Flutter client from a release tarball,
nginx/Dockerfilenow copies it from theghcr.io/soliplex/frontend-webimage, already cache-busted. Replace the Dockerfile with the template's, keeping your own-subj "…"certificate subject on itsopenssl reqline:# The built Soliplex web client, published by soliplex/frontend as an image # holding only 'build/web', already cache-busted: everything but index.html, # the manifest, favicon and icons sits under a 'v<release-tag>/' directory, # with relative URLs, so the same tree serves under any '<base href>'. Its tag # picks the release: a moving '<major>.<minor>' tag follows patch releases on # 'docker compose build --pull nginx'; an exact '<major>.<minor>.<patch>' (or # '<major>.<minor>.<patch>-<build>') pins one. FROM ghcr.io/soliplex/frontend-web:0.110 AS frontend FROM debian:trixie # UID/GID the service runs as (build.args from docker-compose.yml, sourced from # PUID/PGID in .env; defaults to the historical 1000:1000). Used below to own # the build-time self-signed TLS material and create the runtime user. ARG PUID=1000 ARG PGID=1000 # Install: # - iputils-ping for network debugging # - openssl for certificate generation, # - ca-certificates # - curl for healthcheck # - headers-more module to suppress the Server header RUN apt-get update && \ apt-get install --no-install-recommends -y \ openssl ca-certificates curl \ libnginx-mod-http-headers-more-filter \ libnginx-mod-http-ndk \ libnginx-mod-http-lua && \ rm -rf /var/lib/apt/lists/* # Generate self-signed SSL certificate RUN mkdir -p /etc/nginx/ssl && \ openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ -keyout /etc/nginx/ssl/server.key \ -out /etc/nginx/ssl/server.crt \ -subj "/C=US/ST=State/L=City/O=Organization/CN=localhost" && \ chown -R ${PUID}:${PGID} /etc/nginx/ssl ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt ENV SSL_CERT_DIR=/etc/ssl/certs ENV REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt # The built web client, served from here (see nginx.conf) COPY --from=frontend /build/web /app/build/web # nginx configuration (nginx.conf, mime.types, error-pages) is bind-mounted # from the host at runtime (see docker-compose.yml), so edits don't require # an image rebuild. EXPOSE 9000 9443 RUN groupadd -g ${PGID} soliplex && useradd -m -u ${PUID} -g ${PGID} soliplex USER soliplex HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ CMD curl -f http://localhost:9000/ || exit 1 CMD ["nginx", "-g", "daemon off;"]delete
nginx/post-build-cache-bust.sh, which it no longer runs, and innginx/nginx.confreplace the hashed-asset locations — in bothserverblocks, the HTTP one and the HTTPS one — with the one below. The image names its asset directory for the release tag, build metadata included (v0.110.0+96/), which the old pattern does not match, so without this change the assets lose their long-lived caching:location ~ "^/v[0-9]+\.[0-9]+(\.[0-9]+)?(\+[0-9A-Za-z.-]+)?/(.*)" { expires 1y; add_header Cache-Control "public, max-age=31536000, immutable"; }Then, in the manifest, set
frontend_versionto the image tag (0.110follows the 0.110 series) and dropfrontend_release_path, which nothing reads any more. -
Tag the current backend image, then rebuild with
--pull, so Docker fetches the release image, and bring the stack up:docker tag <project>-backend:latest <project>-backend:pre-release-image docker compose build --pull backend nginx docker compose up -dTag and rebuild
tuithe same way if the stack has it, and addsoliplex-devto the build if the stack has that (it builds on the backend's image). Then check it, and commit.
3. Move to Soliplex 0.86.1 and haiku.rag 0.101.0¶
Walk this rung with the template-skill-2026.10.24-932e134 skill release — its generator produces the rung's end state (the current template shape). The stack being upgraded names the release that generated it in its own [tool.soliplex-template] manifest — use that
one when climbing from an older shape.
```bash
uv run <skill>/scripts/skill_versions.py downgrade template-skill-2026.10.24-932e134
```
-
Stop
backendandhaiku-ingester, and back up the Postgres databases andrag/db/before upgrading. -
Adopt the image-shape changes, which came to the template ahead of this step: the image shim Dockerfiles (
haiku.rag/Dockerfile,docling-serve/Dockerfile) are gone; their build recipes moved toREADME.mdfiles beside their build contexts, and the template'sbuild:stanzas for those services ship commented. Delete your copies (or rename them to theREADME.mds, which carry the recipes), and comment the stanzas the same way. -
Set the backend and optional TUI image tags to
0.86.1; set the haiku.rag image to0.101.0in thehaiku_rag_imageanchor indocker-compose.yml; set thedocling-serveimage toquay.io/docling-project/docling-serve-cpu:v1.38.0. -
Update
soliplex_image_tag,haiku_rag_version, andsoliplex_dev_constraintin the project's manifest. Use>= 0.86.1, < 0.87for the Soliplex requirement in the root and packagepyproject.tomlfiles, then runuv lockin each project. -
In
haiku.rag/haiku.rag.yaml, move the source list fromingester.sourcesto thesourceslist of theingester.databasesentry keyedhaiku.rag(the key is the database's own name, dot included). Keep its source ids and paths. Addnamespace: soliplexunderingester.queue. This still writes/data/haiku.rag.lancedb. Additional databases are described in RAG pipeline. -
In the
haiku-ingesterCompose command, changesh -ctosh -ecand insert this command after exportingINGESTER_DB_PASSWORDand beforeexec haiku-ingester --config /app/haiku.rag.yaml serve: -
Rebuild
backend,soliplex-dev,haiku-ingester,docling-serve, and the optionaltuiwithdocker compose build --pull. -
Before restarting the ingester, follow the queue migration to migrate the schema and assign existing rows to
soliplex. Otherwise the ingester will not see its previous jobs and sync state. The template prepares the queue schema before every ingester start. Manual namespace assignment is needed only when upgrading rows from a queue without namespaces. -
The ingester's new database API is
GET /databases/{name}; update any callers ofGET /databaseto follow suit. -
Run
uv run scripts/migrate_soliplex_dbs.py --checkanduv run scripts/migrate_rag_dbs.py --check; apply any reported migrations with the relevant service stopped. Start the stack withdocker compose up -dand inspect the backend and ingester logs. Then check it, and commit.