# Backup and restore

Protect database and media together, rehearse recovery and resume schedules deliberately.

GroCMS 0.1.0 · Setup & operations · Deployment operators and administrators responsible for recovery · Reviewed 15 September 2026

## Protect the complete installation

A usable recovery set includes the database, immutable media files, compatible application code or images, and the protected configuration needed to run them. The database contains content history, publication pointers, routes, permissions, accounts and pending work. It does not contain the image and document bytes stored in the private media volume. A content export is useful for portability and inspection, but it excludes authentication, jobs and media bytes and is not the operations backup described here.

The included backup helper uses a maintenance window. It stops the target project's running web and worker, creates a custom PostgreSQL dump, archives media and restarts the exact containers it paused. PostgreSQL's dump provides a database snapshot; it cannot synchronize unrelated filesystem changes by itself. Ensure no separately running scripts, deployments or external writers are changing this installation while the database and media are being captured.

Backup, deployment, setup, restoration and local marketing maintenance commands share a project operation lock in the workspace. A concurrent command fails instead of overlapping the operation. Their read-only preflight refuses ambiguous recovery containers before service actions or backup-health database receipts. If a killed or timed-out process leaves a lock file, confirm that its recorded process and Docker operation have finished before removing that specific lock. Backups validate both archives, restart exact original containers and verify web readiness plus a fresh worker heartbeat before writing a completed manifest. An intentionally paused worker must return to its paused state. Docker accepting a start command alone is not successful recovery; a failed or partial run has no completed manifest.

