---
title: "Restore"
description: "Download and decrypt an artifact on your machine, with your key."
url: "https://saved.sh/docs/cli/restore"
---

```bash
sctl restore postgres <artifact-id> --database scratch
```

Restore happens **entirely on your machine**. The artifact is downloaded through a presigned
URL, decrypted with your own private key, decompressed, and reconstructed according to its
source type. Neither the target nor its credentials are ever sent to us.

We are not in the path and could not be: we have no private key.

## One subcommand per target [#one-subcommand-per-target]

Restoring a folder is not the same operation as restoring a database, so the target type is
the subcommand and its flags are only the ones that type needs.

```bash
# Postgres: replayed into a database
sctl restore postgres <artifact-id> --host localhost --database scratch --user postgres

# MySQL: replayed with the mysql client
sctl restore mysql <artifact-id> --database scratch --user root

# Written to a path
sctl restore file <artifact-id> --path ./uploads.tar
sctl restore folder <artifact-id> --path ./restored.zip
```

The target is described with the **same field names its source type uses**, so a `source:`
block in a manifest and a restore command name things identically.

| Subcommand | Flags                                                                             |
| ---------- | --------------------------------------------------------------------------------- |
| `postgres` | `--host`, `--port`, `--database` (required), `--user`, `--password`, `--ssl-mode` |
| `mysql`    | `--host`, `--port`, `--database` (required), `--user`, `--password`, `--ssl-mode` |
| `file`     | `--path` (required)                                                               |
| `folder`   | `--path` (required)                                                               |

`file` and `folder` do the same thing: they write the artifact to `--path`. Neither unpacks
it, because extracting into a tree you have not looked at is not a backup tool's decision.

`--key` applies to all of them: a private key file, defaulting to your gpg keyring or agent.

The target is validated **before anything is downloaded**, so a missing `--database` costs
you nothing:

```
$ sctl restore postgres <artifact-id>
Error: postgres target: database is required
```

## What it does [#what-it-does]

```
download → decrypt → decompress → reconstruct
```

| Step        | Detail                                                                                          |
| ----------- | ----------------------------------------------------------------------------------------------- |
| Download    | Presigned URL, into a temporary directory that is removed on exit                               |
| Decrypt     | `gpg`, if the artifact is encrypted                                                             |
| Decompress  | `gunzip`, if the artifact is compressed. **Whether or not it was encrypted**                    |
| Reconstruct | `pg_restore` or `psql` for Postgres, `mysql` for MySQL; otherwise the file is moved to `--path` |

Compression is applied **before** encryption, so an artifact with both peels in that order and
neither step substitutes for the other. Doing this by hand is where that bites: stopping at
`gpg --decrypt` hands you a gzip stream, which `psql`, `mysql`, `tar` and the RDB reader all
reject. See [the four combinations](/docs/recover/artifact-format#the-four-combinations).

For Postgres it reads the first bytes of the dump to decide: `PGDMP` means custom format and
`pg_restore`, anything else means plain SQL and `psql`. You do not have to know which you
have.

## Using a key file [#using-a-key-file]

```bash
sctl restore file <artifact-id> --path ./uploads.tar --key ./prod-private.asc
```

The key is imported into a **temporary keyring that is deleted when the command exits**, so
nothing is left behind in your own keyring. That is the right choice on a machine you do not
own, such as a recovery box during an incident.

Without `--key`, your normal gpg keyring and agent are used, and a passphrase prompt appears
if the key has one.

## Restoring without the backup definition [#restoring-without-the-backup-definition]

```bash
sctl restore folder <artifact-id> --path ./data.tar
```

You name the target yourself, so nothing is looked up from the backup. That matters when the
definition has been deleted but the artifact has not: you need the artifact and your key, and
nothing else.

## Requirements [#requirements]

| Restoring             | Needs on your machine               |
| --------------------- | ----------------------------------- |
| An encrypted artifact | `gpg`, and the matching private key |
| A `postgres` artifact | `pg_restore` and `psql`             |
| A `mysql` artifact    | `mysql`                             |
| Anything else         | Nothing                             |

If a tool is missing the command says which one and stops, rather than failing partway
through. See [installing gpg](/docs/security/key-management#installing-gpg).

## Restore into scratch first [#restore-into-scratch-first]

Always, and especially during an incident.

```bash
createdb scratch
sctl restore postgres <artifact-id> --host localhost --database scratch
psql -d scratch -c 'SELECT max(created_at) FROM orders;'
```

That last query gives your **actual recovery point**, which is when the dump started rather
than when the run finished. It is the number to report to everyone else.

<Callout type="error">
  Restoring straight into a live database is destructive and irreversible. Restore to scratch,
  confirm the data is what you expect, then decide how to promote it. Most real recoveries want
  a few rows copied across, not a whole database replaced.
</Callout>

## Doing it without sctl [#doing-it-without-sctl]

The CLI is a convenience, not a dependency. Every step is a standard tool:

```bash
sctl artifact download <artifact-id> --output ./artifact
gpg --decrypt ./artifact > ./app.dump.gz
gunzip ./app.dump.gz
pg_restore --dbname scratch ./app.dump
```

Rehearse that path at least once, so you know the recovery does not depend on us shipping a
working binary. See [Recover](/docs/recover).

## Troubleshooting [#troubleshooting]

| Message                                                    | Cause                                                            |
| ---------------------------------------------------------- | ---------------------------------------------------------------- |
| `postgres target: database is required`                    | The target is checked before any download                        |
| `this artifact is encrypted and gpg was not found on PATH` | Install gpg                                                      |
| `gpg: decryption failed: No secret key`                    | The private key for this artifact's `Key` is not in your keyring |
| `restoring a postgres backup needs pg_restore on PATH`     | Install the Postgres client tools                                |
| `restoring a mysql backup needs mysql on PATH`             | Install the MySQL client                                         |
| `not_in_our_storage`                                       | Delivered only to your own bucket. Fetch it from there           |

## Next [#next]

<Cards>
  <Card href="/docs/recover" title="Recover" description="The same thing by hand, with no account." />

  <Card href="/docs/recover/rehearsal" title="Rehearsal" description="Practising before it matters." />

  <Card href="/docs/recover/restore-postgres" title="Restore Postgres" description="Selective restores and schema drift." />
</Cards>
