Skip to content
Talk to our solutions team

Restore from a backup

A restore you have performed before, into a scratch target, with a check that the data arrived — and the same flow able to restore for real behind an explicit flag.

When you finish you will know your backups work, which is a different claim from knowing your backup job succeeds.

This is the guide people skip, and the omission is the whole risk.

Without thisWith this
Backups are a hypothesis nobody has testedA rehearsal you have run and timed
The first restore happens during an incident, from memoryThe steps are written down and were followed last month
”How long does a restore take?” is a guessYou have measured it, so your RTO is a number
An archive that restores into a corrupt state looks fine on the shelfThe verify step reads the data back
The restore command is typed under pressureIt is a flow, run with one flag

A backup job proves a file was written. Only a restore proves the file is worth having. Run this on a schedule against scratch, not just when something breaks.

You needWhy
An archive from Back up a serviceThe input
A scratch targetSomewhere a mistake costs nothing
The service’s restore toolMatching the tool that produced the archive

Step 1 — make the destructive path opt-in

Section titled “Step 1 — make the destructive path opt-in”

The default must be the safe one. A flow that restores over production unless told otherwise is a loaded weapon.

name: restore-service
vars:
host: scratch.example.com
user: service
keypath: /path/to/key
archive_store: /mnt/backups
archive: ""
target: scratch
confirm: "no"

target: scratch and confirm: "no" are the defaults. Restoring for real requires saying so.

tasks:
- name: select
ssh:
host: "{{host}}:22"
username: "{{user}}"
privatekeypath: "{{keypath}}"
commands: |
set -e
if [ -z "{{archive}}" ]; then
echo "available archives:"
ls -1t {{archive_store}}/backup-*.tar.gz | head -10
echo "ERROR: pass -v archive=<filename>"
exit 1
fi
test -f {{archive_store}}/{{archive}}
echo "selected {{archive}}"

Refusing to guess is deliberate. “Restore the latest” is right until the latest is the one that captured the corruption.

Step 3 — verify the archive before touching anything

Section titled “Step 3 — verify the archive before touching anything”
- name: verify-archive
ssh:
host: "{{host}}:22"
username: "{{user}}"
privatekeypath: "{{keypath}}"
commands: |
set -e
tar -tzf {{archive_store}}/{{archive}} > /dev/null
echo "archive opens cleanly"
- name: guard
check: |
"{{target}}" !== "scratch" && "{{confirm}}" !== "yes"
shell: |
echo "REFUSING: target={{target}} requires -v confirm=yes"
exit 1
- name: restore
ssh:
host: "{{host}}:22"
username: "{{user}}"
privatekeypath: "{{keypath}}"
commands: |
set -e
echo "restoring {{archive}} into {{target}}"
<service-restore-command> --input {{archive_store}}/{{archive}} --target {{target}}

The step that separates a restore from a hopeful copy.

- name: verify-restore
ssh:
host: "{{host}}:22"
username: "{{user}}"
privatekeypath: "{{keypath}}"
commands: |
set -e
<service-query-command> --target {{target}} --count
echo "RESTORE VERIFIED"

Count something you can predict. A restore that produces an empty but valid database passes every check except this one.

name: restore-service
list: true
vars:
host: scratch.example.com
user: service
keypath: /path/to/key
archive_store: /mnt/backups
archive: ""
target: scratch
confirm: "no"
tasks:
- name: select
ssh:
host: "{{host}}:22"
username: "{{user}}"
privatekeypath: "{{keypath}}"
commands: |
set -e
if [ -z "{{archive}}" ]; then
echo "available archives:"
ls -1t {{archive_store}}/backup-*.tar.gz | head -10
echo "ERROR: pass -v archive=<filename>"
exit 1
fi
test -f {{archive_store}}/{{archive}}
echo "selected {{archive}}"
- name: verify-archive
ssh:
host: "{{host}}:22"
username: "{{user}}"
privatekeypath: "{{keypath}}"
commands: |
set -e
tar -tzf {{archive_store}}/{{archive}} > /dev/null
echo "archive opens cleanly"
- name: guard
check: |
"{{target}}" !== "scratch" && "{{confirm}}" !== "yes"
shell: |
echo "REFUSING: target={{target}} requires -v confirm=yes"
exit 1
- name: restore
ssh:
host: "{{host}}:22"
username: "{{user}}"
privatekeypath: "{{keypath}}"
commands: |
set -e
echo "restoring {{archive}} into {{target}}"
<service-restore-command> --input {{archive_store}}/{{archive}} --target {{target}}
- name: verify-restore
ssh:
host: "{{host}}:22"
username: "{{user}}"
privatekeypath: "{{keypath}}"
commands: |
set -e
<service-query-command> --target {{target}} --count
echo "RESTORE VERIFIED"

Rehearse with either; run the real thing as a flow. A restore is the operation you least want to repeat from the beginning, and the flow is the one that can tell you the extract succeeded and the load did not.

Rehearse into scratch:

Terminal window
kis flow -f restore-service.yaml -v archive=backup-20260731T020000Z.tar.gz

Restore for real, deliberately:

Terminal window
kis flow -f restore-service.yaml \
-v archive=backup-20260731T020000Z.tar.gz \
-v target=production -v confirm=yes

The flow verifies itself, twice — once that the archive opens, once that the restored data is readable. Time the run and record it: that number is your recovery time, and it is worth knowing before someone asks.

ChangeWhere
Restore to a point in timeAdd a timestamp argument to the restore command
Rehearse monthlySchedule it against scratch — see Control tasks
Stronger verificationCompare a row count or checksum against the source