# Backup and restore

Mangalord persists state in two places:

1. **Postgres** (`postgres-data` volume) — users, mangas, chapters, pages, bookmarks, sessions, tokens.
2. **Storage** (`storage-data` volume) — cover and page image bytes. Keys in the DB reference paths inside this volume; a backup that has the DB without the storage (or vice versa) is incomplete.

Always back both up together, and restore them together.

## Backing up

Run while the stack is up. The compose service name is `postgres`; if you renamed it, substitute below.

```bash
# 1. Dump Postgres. `--format=custom` produces a single compressed file that
#    pg_restore can stream.
docker compose exec -T postgres pg_dump \
    -U "${POSTGRES_USER:-mangalord}" \
    -d "${POSTGRES_DB:-mangalord}" \
    --format=custom \
    > mangalord-$(date -u +%Y%m%dT%H%M%SZ).dump

# 2. Tar the storage volume. The simplest way is to mount the named volume
#    into an ephemeral container and tar from there.
docker run --rm \
    -v mangalord_storage-data:/source:ro \
    -v "$PWD":/backup \
    alpine \
    tar czf "/backup/mangalord-storage-$(date -u +%Y%m%dT%H%M%SZ).tar.gz" -C /source .
```

Both files are timestamped so consecutive runs don't overwrite. Copy them off-host (rsync, rclone, your object store of choice) — keeping them on the same machine doesn't help if the disk fails.

> The volume's prefix (`mangalord_`) comes from the compose project name, which defaults to the directory name. Confirm with `docker volume ls | grep storage-data`.

## Restoring

The procedure is destructive; do it on a fresh stack or after confirming you really want to overwrite the current state.

```bash
# 0. Stop the stack so nothing writes during the restore.
docker compose down

# 1. Wipe the existing volumes and recreate them. (Skip this if restoring
#    onto a host that doesn't have prior state.)
docker volume rm mangalord_postgres-data mangalord_storage-data || true

# 2. Bring Postgres up alone so we can restore into it before the backend
#    starts and runs migrations against the wrong schema.
docker compose up -d postgres
# Wait for it to become healthy.
until docker compose exec -T postgres pg_isready -U "${POSTGRES_USER:-mangalord}" >/dev/null; do
    sleep 1
done

# 3. Restore the dump.
docker compose exec -T postgres pg_restore \
    -U "${POSTGRES_USER:-mangalord}" \
    -d "${POSTGRES_DB:-mangalord}" \
    --clean --if-exists \
    < mangalord-YYYYMMDDTHHMMSSZ.dump

# 4. Restore the storage tarball.
docker run --rm \
    -v mangalord_storage-data:/target \
    -v "$PWD":/backup \
    alpine \
    sh -c "cd /target && tar xzf /backup/mangalord-storage-YYYYMMDDTHHMMSSZ.tar.gz"

# 5. Start the rest of the stack.
docker compose up -d backend frontend
```

The backend re-runs migrations on startup. They're idempotent (additive), so restoring a dump from an older schema and then starting a newer backend will forward-migrate the data; the reverse is not supported.

## Restore drill

It's worth running this end-to-end at least once before you need it for real. A minimal smoke check after restore:

```bash
# Health endpoint should respond.
curl -sf http://localhost:8080/api/v1/health

# An expected manga should be visible.
curl -s 'http://localhost:8080/api/v1/mangas?limit=1' | jq '.items[0].title, .page.total'

# A known cover or page should stream back with the right Content-Type.
curl -sI http://localhost:8080/api/v1/files/<known-key>
```

If `.page.total` matches your expectation and a known file is reachable, the restore is complete.

## What's *not* in the backup

- The `.env` file — keep this in your password manager / secrets store; the backup deliberately doesn't include it so a leaked archive doesn't leak credentials.
- Application logs — stream them to your usual log sink (journald, Loki, whatever) rather than relying on container logs.
- Anything outside the two named volumes.
