# Backups & restore

> Daily database dumps and volume snapshots, retention against your plan, downloads, and restores from an instance's own backups.

An instance has up to two backup streams. Each is a [plug](/platform/operations-and-runs#plugs) whose runs record their files as artifacts:

| Stream | Plug | Artifact |
| --- | --- | --- |
| Database | `backup` | PostgreSQL: one `pg_dump` custom-format file (`.dump`). MySQL (Ghost): one plain `mysqldump` file (`.sql`). |
| Volumes | `snapshot_volumes` | One gzipped tar (`.tar.gz`) per data volume. |

An instance with a database has the dump; one with data volumes has the snapshot. A custom app's dump covers the database it declares and its snapshot the volumes; with neither declared, it has no backup stream. `GET /api/v1/instances/{slug}/plugs` lists what an instance supports.

## Schedule

Each stream gets a daily routine when the instance is created, at 04:00 UTC plus a minute derived from the instance. The volume snapshot runs on a different minute from the database dump. Change the cadence or disable the routine on the **Actions** tab, or with `PATCH /api/v1/routines/{id}`. A second routine on the same plug (an hourly dump beside the daily one) needs `POST /api/v1/routines`; see [routines](/platform/operations-and-runs#routines). **Run now** on the same tab, or `POST /api/v1/instances/{slug}/plugs/{plug}/runs` (`write_plugs`), takes one immediately.

The **Overview → Composition** card shows each container and volume with what backs it up, for example `pg_dump · daily · off-host`. A row with no successful backup in 48 hours reads `stale`; one that has never had one reads `never run`. Lunary's operators are alerted when a backup run fails and when a stream goes 48 hours without a successful run.

## What a backup contains

- **Database dump**: taken inside the database container with the container's own credentials, then checked before upload (the custom-format header for PostgreSQL, the completion line for MySQL). A dump that fails the check fails the run.
- **Volume snapshot**: read-only and crash-consistent. Complete files are captured; a file being written during the snapshot may be partial. An embedded database kept inside a volume, such as SQLite, is captured as it would be after a power loss.

Backups are stored in object storage in Canada, off the server they came from.

## Downloading

Dashboard: the **Activity** tab, then the run, then **Download** beside each artifact. API: `GET /api/v1/runs/{id}/artifacts/download?key=…` (`read_plugs`) returns a presigned `url` valid for 15 minutes, with its `expires_at`; pass the artifact's `key` from `GET /api/v1/runs/{id}` as given. MCP: `get_artifact_url`.

The files are standard formats: a `.dump` restores with `pg_restore`, a `.sql` with the `mysql` client, a volume archive with `tar -xzf`.

## Retention

Your plan caps the backup storage you retain: 100 GiB on Essential, 500 GiB on Pro, 1,024 GiB on Enterprise. The dashboard's **Backups** tile shows the reading against the cap. Storage over the cap is reclaimed oldest first, across all your instances, after each backup and in a daily sweep. Each instance's newest database dump and newest volume archive are kept whatever the cap. A pruned artifact shows `pruned_at` in its run detail and can no longer be downloaded or restored from.

When those newest backups alone reach the cap, new backups are refused. Running the plug directly (**Run** on the **Actions** tab, `POST /api/v1/instances/{slug}/plugs/{plug}/runs`, or MCP `trigger_plug`) is refused up front, with `422 over_budget` over the API. A routine's run, scheduled or started by hand (**Run now**, `POST /api/v1/routines/{id}/runs`), ends `unauthorized` and pauses the routine (`paused_reason: "over_budget"`). Resume it once the cap is raised. Dry runs are exempt.

## Restoring

Two plugs restore an instance from its own backups:

- `restore` replaces the database with one of the instance's dumps, named by `key`: the dump's artifact key without the leading `<org>/`.
- `restore_volumes` replaces every volume in one of the instance's snapshots, named by `snapshot_run`: the id of the snapshot run. Files written since the snapshot are lost.

The instance's app containers stop for the restore and restart afterwards. Both plugs take `dry_run`, which downloads and checks the backup and changes nothing. A passing dry run means the backup is intact; for `restore` it also means the database is reachable with its own credentials. It does not replay the restore.

Dashboard: the **Actions** tab, **Run** on the plug, then pick the backup from the list; tick **Dry run** to check it first. API (`write_plugs`):

```bash
curl -X POST https://lunary.cloud/api/v1/instances/nocobase/plugs/restore/runs \
  -H "Authorization: Bearer lc_your_token_here" \
  -H "Content-Type: application/json" \
  -d '{"params": {"key": "nocobase/backup/20260731T120000Z.dump"}, "dry_run": true}'
```

MCP: `trigger_plug`. A key or run that is not one of the instance's own backups is refused (`422 unknown_artifact`). To copy another instance's database, use a [data sync](/platform/services-and-instances#mirroring-another-instance).

## After an instance is deleted

Deleting an instance keeps its data on the server for 30 days, and provisioning the same app under the same slug in that window starts it on that data. `DELETE /api/v1/instances/{slug}?remove_volumes=true` deletes the data at teardown instead. Either way the instance's backups stay downloadable from its runs (`GET /api/v1/runs?instance_slug={slug}`), pruned as above, until 30 days after the teardown, when they are deleted.
