Documentation / Exploitation / Guides pratiques / Back up and restore

Back up and restore

This recipe covers what actually needs backing up on a Meridian instance and the restore sequence to bring it back. For why the durable state lives in Restate rather than in application code, see explanation/why-durable-execution.md; for the process/volume layout referenced below, see reference/topology.md.

What to back up

Data Where Criticality
Durable run state (journal, suspended runs) restate-data volume Critical — this is the only copy
Run index (id, workflow, patient, event, date) proxy-data volume (SQLite) or Postgres Convenience only — rebuildable
Workflows, agents, instance manifests YAML files in git Already backed up (the repo is the backup)
Patient record The hospital's own FHIR warehouse Out of platform scope

The distinction that matters: Restate's journal is the only place a run's state actually lives. Losing the run index loses the list the API shows at GET /api/runs, not any run's execution state — see reference/run-store.md for what that index does and does not hold. Losing restate-data loses in-flight and suspended runs outright.

Nothing else needs a backup procedure:

  • Workflow, agent, and instance-manifest YAML are versioned in git — the clone/checkout you deploy from is already the backup.
  • The plugin cache (MERIDIAN_PLUGIN_CACHE, default .data/plugins) only holds copies re-downloadable from the registry at next boot — see Set up a plugin registry. Losing it costs a re-download, not data.
  • The patient record lives in the hospital's FHIR warehouse, connected per Connect a FHIR warehouse — it is outside this platform's backup responsibility.

Back up Restate's durable state

Restate persists its journal to the path mounted at /restate-data, backed by the restate-data volume in docker-compose.yml. Snapshot that volume with Restate stopped, so nothing is mid-write:

1. Stop Restate

docker compose stop restate

Stopping only the restate service is enough — endpoint and proxy will simply fail their calls to it until it comes back, they don't need to be stopped to take a consistent snapshot of restate-data.

2. Snapshot the volume

docker run --rm \
  -v restate-data:/data:ro \
  -v "$(pwd)":/backup \
  alpine tar czf /backup/restate-data-$(date +%Y%m%d-%H%M%S).tar.gz -C /data .

Store the resulting archive wherever your other backups go (off-host, per your retention policy) — there is nothing Meridian-specific about handling this tarball.

3. Restart Restate

docker compose start restate

Back up the run index

SQLite (default)

The proxy's proxy-data volume holds the SQLite file at the container path configured by RUN_DB_URL (default sqlite:/data/runs.sqlite in compose, sqlite:.data/runs.sqlite for a bare process — see apps/api/src/server/run-store.ts). Copy the file directly; a stopped or running proxy can both be copied since SQLite runs in WAL mode, but stopping the proxy first avoids capturing a WAL file mid-checkpoint:

docker compose stop proxy
docker run --rm -v proxy-data:/data:ro -v "$(pwd)":/backup \
  alpine cp /data/runs.sqlite /backup/runs-$(date +%Y%m%d-%H%M%S).sqlite
docker compose start proxy

Postgres

If you've switched the run index to Postgres (see Use Postgres for the run index), back it up the same way as any other Postgres database in your stack — this index is convenience data, not the source of truth, so an occasional dump is enough:

pg_dump "$RUN_DB_URL" -f runs-$(date +%Y%m%d-%H%M%S).sql

Restore

Restore order matters: Restate first (it's the source of truth), then the processes that depend on it, then the run index — which, if lost, just starts empty rather than blocking anything.

1. Restore Restate

docker compose stop restate
docker run --rm -v restate-data:/data -v "$(pwd)":/backup alpine sh -c \
  "rm -rf /data/* && tar xzf /backup/restate-data-<timestamp>.tar.gz -C /data"
docker compose start restate

Keep RESTATE_NODE_NAME identical to what it was when the volume was written. Restate otherwise derives its node name from the container's hostname; a new container gets a new hostname and refuses to start against the restored data (error RT0002) — this is exactly why docker-compose.yml pins RESTATE_NODE_NAME: restate-dev for the dev service instead of leaving it to default. Pin the same fixed name in whatever compose file or manifest you restore into.

2. Restart the endpoint

docker compose restart endpoint

The endpoint self-registers with Restate's admin API on startup with retries (apps/api/src/restate/register.ts, autoRegister) — no manual re-registration step is needed. Restate resumes any run left suspended or in-flight in the restored journal from wherever it was left off; this is the same guarantee an ordinary endpoint restart relies on, see Upgrade the platform.

3. Restart the proxy and UI

docker compose restart proxy ui

4. Restore the run index (optional)

Only do this if you want old entries back in GET /api/runs — it does not affect any run's actual state:

# SQLite
docker compose stop proxy
docker run --rm -v proxy-data:/data -v "$(pwd)":/backup \
  alpine cp /backup/runs-<timestamp>.sqlite /data/runs.sqlite
docker compose start proxy

# Postgres
psql "$RUN_DB_URL" -f runs-<timestamp>.sql

Verify

Confirm the stack is healthy and the restored journal is actually being served:

curl http://localhost:4000/readyz

/readyz returns 200 only once both the Restate ingress and the run index respond (apps/api/src/server/api.ts); on failure it returns 503 with a { checks } detail naming which one is down. Then confirm a run that existed before the backup is still resolvable:

curl http://localhost:8080/meridianWorkflow/<eventId>/getRun
75 documents15 sectionssource : /docs · généré au build