Operating an instance: backup, upgrade, dataset updates
What an operator needs to know on day two: what holds the state of an instance, what to back
up and when, what a code upgrade does to the database and how to roll it back, and how a
dictionary update differs from a code update. The upgrade itself, step by step:
upgrading.md.
Nota
This page says what to back up and when; the pg_dump / pg_restore commands, for the
bundled database and for any other, are in database.md.
What holds state
The database behind DATABASE_URL is the only state of an instance:
- the dictionary — imported entries and every edit made in the admin UI; on an instance with
several datasets (
datasets.md), every one of them, each in a schema of its own inside the same database; - the registry of the datasets (
datasets) with the terms of each, and which one is active; - the
settingstable, includingen_dataset_version, a mirror of the version of the active dataset (the registry is whatGET /api/v1/metareports); - the
migrationstable — which schema migrations have been applied — and thedataset_migrationsof every dataset.
Both processes (server and frontend) are stateless: the only files the server writes are temporary (under the system temp directory, while an import or export runs). A backup of the database is a backup of the instance. Keep alongside it the things that are not data but are needed to bring an instance back:
- the
.env(or the process environment) — in particularADMIN_USERNAME/ADMIN_PASSWORD, which are the only credentials; there is no user table; - the folder named by
DICTIONARY_IMPORT_DIR, if the instance imports datasets from a local folder (offline-import.md) — it is a source, not state, and can be re-created from the dataset.
Back it up on whatever schedule matches how often the dictionary is edited — a dictionary
imported once and never edited needs one backup, after the import; an edited one on a
schedule, with a cron recipe in database.md.
External uptime check
The instance's own monitoring (observability.md) runs next to it and
goes down with the host. Something outside — an uptime service, a cron job on another machine,
the load balancer's health check — should poll the readiness probe and alert when it is not
200:
GET https://dict.example.com/api/ready every 1–5 minutes, alert after 2 failures
/api/ready is the right probe for this: it turns 503 when the database is unreachable, an
import runs or fails, or a stop begins, and its body names the reason
(deployment/README.md). /api/health only says the process
answers HTTP. Polling readiness also feeds the VbhNotReady alert rule of the overlay, which
watches the probe's own answers (observability.md). An
installation with the website adds its start page (GET /en, 200) to the same check.
Database backup vs dictionary export
The admin UI has Export on the card of a dataset (GET /api/en/dictionary/export). It is not a backup:
Database backup (pg_dump) | Dictionary export (dataset) | |
|---|---|---|
| Contains | Everything: entries, edits, settings, ids, migrations | Dictionary content, origins, licenses, notices and edit history; JSONL, manifest, format marker and LICENSE |
| Restores | The instance exactly as it was | Nothing — importing it merges into the current dictionary |
| Ids and timestamps | Preserved | Dictionary row ids are reassigned; source/acquisition dates and history remain in the portable data |
| Made for | Disaster recovery, rollback of an upgrade | Sharing, versioning, moving content between instances, publishing |
| Format | Postgres-specific, tied to the schema version | Portable, diffable, independent of the database |
The export is taken from the dataset selected on its card (the active one when the API request specifies none). Its manifest and word/history records retain that dataset's terms and the sources and contributions of its words; the database backup holds all datasets.
Take the export when you want the content — to publish it, diff it against the upstream
dataset, or seed another instance (offline-import.md). Take the
database backup when you want to be able to go back. The data terms of an export are in
DATA_LICENSE.md.
Upgrading the code
Consejo
The step-by-step upgrade for each way of installing — the published images, a build from a
checkout, a start without Docker — and the update notice of the admin UI are on their own
page: upgrading.md. This section is about what an upgrade does to the
database and how to get back.
On start the server applies every pending migration shipped with its version, then serves
requests (database.md). Deploying is
therefore just: ship the new version and restart. Two consequences follow:
- The database becomes bound to the new version. Once a migration has run, the previous version of the code no longer matches the schema and is not expected to work against it.
- A failed migration stops the server deliberately (fail-fast; every pending migration of a start runs in one transaction, so a failure rolls the whole batch back) — the database is left as the previous version left it and the service is down until the problem is fixed or the backup is restored.
The procedure:
# 1. back up the database (pg_dump -Fc / a provider snapshot — database.md#backups)
# 2. see what the new version is about to apply (optional)
DATABASE_URL=... yarn workspace server migration:show
# 3. deploy: new code, yarn install --immutable, rebuild both apps, restart the processes
# (docs/deployment/README.md) — pending migrations run on the server's start
# 4. confirm: GET /api/ready answers 200 (migrations ran, the database answers),
# migration:show lists nothing pending, GET /api/health returns the new version
Precaución
Rollback = restore the pre-upgrade backup and start the previous version. That is the only
supported way back. migration:revert exists for development; migrations are not guaranteed to
be reversible (some drop or rewrite data), and a revert does not undo the edits made in the
admin UI after the upgrade — which is why the backup is taken right before the restart.
Upgrades that do not ship a migration (migration:show lists nothing pending) do not touch the
schema; rolling those back is just starting the previous build again.
Importante
The frontend has no state of its own, but NEXT_PUBLIC_* values are inlined at build time — a
frontend rebuild is part of every upgrade (deployment/README.md).
Dataset updates vs code updates
The dictionary content and the code are versioned independently: the code has releases, the
dataset has its own version (manifest.json, kept in the registry of datasets after the import,
mirrored in the settings as en_dataset_version, exposed by GET /api/v1/meta as
dataset_version; read-only in the settings). Own datasets and forks can set their version
through Edit the terms. Upgrading the code never changes the
dictionary; loading a newer dataset never changes the code.
For a catalog source, use Update from a newer file on its card: download the catalog files
on the server or upload source files manually. Installation converts them and uses update
mode in that source's dataset. Catalog links may pin a release: OpenGloss currently installs
2.4 and does not check for newer releases. For independent edits, create a fork; a fork does
not automatically update or merge changes from its parent (datasets.md).
The import of the project's dataset shows both versions side by side — Your version (the registry of
datasets) against Latest version (the published manifest.json) — and offers two ways to
load a newer dataset:
- Update the dictionary — the one-click update, shown when the versions differ. It runs the
import in update mode (
POST /api/en/dictionary/importwithupdate: true): every entry already in the dictionary is replaced with the new dataset content — except the entries you edited, which are kept (see the flag below). Entries missing in the instance are added; entries that disappeared from the dataset are not deleted. The completed import reports the split — updated / added / kept with your edits — in the UI, the server log and the History page. - Start importing — the historical add-only merge: entries missing in the instance are
added, entries that already exist (same word, part of speech and form) are skipped,
whatever the dataset says about them. This is what the first load, uploads and the automatic
import on first start (
DICTIONARY_AUTO_IMPORT) do.
The user-modified flag
Every admin mutation of an entry's content — editing the word, its forms, meanings,
translations or links — flags the whole entry as modified by you, provided it changed
something: a dialog saved as it was opened leaves the entry as it was, and the next update
still replaces it (user_modified on
en_entries; an edit to a form flags its base word's entry, because a base word and its forms
are replaced as one unit). The flag is what update mode consults: flagged entries keep your
content, everything else follows the dataset. It is visible:
- on the word card in the admin UI (the Modified by you tag) and in every word answer of the
admin API (
user_modifiedonGET /api/en/{id}; the public/api/v1projection does not carry editorial state); - in the History page — the edit that set it is a row of the history of edits, with the values it changed.
The flag answers one question: is this entry kept through an update. Whether an entry differs
from its source is another one, answered by the history of edits
(datasets.md): readers are told that
an entry was changed for as long as an edit of it shows in what is served, whatever the flag
says.
Return to the official version on the word card (or PATCH /api/en/reset-user-modified/:word) clears the flag: your content stays until the next update
replaces the entry with the dataset again. Two caveats:
- deleting an entire entry leaves nothing to carry the flag, so the next update re-adds the entry from the dataset; deleting only a part of it (one part of speech, a form, a meaning) flags the entry and survives updates;
- a word the admin created from scratch is flagged from the start — updates never touch it.
Sizing
The full English dictionary takes about 0.9 GB in Postgres, tables and indexes together; a
compressed pg_dump -Fc of it is about 150 MB. Plan for the database, the working space Postgres
needs for index builds during migrations, and the backups you keep. The breakdown by table and the
row counts: database.md.
Additional datasets and forks need their own database space. Source installation also needs temporary disk space for both the downloaded/uploaded files and the converted output; OpenGloss alone downloads about 1.32 GB before conversion. The source and converted working files are removed after installation, including on failure. See installation.
SQLite is development-only — nothing to back up or upgrade, and the server refuses it with
NODE_ENV=production (database.md).