Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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
|
||||
|
||||
+97
-26
@@ -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`).
|
||||
|
||||
Reference in New Issue
Block a user