Sources: Database and media backup helper (repository: `scripts/ops/backup.mjs`); Recovery policy and retention commands (repository: `docs/operations.md`); [PostgreSQL 18: SQL Dump](https://www.postgresql.org/docs/18/backup-dump.html).

## Create and retain a completed backup

```powershell
node scripts/ops/backup.mjs --env-file .env.production.local --project gcms --output backups/RELEASE_BACKUP
```

Choose a new output directory for each run. The helper writes database.dump, media.tar.gz and a manifest containing file checksums, database version, source image information and the backup completion time. An incomplete run has no valid completed manifest. Check the command result and the restarted services before treating the run as successful. Reusing the same directory or manually declaring a partial archive complete defeats the verification boundary.

Store an encrypted off-host copy and verify that the copy is available and readable. Keep production secrets and host configuration in a protected recovery store separate from the manifest. The archive still contains sensitive content and account records even though it omits environment secrets. Restrict access to the backup directory, including Windows ACLs where applicable. Retain the application release associated with the backup rather than assuming the newest code can safely read every older schema.

Sources: Database and media backup helper (repository: `scripts/ops/backup.mjs`); Archive checksums and operations guards (repository: `scripts/ops/common.mjs`); Recovery policy and retention commands (repository: `docs/operations.md`).

## Restore into a new isolated project

Prepare a private .env.restore.local with the recovery configuration, a separate loopback inspection port and GCMS_WORKER_PAUSED=true. Choose a fresh project name beginning gcms-restore-. The restore helper refuses an existing target's containers or volumes and verifies checksums before creating the new database. It leaves a failed attempt isolated for diagnosis. Select another fresh target for a retry instead of deleting unfamiliar data to satisfy the guard.

```powershell
node scripts/ops/restore.mjs --backup backups/RELEASE_BACKUP --project gcms-restore-check --env-file .env.restore.local
```

The helper initializes database roles, restores in one transaction, preserves archived table privileges and checks that the runtime role cannot mutate immutable revisions. It restores media into a new volume with the application's ownership. Only the database starts. Web and worker remain stopped, so an overdue schedule cannot publish simply because the database has reappeared. The PostgreSQL 18 image uses a version-specific data layout; use the supplied volume definition rather than copying an older image tutorial blindly.

Sources: Fresh-target restore helper (repository: `scripts/ops/restore.mjs`); Archive checksums and operations guards (repository: `scripts/ops/common.mjs`); [PostgreSQL 18: SQL Dump](https://www.postgresql.org/docs/18/backup-dump.html); [PostgreSQL official Docker image](https://hub.docker.com/_/postgres).

## Inspect before resuming work

Select the compatible application image and verify the target origin and port before starting the web service. The --no-deps flag in the inspection command avoids automatically starting its worker dependency path. Log in to the restored site, compare working and published revisions, inspect routes and search, and check representative media checksums. Confirm restricted runtime grants and review the pending schedule records against the intended recovery point.

```powershell
docker compose --env-file .env.restore.local --project-name gcms-restore-check up -d --no-deps web
```

An operator may also start a worker explicitly in paused mode to check database connectivity; paused readiness is not evidence that jobs are executing. Reconcile overdue intentions and any external effects that may already have happened before deciding to cancel, reschedule or resume them. Only after verification should the operator configure the final hostname and SMTP, remove the deliberate pause and switch traffic. The local rehearsal verified matching revision pointers, media bytes and a held overdue schedule, rather than merely importing an empty table.

Sources: Fresh-target restore helper (repository: `scripts/ops/restore.mjs`); Paused worker behavior (repository: `apps/cms/src/worker/index.ts`); Full content and media recovery verification (repository: `docs/evidence/full-content-restore.json`).

## Separate recovery targets from measured guarantees

The planning targets are a fifteen-minute recovery point and a four-hour recovery time. The completed local content-and-media restore finished well within four hours for its small fixture. That does not prove an off-host restore or the same duration at the 100,000-record stress size. Record real archive size, transfer time, import duration and verification time for the installation you operate, and rehearse the complete process on a replacement environment.

A backup starting every fifteen minutes cannot by itself promise a fifteen-minute recovery point when creation and transfer take extra time. The start interval plus maximum delay until a verified off-host copy is available must fit the target; a ten-minute interval with a five-minute completion-and-transfer bound is one conditional example. Failed runs also need detection and escalation. The repository does not install a scheduler or continuous PostgreSQL point-in-time recovery. Those remain explicit operational choices.

Sources: Recovery policy and retention commands (repository: `docs/operations.md`); Full content and media recovery verification (repository: `docs/evidence/full-content-restore.json`); Measured dataset and capacity limits (repository: `docs/evidence/performance.md`).

## Retain useful history without endangering live content

Revision retention is a separate audited maintenance operation, not a substitute for backup. Its default preview keeps revisions younger than thirty days and the newest twenty per resource, and considers at most five hundred eligible revisions per batch. Working and published heads, review and schedule references, and unexpired preview selections stay protected. Runtime credentials cannot delete immutable history, so the command requires maintenance authority and the matching private media location.

```powershell
node --env-file=.env.maintenance.local --import tsx scripts/retention.ts --site SITE_UUID --operator OPERATOR_NAME
```

Inspect the dry-run result before adding --apply. Physical media cleanup follows the committed retention audit, allowing interrupted file cleanup to resume without making live database references disappear. Backup retention policy must be considered separately: deleting old live history does not remove it from previously retained archives. Operators should document both policies, the people allowed to run them and the recovery evidence supporting them.

Sources: Audited retention and interrupted cleanup (repository: `scripts/ops/retention.ts`); Recovery policy and retention commands (repository: `docs/operations.md`).

## References

- Database and media backup helper — repository path: `scripts/ops/backup.mjs`

- Fresh-target restore helper — repository path: `scripts/ops/restore.mjs`

- Archive checksums and operations guards — repository path: `scripts/ops/common.mjs`

- Recovery policy and retention commands — repository path: `docs/operations.md`

- Paused worker behavior — repository path: `apps/cms/src/worker/index.ts`

- Full content and media recovery verification — repository path: `docs/evidence/full-content-restore.json`

- Measured dataset and capacity limits — repository path: `docs/evidence/performance.md`

- Audited retention and interrupted cleanup — repository path: `scripts/ops/retention.ts`

- [PostgreSQL 18: SQL Dump](https://www.postgresql.org/docs/18/backup-dump.html). Database snapshot, restore and role prerequisites; filesystem coordination is provided by the CMS helper.

- [PostgreSQL official Docker image](https://hub.docker.com/_/postgres). PostgreSQL 18 storage layout and initialization behavior.

## Related guides

- [Production setup and upgrades](https://grocms.com/docs/production-setup)

- [Troubleshooting](https://grocms.com/docs/troubleshooting)

- [How the CMS works](https://grocms.com/docs/how-cms-works)
