本次實測環境
| 虛擬化平台 | Proxmox VE 9.2.18,新建測試 VM |
| CPU | 2 vCPU(x86-64-v2-AES) |
| 記憶體 | 4 GiB RAM |
| 虛擬磁碟 | 32 GiB,HDD-backed、VirtIO SCSI、ext4 |
| 作業系統 | Ubuntu 24.04.5 LTS,amd64 |
| PostgreSQL | 16.15,Ubuntu 原生套件 |
| 驗證日期 | 2026-10-02 |
本文使用新建的 Ubuntu 測試主機實際執行。這是本次實測配置,並非最低硬體需求;以少量範例資料確認功能,不作為效能或大規模負載測試結果。
文章目錄
實測結果:備份腳本、SHA-256、互斥鎖、保留檔案預覽、隔離還原與重開機驗證均通過;損壞的備份會被拒絕。timer 已實際觸發備份,測試時加速觸發後恢復每日排程,沒有等待隔夜排程。本文尚未實測異地傳送、PITR 或大量資料負載。
上一篇完成了 Ubuntu 24.04 安裝 PostgreSQL 16,這一篇把備份做成每天能執行、出錯能追查、需要時能還原的流程。沿用範例資料庫 appdb 與應用帳號 app_user,採原生工具與 systemd,不使用 Docker。
本文範例適用於本機 PostgreSQL 16、Unix socket 目錄 /var/run/postgresql、連接埠5432,且 Linux 的 postgres 帳號可使用 peer 驗證。若你的版本、叢集或連接埠不同,請先對照 pg_lsclusters;以下路徑都是教學範例。
終端畫面說明:下方截圖來自測試主機實際執行的 PTY 紀錄,使用瀏覽器終端樣式呈現;提示符統一為 lab$,不顯示內網主機識別。echo $? 的0表示上一個指令成功。畫面沒有改寫執行結果,文字指令仍保留供複製。
一、先決定要能救回什麼
先寫下兩個目標:最多能接受遺失多久的資料,以及多久內必須恢復服務。每天一次的備份,在每次都成功的前提下,可能遺失接近一天的變更;若失敗沒人發現,缺口會更大。還原需要的時間,則要靠實際演練量測。
這篇使用 pg_dump 的 custom 格式,保存一個資料庫的結構與資料。它能在資料庫仍有讀寫時取得一致的備份,但大型資料庫仍會增加磁碟與 CPU 負擔,應避開尖峰與結構異動。這種備份沒有涵蓋整台主機、應用程式上傳檔案或所有伺服器設定,工具範圍可查 pg_dump 官方文件。
二、準備只供備份使用的目錄
使用有 sudo 權限的一般帳號執行。若這個目錄已存在,先確認用途與現有檔案,不要直接套用到共用目錄。此範例讓 postgres 帳號寫入自己的備份,其他一般帳號無法讀取。
pg_lsclusters
sudo install -d -o postgres -g postgres -m 0700 /var/backups/postgresql/appdb
sudo -u postgres /usr/lib/postgresql/16/bin/psql \
-h /var/run/postgresql -p 5432 -d appdb \
-c "SELECT current_database(), current_user;"
df -h /var/backups/postgresql/appdb
請先確認連線成功、資料庫名稱正確、磁碟有足夠空間。本篇透過本機 peer 驗證,不把密碼放進腳本。若改成跨主機備份,還需要另行規劃 TLS、備份角色與憑證保管。
三、建立每天可重複執行的備份腳本
用 sudoedit /usr/local/sbin/backup-appdb 建立下列檔案;若已有同名腳本,先保存原檔並確認用途。腳本先寫入暫存檔,成功檢查後才改成正式名稱,避免把失敗的半成品當作完成的備份。
#!/bin/bash
set -euo pipefail
umask 077
backup_dir=/var/backups/postgresql/appdb
pg_bin=/usr/lib/postgresql/16/bin
cd "$backup_dir"
exec 9>.backup.lock
flock -n 9 || { echo "Another backup is running" >&2; exit 1; }
stamp=$(date -u +%Y%m%dT%H%M%SZ)
name="appdb-${stamp}.dump"
temp_file=$(mktemp .appdb-XXXXXX.partial)
trap 'rm -f -- "$temp_file"' EXIT
"$pg_bin/pg_dump" -h /var/run/postgresql -p 5432 \
-U postgres -d appdb -Fc -f "$temp_file"
test -s "$temp_file"
"$pg_bin/pg_restore" --list "$temp_file" > /dev/null
mv -n -- "$temp_file" "$name"
test ! -e "$temp_file"
sha256sum "$name" > "$name.sha256"
sha256sum --check "$name.sha256"
printf 'Backup completed: %s\n' "$name"
set -euo pipefail 讓錯誤停止流程;flock 防止本腳本同時執行;umask 077 限制新檔權限。正式檔名使用 UTC 時間。最後的 SHA-256 用於確認檔案複製前後一致,不能證明資料庫一定能還原;pg_restore --list 也只是檢查備份目錄可讀。
sudo chown root:postgres /usr/local/sbin/backup-appdb
sudo chmod 0750 /usr/local/sbin/backup-appdb
sudo bash -n /usr/local/sbin/backup-appdb
sudo -u postgres /usr/local/sbin/backup-appdb
sudo -u postgres ls -lh /var/backups/postgresql/appdb
先手動跑通再排程。備份檔雖有權限限制,仍是未加密的資料;postgres 帳號也能修改這個目錄,因此它只適合作為本機備份起點,還需要獨立保存的副本。

