# 備份、還原與升級

本教學備份哨站
的設定、登入憑據、裝置授權、監聽進度和通知佇列。郵件原文儲存在信箱服務商，本教學備份的是
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
)
```

每條命令成功後再繼續，按上面的驗證清單驗收。保留原容器與原資料卷，執行恢復例項時讓原例項保持停止。
