From a8ded7ecc8c6eecbcb4321e2a2c3d43671fd97c9 Mon Sep 17 00:00:00 2001 From: Mikhail Yenuchenko Date: Tue, 18 Aug 2026 23:13:26 +0300 Subject: [PATCH] Document all full-backup capture methods in the operator guide. Co-authored-by: Cursor --- CHANGELOG.md | 7 +++ docs/guide.md | 123 +++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 104 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11804bd..f03aaad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version ## [Unreleased] +### Changed + +- [guide.md](docs/guide.md): full-backup section documents all four capture + methods (panel, `selfpost-backup`, stopped `tar` of the project directory, + stopped `tar` of `./data` only), warns against `tar` on a live container, and + compares downtime, archive contents, and restore behaviour in a table. + ## [1.8.0] - 2026-08-18 Built-in operator documentation in the panel: a CSS-only help drawer and Help diff --git a/docs/guide.md b/docs/guide.md index 9e0b4ec..17f18c7 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -31,6 +31,8 @@ domains hosted on that instance — DNS, deliveries, rate limits, applications). - [Server-level DNS (PTR/rDNS)](#server-level-dns-ptrrdns) - [Rate limiting — level 1 (IP backstop)](#rate-limiting--level-1-ip-backstop) - [Full backup and restore](#full-backup-and-restore) + - [Taking a full backup](#taking-a-full-backup) + - [Restore](#restore) - [Encrypting a backup or export](#encrypting-a-backup-or-export) - [Inbound relay](#inbound-relay) - [DMARC reports](#dmarc-reports) @@ -459,26 +461,106 @@ logs under `data/log/` are excluded. The base compose file mounts the project directory read-only at `/selfpost-deploy` so the panel and CLI can read those deploy files — without that mount, *Full backup* refuses with an error. -Take a backup from the panel (*Backup* → *Full backup*) or from the host: +#### Taking a full backup + +There are four ways to capture a full backup. They differ in whether the +instance keeps running, what goes into the archive, and what happens on restore. + +**1. Panel — while the container is running (no downtime).** *Backup* → *Full +backup* (optionally tick *Encrypt with a password*). Same archive as the CLI +below. + +**2. `selfpost-backup` CLI — while the container is running (no downtime).** +The panel button and this command call the same code path: a consistent SQLite +snapshot via `VACUUM INTO`, then a gzip tar of `data/` (minus diagnostics) plus +deploy files. ```sh docker exec selfpost-backup > selfpost-backup.tar.gz ``` -**Restore** means unpacking that archive into an **empty project directory** -(not into `./data` alone) and starting a container of the **exact same image -version** that created it — SelfPost refuses to start otherwise and tells you -which tag to use. On the first successful start after restore, `data/manifest.json` -from the archive is **deleted** — it guards only that one boot, so a later -in-place upgrade is not blocked. On that same first boot the panel also runs -one **Resync** — OpenDKIM's tables and Postfix's sender map are re-derived from -SQLite (and inbound relay maps when `INBOUND_RELAY_ENABLE=true`) and both -daemons are reloaded, healing any drift between the extracted files and the -database (the Status page's *Reload configuration* button runs the same step on -demand). This is why the compose file pins a fixed tag rather -than `:latest`: without a known version, there'd be no way to tell which image -restoring a given backup actually requires (see [Fixed image -tag](#fixed-image-tag)). +Use `-o /path/inside/container` to write inside the container instead of +stdout. Optional encryption: set `SELFPOST_BACKUP_PASSWORD` or pass +`-password-file` — see [Encrypting a backup or +export](#encrypting-a-backup-or-export). + +**3. `tar` of the whole project directory — with the service stopped.** Stop +the instance so nothing is writing to SQLite, then archive the operator +directory (the folder that contains `docker-compose.yml`, `.env`, `data/`, and +usually `certs/`): + +```sh +docker compose down +tar czf ../selfpost-backup.tar.gz . +docker compose up -d +``` + +Postfix delivery logs under `data/log/` are included and are often the bulk of +the archive; add `--exclude='./data/log'` if you only want state. Unlike the +panel/CLI backup, this archive has no `data/manifest.json`, so restore does not +run the version guard or the one-time post-restore *Resync* (see +[Restore](#restore) below) — a normal `docker compose up` is enough when the +files were captured cleanly while stopped. + +**4. `tar` of `./data` only — with the service stopped.** Same as (3), but only +the data volume: + +```sh +docker compose down +tar czf ../selfpost-data.tar.gz ./data +docker compose up -d +``` + +This is **not** self-contained: you must keep `docker-compose.yml`, `.env`, +and `certs/` separately (another backup, or unchanged on the same host). +Restore is `tar xzf selfpost-data.tar.gz` into an existing project directory, +not into an empty one. Also includes `data/log/` unless you exclude it. + +**Do not `tar` while the container is running.** SQLite uses WAL mode; copying +or archiving `./data` (or the whole project directory) while the panel and +Postfix are writing can produce an inconsistent database. The panel and CLI +backup avoid this by snapshotting the database on a live instance; the `tar` +methods avoid it by stopping first. + +| | Panel / `selfpost-backup` | `tar` project dir (stopped) | `tar` `./data` only (stopped) | +|--|---------------------------|------------------------------|-------------------------------| +| Downtime | None | Yes (`docker compose down`) | Yes | +| Self-contained archive | Yes | Yes | No — deploy files separate | +| SQLite consistency | `VACUUM INTO` snapshot | Safe when stopped | Safe when stopped | +| `data/log/` | Excluded | Included (optional exclude) | Included (optional exclude) | +| `data/manifest.json` | Written (version stamp) | Absent | Absent | +| Restore version check | Yes — must match image tag | No | No | +| Post-restore *Resync* | Yes, on first boot | No | No | +| Password encryption (`.spbk`) | Yes | No — encrypt the `.tar.gz` yourself if needed | No | + +Prefer the panel or CLI when you cannot afford downtime. Prefer a stopped `tar` +of the whole project directory when you already plan maintenance and want a +plain archive without going through the container. Use `tar` of `./data` only +when deploy files are managed elsewhere and never change. + +#### Restore + +**Panel and CLI archives** (with `data/manifest.json`) restore by unpacking into +an **empty project directory** (not into `./data` alone) and starting a +container of the **exact same image version** that created the backup — SelfPost +refuses to start otherwise and tells you which tag to use. On the first +successful start after restore, `data/manifest.json` from the archive is +**deleted** — it guards only that one boot, so a later in-place upgrade is not +blocked. On that same first boot the panel also runs one **Resync** — OpenDKIM's +tables and Postfix's sender map are re-derived from SQLite (and inbound relay +maps when `INBOUND_RELAY_ENABLE=true`) and both daemons are reloaded, healing +any drift between the extracted files and the database (the Status page's +*Reload configuration* button runs the same step on demand). This is why the +compose file pins a fixed tag rather than `:latest`: without a known version, +there'd be no way to tell which image restoring a given backup actually requires +(see [Fixed image tag](#fixed-image-tag)). + +**Stopped-`tar` archives** (methods 3 and 4 above) have no `manifest.json`, so +there is no version guard and no automatic post-restore *Resync*. Unpack a +self-contained project archive into an empty directory (or replace `./data` in +place for a data-only archive) and `docker compose up -d`. Use an image tag +compatible with the data on disk; when in doubt, match the tag that was running +when the archive was taken. **Restoring in place** (same host — recovering from data loss, or rolling back after a bad change): @@ -548,17 +630,6 @@ on `/settings` deletes your other sessions, but a global administrator resetting another user's password on `/users` does not invalidate that user's existing sessions. -**Alternative: archive `./data` while stopped.** If the service can be taken -offline, `docker compose down` then `tar czf selfpost-data.tar.gz ./data` on -the host is safe — nothing is writing to SQLite. Unlike the panel/CLI backup -this sweeps in `./data/log/` too, which is Postfix's raw log and usually the -bulk of the archive; add `--exclude=./data/log` if you only want the state. -Do **not** tar `./data` while the container is running: the database uses -WAL mode and a naive copy can capture an inconsistent snapshot. The -panel/CLI backup remains preferable when you cannot afford downtime because -it takes a consistent SQLite snapshot via the Backup API on a live -container. - See also [Exporting and importing a single domain](#exporting-and-importing-a-single-domain) — a different, domain-scoped operation that also lives on the *Backup* page (`/backup`).