本页暂无完整中文版。以下先提供中文导读,随后是英文原文。
theta_ai schema and vector extension in place.
What actually holds your data
compose.yaml declares two volumes. The database and uploaded files both need
backups; the application image can be pulled again.
There is no
mirobody_charts volume. The ChartService MCP tools that
rendered PNGs through a Node toolchain were removed; the agent now writes a
fenced vis-chart data block that the frontend renders, so nothing writes
chart files at all.
Two things that make copy-pasted commands fail silently
Volume names are project-prefixed. The volume declared asmirobody_upload is <project>_mirobody_upload on the daemon — for a checkout
in mirobody/, that is mirobody_mirobody_upload. docker volume ls shows
the real names. This matters because getting it wrong does not error:
shell/backup.sh checks docker volume inspect before reading anything, so a
name that does not exist is reported rather than archived as 45 empty bytes.
Container names come from service names. There is no container_name: in
compose.yaml and the database service is pg, so the container is
mirobody-pg-1, not mirobody-db-1. Prefer docker compose exec pg …, which
does not care what the container is called.
Backing up
pg_dump -Fc, not a tar of/var/lib/postgresql/data. Tarring a running cluster copies files that are moving under the tar; the result restores, or does not, depending on timing.pg_dumpis consistent by construction and restores into any pgvector image rather than only a byte-identical one.- The dump is verified before the run is called a success.
pg_restore --listparses the archive’s table of contents, which fails on a truncated or empty file. A backup nobody has read is a hope, not a backup. - The dump is written and verified inside the container, then copied out. A
custom-format archive is not seekable through a pipe, so verification cannot
read one from stdin, and
pg_dump > filethat dies half-way leaves a plausible-looking truncated file. - Uploads are archived only if the local volume exists (see the table).
- The upload archive streams into a host file and is read back before it is
accepted. A Docker VM can map a host
/tmpbind mount to its own/tmp, leaving a successful tar command but no archive on the host. - Retention is pattern-scoped — it prunes
mirobody-db-*.dump,mirobody-uploads-*.tar.gzolder thanRETENTION_DAYS, and leaves anything else in the directory alone.
Restoring
Stop the app first so nothing writes while the schema is being replaced. Keeppg running — it is what does the restoring.
Database
Into a fresh database (the safe path — the old one stays until you are satisfied):PG_DBNAME=holistic_db_restored in .env and start the
app. To restore over the existing database instead, add --clean (it drops
each object before recreating it) and be aware that there is no undo:
pgvector/pgvector image already has the extension. Nothing writes new
vectors any more (the semantic tier went in 1.5.0), so a restore needs no
re-embedding.
Uploaded files
docker volume ls first. The archive is made
with -C /data ., so it extracts as the volume’s contents, not as a nested
data/ directory.
Then
Before upgrading
- Run
shell/backup.shand keep the output off this machine. This is the whole checklist, because it is also the rollback: there are no down migrations. - Read
CHANGELOG.mdfor the version you are moving to. git pull && ./deploy.sh— the script keeps existing volumes and pulls the selected application image before waiting for healthy services.- Watch that log tail: the schema DDL runs on the first start (see below), and
a file that fails is logged with its
sql_filename.
Upgrading from 1.5.2
1.5.3 runs a built image with Postgres as its only state service; 1.5.2 ran the checkout with Redis beside it../deploy.sh does the rest, with four
things to know first:
-
Stop the worker and let it finish (
docker compose stop mirobody_workeron the 1.5.2 checkout) if a profile refresh was queued. Redis’s queue is not carried over; a lost refresh is redone at the next upload. -
A
compose.override.yamlwritten for 1.5.2 stops the deploy. It names the redis service and mounts the checkout over/app. Move it aside and start from the current example: -
Secrets carry over. The overlay’s
JWT_KEYstays in charge, so nobody is signed out. A database the 1.5.2 compose stack created keeps its password and its field-encryption key, which wereconfig.yaml’s placeholder (see SECURITY.md before exposing it). A value the app encrypted in the overlay that compose needs in plain text (PG_PASSWORD) is asked for. -
Uploads change owner once. 1.5.2 ran the app as root, so its upload
directories are root’s; the 1.5.3 image runs as uid and gid 10001. The
mirobody_initservice incompose.yamlgives the upload volume (or theuploadbind mount ofcompose.override.yaml.example) to that user before every start, so nothing is needed by hand, and a 1.5.2 upload archive restored later is handled the same way. After the upgrade, upload one file to check. -
Afterwards the redis container is removed (
--remove-orphans); the script names the two 1.5.2 volumes that are no longer used (*_mirobody_redis,*_mirobody_site_packages) fordocker volume rm. Personal MCP links from 1.5.2 stop working: make new ones in Settings. The first start builds one index onth_observation(idx_th_observation_user_created), which on a large table holds writes to it for that long.
How the schema actually changes
server/bootstrap.py::create_schema replays every file in
mirobody/schema/*.sql, in filename order, on each start. Every file is
written to be safely re-runnable — there is no ledger of what has been applied,
and no schema-version table by design. Verified by running the whole set three
times against a clean database: zero errors. A failing file is rolled back on
its own and logged; the rest still run, so one bad increment cannot leave half
a schema with no explanation.
Two consequences worth knowing:
- Upgrades are additive. A new release adds tables or columns on first start; you do not run a migration command.
- There is no automated downgrade. Rolling back a release that added a column means restoring the dump you took in step 1. That is why step 1 is step 1.