# 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/ ``` 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.