# 备份、恢复与升级

本教程备份哨站
的配置、登录凭据、设备授权、监听进度和通知队列。邮件原文保存在邮箱服务商，本教程备份的是
Mailwake 实例。

## 1. 备份哪些内容

一份可恢复的备份包含：

- `/data` 完整目录：`mailwake.db`、`secret.key`
  及 SQLite 的相关文件。数据库与密钥需要成套保存。
- Compose 配置：端口、数据卷和环境设置。
- 当前运行镜像：恢复时使用同一个版本，避免数据库与程序版本不匹配。

下面的脚本适用于标准 Compose 部署，会先保存配置与镜像，再短暂停止
哨站、复制完整数据并恢复运行。备份期间监听暂停，重启后按既有进度补查仍在文件夹内、接收时间在
24 小时内的新邮件。

部署使用反向代理配置或其他宿主机挂载文件时，将这些文件另行保存到同一备份目录。恢复到另一台机器时也要调整这些文件的路径。

## 2. 手动完成第一次备份

下载[备份脚本](/downloads/backup.sh)，保存为
`backup.sh`，放到服务器的安装目录，例如
`~/mailwake/backup.sh`。

查看备份脚本内容

```sh
#!/bin/sh
# Copyright (C) 2026 Mailwake contributors
# SPDX-License-Identifier: AGPL-3.0-only
# License: https://www.gnu.org/licenses/agpl-3.0.html
# Back up a running Compose installation; execute as a user with Docker access.
set -eu

if [ "$#" -ne 2 ]; then
  echo "Usage: backup.sh /absolute/compose-directory /absolute/backup-directory" >&2
  exit 2
fi
case "$1:$2" in
  /*:/*) ;;
  *) echo "Use absolute directory paths." >&2; exit 2 ;;
esac
cd "$1"
umask 077
lock=".mailwake-backup.lock"
if ! mkdir "$lock" 2>/dev/null; then
  echo "Could not create $PWD/$lock. Check directory permissions and active backups." >&2
  exit 1
fi
restart=0
cleanup() {
  result=$?
  trap - EXIT
  if [ "$restart" -eq 1 ]; then
    docker compose start core >&2 || result=1
  fi
  rmdir "$lock" || result=1
  exit "$result"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
container=$(docker compose ps -q core)
if [ -z "$container" ]; then
  echo "Start the Core service before running this backup." >&2
  exit 1
fi
mkdir -p "$2"
backup="$2/mailwake-$(date +%Y%m%d-%H%M%S)"
test ! -e "$backup"
staging="$backup.incomplete"
mkdir "$staging"
mkdir "$staging/data"
docker compose config > "$staging/compose.yaml"
docker inspect --format '{{.Image}}' "$container" > "$staging/image-id.txt"
docker image save --output "$staging/image.tar" "$(cat "$staging/image-id.txt")"
restart=1
docker compose stop core >&2
docker compose cp core:/data/. "$staging/data/" >&2
docker compose start core >&2
restart=0
mv "$staging" "$backup"
printf 'Backup saved: %s\n' "$backup"

```

在服务器终端执行：

```
sh "$HOME/mailwake/backup.sh" "$HOME/mailwake" "$HOME/mailwake-backups"
```

**成功标志**：终端输出
`Backup saved:` 和新目录路径，`docker compose ps`
显示哨站已重新运行。脚本在复制失败时也会尝试启动
哨站，并返回失败；以
`.incomplete` 结尾的目录表示备份未完成，保留它用于排查。

锁目录用于避免定时备份与手动备份同时执行。主机崩溃或进程被强制结束后，先确认备份进程已结束，再删除安装目录中的
`.mailwake-backup.lock` 并重试。

每份备份的结构如下：

```
mailwake-20261008-030000/
  compose.yaml
  image-id.txt
  image.tar
  data/
    mailwake.db
    secret.key
    ...
```

脚本保存镜像需要额外磁盘空间，耗时主要发生在停止哨站
之前。备份目录使用私有权限，里面含有可恢复邮箱凭据的数据库与密钥。查看新目录，确认
`data/mailwake.db`、`data/secret.key` 与
`image.tar` 均存在；随后至少完成一次下一节的恢复演练。

## 3. 恢复到新数据卷

先停止原实例，保留原数据卷。恢复实例与原实例使用同一套邮箱和设备身份，应只运行其中一个。

```
cd ~/mailwake
docker compose stop core
```

将下面的
`backup` 改为实际备份路径。新建一个恢复目录，把镜像和
Compose 配置准备好：

```
backup="$HOME/mailwake-backups/mailwake-20261008-030000"
mkdir -p ~/mailwake-restored
cd ~/mailwake-restored
cp "$backup/compose.yaml" ./compose.yaml
docker load --input "$backup/image.tar"
restore_volume="mailwake-restored-$(date +%Y%m%d-%H%M%S)"
cat > compose.override.yaml <<EOF
name: mailwake-restored
services:
  core:
    image: $(cat "$backup/image-id.txt")
volumes:
  core-data:
    name: $restore_volume
networks:
  default:
    name: $restore_volume-network
EOF
```

`compose.override.yaml` 会由 Compose
自动加载。它将恢复服务指向一个新的数据卷，并固定到备份时的镜像。下面先创建容器，然后复制数据、修正容器用户权限，最后启动：

