Document all full-backup capture methods in the operator guide.
test / test (push) Waiting to run

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-18 23:13:26 +03:00
parent 33ee08f7b6
commit a8ded7ecc8
2 changed files with 104 additions and 26 deletions
+97 -26
View File
@@ -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 <container> 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`).