docs/recovery/data-recovery.md
Rama main · réplica de lectura del repositorio original.
# Data recovery
> __A backup you have never restored is not a backup. It is a file you hope is a backup.__
>
> Test __recovery__, not backup. A green "Backup complete" proves a file was written. It proves nothing about whether that file can bring your records back, on a machine that no longer has anything on it, on the day you actually need it.
This page is about the drill. For how the buttons work see [`README.md`](README.md); for what a backup contains and why, see [`backup-model.md`](backup-model.md).
## Why this page exists
Every failure mode below has happened to somebody, and none of them is visible from the backup side:
| What the backup side showed | What was actually true |
|---|---|
| "Backup complete", nightly, for months | The destination was a NAS mount that unmounted in March. Files were written to the empty mountpoint on the local disk, which filled. |
| A folder full of `.tar.gz`, correct sizes | The archive held the database and nothing else, so restoring returned records with no operator contact, no schedule, no theme. |
| Backups running, retention pruning | At-rest encryption was on, so backup and restore were __both__ refused, and nothing had run since the day it was enabled. |
| A backup taken every night | Nobody had ever run a restore, so nobody knew the restore step needed a restart, and the first attempt was made during an outage. |
Every one is discovered in seconds by a restore drill, and never by looking at the backup.
## What is already tested, and what is not
Be clear about the boundary, because "it has tests" is often heard as "it is proven".
__Tested in CI, on every commit:__
- a backup archive contains the data root, and excludes the cache and the backup destination
- the database is captured with `VACUUM INTO`, never a raw copy of a live file
- restore accepts both the current `*.tar.gz` and legacy `*.db.gz`, detected by content rather than filename
- restoring into a __fresh, empty instance__ returns records *and* operator contact, while the signing key is not restored
- a backup destination that does not exist, is read-only, or is a file is refused with the real OS error
__Not tested, and not testable by us:__
- that __your__ destination is still writable this week
- that __your__ NAS is still mounted, and that it is a real mount rather than an empty directory
- that your archives are not silently truncated by a disk that filled three weeks ago
- that __you__ can perform a restore under pressure, having done it before
- that the machine you would restore onto still exists, and that you can build or pull the image on it
CI proves the code does what it claims. Only a drill proves your instance can recover.
## The drill
Do this on a __separate__ instance. Never practise on the one holding your records — a restore replaces the database, and a drill that damages live data has inverted the point of the exercise.
### 1. Take a backup, and copy it somewhere else
Admin → Database → __Backup now__. Then copy the resulting `*.tar.gz` off the machine — to a laptop, another disk, anywhere that would survive the original host being destroyed.
A backup that only exists on the machine it is protecting is not protecting it.
### 2. Bring up an empty instance
```bash
docker run -p 8081:8080 -v "$(pwd)/recovery-drill:/opt/fasten/db" \
-e YOURPHR_DATABASE_ENCRYPTION_ENABLED=false \
ghcr.io/jwilleke/yourphr:latest
```
Fresh volume, different port, nothing in it. Complete the first-run setup so you can sign in.
### 3. Restore into it
Put the archive in the new instance's backup destination, then Admin → Database → __Restore__. The restore is *staged* and applied on the next start, so restart the container.
That restart is not a detail. Find out about it now, not while your real instance is down.
### 4. Check what came back
This is the part that matters, and the part usually skipped. Do not accept "it started" as success.
- [ ] You can sign in — with a __new__ password if this is a fresh instance, since accounts came from the backup
- [ ] Record counts look right: conditions, medications, observations, documents
- [ ] Open an actual record and read it. A database can restore intact and still be the wrong database.
- [ ] The __operator contact__ and theme are yours, not blank — this is the difference between a backup and a *whole-instance* backup
- [ ] The backup schedule came back
- [ ] Connected sources are listed
### 5. Write down what happened
Date, version restored from, version restored to, how long it took, and anything that surprised you. In an outage you will not be inventing the procedure — you will be following the note you wrote when nothing was on fire.
## Things that will make recovery fail
__At-rest encryption is on.__ While `database.encryption.enabled` is true, backup and restore are both __refused__ ([#367](https://github.com/jwilleke/yourphr/issues/367)). It defaults to on. An instance that has never had a successful backup will look calm and have nothing. Check Admin → Database, and if you want backups, set `YOURPHR_DATABASE_ENCRYPTION_ENABLED=false` explicitly.
__The destination is not what you think.__ A path that looks like a NAS mount is a plain empty directory when the mount is gone, and writes to it succeed. The __Test path__ button writes, fsyncs, reads back and removes — the fsync is what catches a full disk or a dropped mount, which a plain write hides in the page cache.
__You have only one copy, in one place.__ Ransomware encrypts every writable path it can reach, including the backup folder. So does a failing disk. At least one copy should be somewhere the instance cannot write to.
__Nobody knows the encryption key.__ If you enabled at-rest encryption, the database is unopenable without that key, and a key stored only on the machine it protects is not stored.
## What your backups contain
Stated plainly, because it changes where you may put them:
- every user's complete medical records
- __live provider credentials__ — OAuth access tokens, refresh tokens and `client_secret` per connected source. A refresh token grants __ongoing__ access to that patient's records at Epic or CMS until revoked. Not stale artifacts.
- the generated session signing key
- your instance configuration
And, because encryption and backup are mutually exclusive today, __every backup that exists is plaintext__. Treat the file exactly as you would treat the database. Encrypting the artifact itself is [#461](https://github.com/jwilleke/yourphr/issues/461).
## How often
- __Restore drill:__ at least twice a year, and after any upgrade that touches backup or restore. This release ([v2.1.0](https://github.com/jwilleke/yourphr/releases/tag/v2.1.0)) changed the archive format and what restore replaces, which makes it a good moment for one.
- __Test path:__ whenever the destination changes, and after anything that could disturb a mount. Enabling a schedule now runs this automatically and refuses if it fails.
- __Glance at the backup list:__ monthly. Sizes that stop growing, or a newest file older than the schedule, are the two cheapest signals of a silent failure.
## See also
- [`README.md`](README.md) — the buttons, endpoints and configuration
- [`backup-model.md`](backup-model.md) — what a backup contains and why
- [`../deployment/README.md`](../deployment/README.md) — encryption, and the trade it forces