```
(
  set -eu
  docker compose create --pull never core
  docker compose run --rm --no-deps -T --user 0:0 --entrypoint sh core \
    -c 'entries=$(ls -A /data) && test -z "$entries"'
  docker compose cp "$backup/data/." core:/data/
  docker compose run --rm --no-deps -T --user 0:0 --cap-add CHOWN --entrypoint chown core \
    -R 10001:10001 /data
  docker compose up -d --pull never
  docker compose logs --tail=50 core
)
```

每条命令成功后再继续。空卷检查失败时，换一个新的
`restore_volume`，重新写入覆盖文件并创建容器。恢复到已有内容的数据卷会混入旧文件；这里始终使用空卷。

**成功标志**：打开管理页面后，用原管理员密码登录，邮箱和文件夹配置仍在，监听恢复；设备授权也来自备份。若看到全新的初始化页，先停止恢复实例，检查复制路径和卷挂载。解密错误则检查
`secret.key` 是否与数据库来自同一备份。

## 4. 验证恢复结果

1. 确认邮箱数量、文件夹选择和检查方式与备份前一致。
2. 发送通道测试通知，确认手机收到。
3. 从另一个邮箱发一封新邮件到已监听的文件夹，确认出现新的投递记录。
4. 重启恢复实例，再次登录，确认配置和监听持续存在。

确认恢复实例正常后，将域名代理指向它，并继续在
`~/mailwake-restored`
管理这个实例；备份任务也改为这个目录。原实例保持停止，原卷和备份保留到你确认恢复完成。

备份中的待处理或失败通知会恢复到当时的状态，可能再次投递。升级回退也使用本流程，同时恢复旧数据和旧镜像。

## 5. 每天自动备份与异地保存

Linux 服务器可以复用同一个脚本。先手动执行成功，再用
`crontab -e` 添加以下配置，将
`/home/you` 替换为自己的绝对目录：

```
PATH=/usr/local/bin:/usr/bin:/bin
0 3 * * * /bin/sh /home/you/mailwake/backup.sh /home/you/mailwake /home/you/mailwake-backups >> /home/you/mailwake-backup.log 2>&1
```

任务按服务器本地时区每天 03:00 运行，执行用户需要有 Docker 权限。NAS
可在任务计划面板中填写相同的 Shell
命令。次日检查是否生成新目录，并查看日志中的
`Backup saved:`；失败记录需要处理，定时器本身不会判断备份可恢复。

同一块磁盘上的备份无法覆盖整机或磁盘损坏。将完成的备份目录复制到另一台机器或受保护的备份存储，例如通过
SSH：

```
scp -r "$HOME/mailwake-backups/mailwake-20261008-030000" \
  backup-user@backup-host:/srv/mailwake-backups/
```

替换源目录、远端用户、主机和目标目录，确保目标仅向备份维护者开放。需要加密静态文件时，使用加密备份存储或加密归档后再上传。可保留最近
7 天的每日备份和最近 4
周的每周备份；清理旧备份前，先确认异地副本和最近一次恢复演练成功。

## Docker Run 安装的备份

Docker Run 安装使用名为 `mailwake` 的容器和
`mailwake-data` 卷。下面这组命令在子 Shell
中执行，复制失败时也尝试恢复原容器运行：

```
(
  set -eu
  umask 077
  backup="$HOME/mailwake-backups/mailwake-$(date +%Y%m%d-%H%M%S)"
  mkdir -p "$backup/data"
  docker inspect --format '{{.Image}}' mailwake > "$backup/image-id.txt"
  docker image save --output "$backup/image.tar" "$(cat "$backup/image-id.txt")"
  trap 'docker start mailwake >/dev/null' EXIT
  docker stop mailwake
  docker cp mailwake:/data/. "$backup/data/"
  docker start mailwake
  trap - EXIT
  printf 'Backup saved: %s\n' "$backup"
)
```

将自己的 Docker Run 启动命令一并保存。恢复时先停止原容器，加载
`image.tar`，创建新数据卷和新容器，将备份复制进去后再启动：

```
(
  set -eu
  backup="$HOME/mailwake-backups/mailwake-20261008-030000"
  image=$(cat "$backup/image-id.txt")
  restore_volume="mailwake-restored-$(date +%Y%m%d-%H%M%S)"
  docker stop mailwake
  docker load --input "$backup/image.tar"
  docker volume create "$restore_volume"
  docker create --name mailwake-restored --restart unless-stopped \
    --stop-timeout 30 --read-only --security-opt no-new-privileges:true --cap-drop ALL \
    -p 127.0.0.1:8080:8080 -v "$restore_volume:/data" "$image"
  docker run --rm --user 0:0 --entrypoint sh -v "$restore_volume:/data" "$image" \
    -c 'entries=$(ls -A /data) && test -z "$entries"'
  docker cp "$backup/data/." mailwake-restored:/data/
  docker run --rm --user 0:0 --entrypoint chown -v "$restore_volume:/data" "$image" \
    -R 10001:10001 /data
  docker start mailwake-restored
)
```

每条命令成功后再继续，按上面的验证清单验收。保留原容器与原数据卷，运行恢复实例时让原实例保持停止。