四、交給 systemd 每天執行
用 sudoedit /etc/systemd/system/appdb-backup.service 建立服務。服務以 postgres 執行,腳本本身由 root 管理;不要把 postgres 設成腳本擁有者。
[Unit]
Description=Logical backup of appdb
After=postgresql@16-main.service
Requires=postgresql@16-main.service
[Service]
Type=oneshot
User=postgres
Group=postgres
UMask=0077
ExecStart=/usr/local/sbin/backup-appdb
TimeoutStartSec=2h
再用 sudoedit /etc/systemd/system/appdb-backup.timer 建立排程。以下指定台北時間每天03:15,最多再延遲5分鐘,避免多個工作同時搶磁碟。服務的兩小時上限需要依實際資料量調整。
[Unit]
Description=Daily appdb backup
[Timer]
OnCalendar=*-*-* 03:15:00 Asia/Taipei
RandomizedDelaySec=5m
Persistent=true
Unit=appdb-backup.service
[Install]
WantedBy=timers.target
Persistent=true 會在排程恢復啟用時補一次錯過的日曆觸發;不會把停機期間每一天的資料補成歷史備份。它也沒有提供外部通知,排程語意可查 systemd 255 的 timer 文件。
sudo systemd-analyze verify /etc/systemd/system/appdb-backup.service \
/etc/systemd/system/appdb-backup.timer
systemd-analyze calendar '*-*-* 03:15:00 Asia/Taipei'
sudo systemctl daemon-reload
sudo systemctl start appdb-backup.service
sudo systemctl enable --now appdb-backup.timer
systemctl list-timers appdb-backup.timer --all
五、確認完成,而不只看排程已啟用
systemctl show appdb-backup.service -p Result -p ExecMainStatus
sudo journalctl -u appdb-backup.service -n 50 --no-pager
sudo -u postgres ls -lh /var/backups/postgresql/appdb
df -h /var/backups/postgresql/appdb
成功的一次應有 Result=success、ExecMainStatus=0,日誌包含完成訊息,且有相配的 .dump 與 .sha256。oneshot 服務做完後顯示 inactive 是正常的;若還在執行,狀態欄位可能尚未代表本次完成結果。
正式使用時,應監控「最後成功時間是否超過預期」、「磁碟剩餘容量」與「服務是否失敗」,並配置你會收到的通知管道。本文沒有替讀者設定通知,也沒有自動清除舊備份。
六、還原到另一個資料庫,驗證可用
從清單選定一份備份,把下方的範例檔名換成實際名稱。確認 appdb_restore_check 尚未存在;還原目標使用新資料庫,保留正式的 appdb。此練習只適用於前文由 app_user 擁有物件的簡單 appdb,沿用已存在的角色與擴充套件環境。
backup_dir=/var/backups/postgresql/appdb
name=appdb-20261002T031500Z.dump
backup_file="$backup_dir/$name"
sudo -u postgres bash -c 'cd "$1" && sha256sum --check "$2.sha256"' \
bash "$backup_dir" "$name"
sudo -u postgres /usr/lib/postgresql/16/bin/createdb \
-h /var/run/postgresql -p 5432 --owner=app_user appdb_restore_check
sudo -u postgres /usr/lib/postgresql/16/bin/pg_restore \
-h /var/run/postgresql -p 5432 \
--exit-on-error --single-transaction \
--no-owner --role=app_user \
--dbname=appdb_restore_check "$backup_file"
sudo -u postgres /usr/lib/postgresql/16/bin/psql \
-h /var/run/postgresql -p 5432 -d appdb_restore_check \
-c "SELECT id, note FROM public.install_check;"
--single-transaction 將這次還原包在一個交易中,錯誤時不留下部分還原的物件;資料庫本身仍會存在。--no-owner --role=app_user 讓本例還原物件由 app_user 建立,並非複製原有多角色擁有者配置,選項細節見 pg_restore 官方文件。
若沿用上一篇測試資料,應能讀到 install_check 的文字。接著讓測試程式連到這個新資料庫,確認重要查詢、資料量、索引及應用功能。持續變動的正式資料不能直接與昨晚備份逐筆比較;應依備份時間與業務規則確認。記錄本次用的檔名、還原耗時、檢查結果,才有可追查的演練紀錄。

