---
title: "Backups"
description: "Creating, configuring and controlling backup definitions."
url: "https://saved.sh/docs/cli/backups"
---

A backup is a definition. These commands create and shape it; the schedule runs it.

For anything more than one backup, prefer [`sctl apply`](/docs/cli/apply) with a manifest.
These commands are the imperative equivalent, and are what `apply` calls underneath.

## Listing [#listing]

```bash
sctl backup list
```

```
NAME       ID                                    KIND    SOURCE     STATE   SCHEDULE
prod-db    018f3c2a-9e11-7c4d-b0a1-2e6f5d3c9a70  local   postgres   active  0 2 * * *
uploads    018f4d1b-...                          cloud   s3         paused  0 4 * * *
```

This is where you get the IDs everything else needs.

```bash
BACKUP_ID=$(sctl backup list | awk '$1 == "prod-db" { print $2 }')
```

## Creating [#creating]

```bash
sctl backup create prod-db --kind local
```

`--kind` is required and is one of `local`, `cloud` or `manual`.

<Callout type="warn">
  **Kind is immutable.** It decides who holds your source credentials, so changing it would
  silently move a credential across a trust boundary. `sctl backup configure` has no `--kind`
  flag for that reason. To change it, create a second backup.
</Callout>

A new backup is a **draft**. It holds no schedule and produces nothing until it is complete.

## Configuring [#configuring]

```bash
sctl backup configure "$BACKUP_ID" \
  --source-type postgres \
  --worker <worker-id> \
  --schedule "0 2 * * *" \
  --keep-last 10 \
  --expire-after 90d
```

**Omitted flags are left alone.** The update is a merge, so you can configure one field
without restating the rest. The backend activates the backup itself as soon as the definition
is complete.

| Flag                      | Applies to       | Notes                                          |
| ------------------------- | ---------------- | ---------------------------------------------- |
| `--name`                  | All              | Rename                                         |
| `--source-type`           | `local`, `cloud` | Must be legal for the kind                     |
| `--worker`                | `local`          | Worker **ID**, required before it can activate |
| `--schedule`              | `local`, `cloud` | Cron, evaluated in UTC                         |
| `--source key=value`      | `cloud` only     | Repeatable. Non-secret source facts            |
| `--credential key=value`  | `cloud` only     | Repeatable. Secrets, routed to our vault       |
| `--encryption-key <path>` | All              | Path to an armoured public key file            |
| `--compression`           | All              | Enable gzip                                    |
| `--compression-level`     | All              | 1 to 9, or 0 for the default                   |
| `--keep-last`             | All              | Retention floor                                |
| `--expire-after`          | All              | Retention age limit, e.g. `90d`                |
| `--lock-for`              | All              | Protection window, e.g. `30d`                  |

### Cloud sources [#cloud-sources]

```bash
sctl backup configure "$BACKUP_ID" \
  --source-type postgres \
  --source host=db.example.com --source port=5432 --source database=app \
  --credential password="$PGPASSWORD" \
  --schedule "0 2 * * *"
```

`--source` carries facts we store and return; `--credential` carries secrets we store in the
vault and never return. Sending a secret in the wrong one is corrected for you: the split is
judged by the source type, not by which flag you used.

<Callout type="warn">
  A **local** backup takes neither. Its credentials live in the worker's own `config.yaml` and
  never reach us, so `--source` and `--credential` are refused on one.
</Callout>

### Retention is write-once [#retention-is-write-once]

```bash
sctl backup configure "$BACKUP_ID" --keep-last 10 --expire-after 90d --lock-for 30d
```

<Callout type="error">
  Retention can be set **once**. Changing it afterwards is refused with `409
    retention_immutable`, including loosening it. Re-sending the identical policy is accepted.
  Decide it before the first run, or create a new backup.
</Callout>

`--lock-for` may never exceed `--expire-after`: a policy cannot forbid a deletion it also
requires.

## Running one now [#running-one-now]

```bash
sctl backup trigger "$BACKUP_ID"
sctl run list --backup "$BACKUP_ID"
```

A triggered run is ordinary in every respect: same pipeline, same artifact, same retention,
same billing. It does not shift the schedule.

<Callout type="warn">
  A trigger **always runs**, even when a scheduled run is already in flight. Scheduled fires
  skip when one is running; manual triggers do not.
</Callout>

## Pausing [#pausing]

```bash
sctl backup pause "$BACKUP_ID"
sctl backup resume "$BACKUP_ID"
```

Pausing suspends the schedule and leaves the definition and its artifacts alone. Runs are not
queued while paused: resuming starts from the next scheduled fire.

A manual backup has no schedule and cannot be paused.

## Submitting to a manual backup [#submitting-to-a-manual-backup]

```bash
sctl backup submit "$BACKUP_ID" ./quarterly-export.tar.gz
```

```
Submitted ./quarterly-export.tar.gz as run manual-018f3c2a-.... post-backup is running.
```

One command does the whole flow: create the run, request an upload target, upload directly to
storage, and confirm. The bytes never pass through our API.

## Deleting [#deleting]

```bash
sctl backup delete "$BACKUP_ID"
```

<Callout type="error">
  **Refused while the backup still has artifacts.** A definition cannot be removed out from
  under the things it produced. Delete or expire the artifacts first, and note that a locked
  artifact blocks this until its lock lapses.
</Callout>

Pause is almost always what you want instead.

## Next [#next]

<Cards>
  <Card href="/docs/cli/apply" title="apply" description="The declarative equivalent of this page." />

  <Card href="/docs/backups" title="Backups in depth" description="What each field means." />

  <Card href="/docs/cli/runs" title="Runs" description="Watching what a backup produced." />
</Cards>