七、保留多個版本,另存獨立副本
小型服務可先採「每日保留14份、每週另存一份保留8週」,再依資料量與可接受的回溯範圍調整。週備份應另外分類,不能讓每日清理規則一併移除。資料被誤改數天後才發現時,只有最新一份往往不夠。
下方只列出已滿14個24小時的每日 dump 候選檔案,不刪除。實際清理前,先確認較新備份成功、獨立副本可讀,並把對應的 checksum 檔案當作一組管理。空間不足或連續失敗時,應先排查原因,避免清掉唯一可還原的一份。
sudo -u postgres sh -c '
cd /var/backups/postgresql/appdb &&
find . -maxdepth 1 -type f -name "appdb-*.dump" -mtime +13 -print
'
這裡先切換到 postgres 可進入的備份目錄,再執行 find。若直接從只有一般帳號可讀的家目錄啟動 find,部分環境會在結束時出現「Failed to restore initial working directory」;上面的寫法已在私有家目錄環境實測。
同一台主機、同一顆磁碟上的副本,遇到磁碟故障仍可能一起消失。可依需求另存另一台設備、離線磁碟或異地儲存;備份含資料內容,傳輸要加密,遠端權限盡量只允許新增,並使用版本保留或不可變保存。這份教學尚未包含自動傳送與加密程式,採用前需要把副本保存流程一併完成。
八、別漏掉角色、設定與網站檔案
單一資料庫 dump 不保存全伺服器角色與 tablespace 定義。若要搬到全新主機,可另外保存共用物件;以下在一般帳號自己的私有目錄執行,刻意不輸出角色密碼雜湊。
umask 077
globals_file="$PWD/postgresql-globals-$(date -u +%Y%m%dT%H%M%SZ).sql"
sudo -u postgres /usr/lib/postgresql/16/bin/pg_dumpall \
-h /var/run/postgresql -p 5432 \
--globals-only --no-role-passwords > "$globals_file"
新環境套用前要先檢視角色與 tablespace 定義,對照既有名稱、路徑及權限,不要直接把整份 SQL 套回正式叢集。因為沒有保存密碼,還原角色後必須重新設定需要密碼登入的角色,例如以管理身分執行 \password app_user;相關限制見 pg_dumpall 官方文件。
此外還要保存並驗證 PostgreSQL 設定、擴充套件需求、應用程式設定與上傳檔案。資料庫與檔案若互相引用,需要設計相配的備份時間,例如短暫停止應用寫入;只對其中一邊備份,可能留下不存在的檔案連結。
需要精確回到某個時間,再規劃 PITR
每天一次的 dump 適合這份入門流程;若要救回備份之後的變更,或還原到誤刪前的某一刻,需要另行設計實體基礎備份、連續 WAL 歸檔與復原演練。只有 dump 或只有 WAL 檔案都不構成完整 PITR 流程,可從 PostgreSQL 的連續歸檔與 PITR 文件 開始。
先讓第一份備份成功、還原成功,再把排程、通知、保留與獨立副本補齊。每次調整資料結構、擴充套件或版本後,再做一次還原演練,才能確認備份仍符合現在的服務。