Sao lưu với Proxmox Backup Server

Tổng quan về Proxmox Backup Server

Proxmox Backup Server (PBS) là gì?

PBS là phần mềm sao lưu cấp doanh nghiệp do đội ngũ Proxmox Server Solutions GmbH phát triển. Nó được thiết kế để sao lưu và khôi phục VM, CT, Host và cả các thư mục, file riêng lẻ.

PBS là một hệ thống chuyên dụng đảm nhiệm vai trò máy chủ lưu trữ cho phép nhiều máy khách (PVE) kết nối và gửi bản sao lưu đến thông qua giao thức HTTPS

PBS hoàn toàn miễn phí với giấy phép mã nguồn mở (AGPLv3). Cũng có phiên bản trả phí với hỗ trợ đến từ đội ngữ.

Cách PBS hoạt động.

PBS không tự theo dõi hay sao lưu các PVE mà hoạt động theo mô hình Client - Server:

diagram-export-08-11-2025-21_51_49.png

Mức độ tiêu thụ tài nguyên.

Môi trường sử dụng

CPU

RAM

Ổ lưu trữ

Ghi chú

Homelab (3–5 VM)

2 cores

4 GB

SSD hoặc NFS

Rất mượt

Trung bình (10–15 VM)

4 cores

8–12 GB

SSD/NAS 1 TB

Ổn định, CPU dùng ~30–50 % khi backup

Nhiều VM (>20)

6–8 cores

16 GB+

NVMe / ZFS RAID

Cần tối ưu song song & dedup metadata

Lưu ý

PBS không cần chạy liên tục 24/7 chỉ cần bật khi có job backup hoặc sync

Có thể dùng cron hoặc schedule để:

Cài đặt PBS

Trong trường hợp sử dụng này sẽ thực hiện backup với những yếu tố sau

Setup trên Synology DSM

Cài đặt VM PBS

Cài đặt NFS cho PBS

Kết nối PVE với PBS

Kiểm tra

Nếu thành setup thành công bạn sẽ thấy console log chạy và có task backup chạy trên PBS












Fix 1

Khi chạy apt install nếu gặp lỗi như hình dưới có thể thử cách sau

unnamed.png

Fix 2

unnamed.png

Sau khi add datastore có thể sẽ gặp lỗi như trên. Lỗi này do user không có quyền đọc ghi trên nfs


Sao lưu với PBS (Simple)

Sao lưu PVE sẽ chia làm 2 phần.

Sao lưu cấu hình PVE

Vì PVE không có chức năng backup cấu hình nên chúng ta sẽ cần sử dụng script và dùng systemd timer để lên lịch backup tự động.

Backup script

Trước khi bắt đầu với script chúng ta cần chuẩn bị 2 thứ là PBS API Token và Fingerprint

Tạo script backup bằng lệnh nano /usr/local/bin/pve-host-backup.sh với nội dung sau. Điền thông tin đã chuẩn bị vào phần Cấu hình.

#!/usr/bin/env bash
set -euo pipefail

########################################
# CẤU HÌNH PBS
########################################
# IP/FQDN PBS và Datastore
PBS_HOST='PBS IP'
PBS_DATASTORE='Datastore'

PBS_USER_TOKEN='Token ID'
PBS_TOKEN_SECRET='API Token Sceret'
PBS_FINGERPRINT='Fingerprint'

########################################
# TUỲ CHỌN CHUNG
########################################
BACKUP_ID="${BACKUP_ID:-"$(hostname)-hostcfg"}"
DRY_RUN="${DRY_RUN:-0}"

export PBS_REPOSITORY="${PBS_USER_TOKEN}@${PBS_HOST}:${PBS_DATASTORE}"
export PBS_PASSWORD="${PBS_TOKEN_SECRET}"
export PBS_FINGERPRINT="${PBS_FINGERPRINT}"

########################################
# NGUỒN BACKUP (CHỈ THƯ MỤC, KHÔNG FILE LẺ)
########################################
SOURCES=(
  "pve-etc.pxar:/etc"                           # Bao trọn: pve, network, hosts, resolv.conf, hostname, apt, ssh, default, kernel, modules, modprobe.d, sysc>
  "pve-cron.pxar:/etc/cron.d"                   # Cron.d
  "pve-crontabs.pxar:/var/spool/cron/crontabs"  # Tất cả crontab user (bao gồm root)
  "pve-root-home.pxar:/root"                    # Script & key quản trị (cân nhắc exclude bớt)
  "pve-usrlocal.pxar:/usr/local"                # Script/service tự thêm (nếu có)
)

# Loại trừ cho nhẹ (tuỳ bạn chỉnh)
EXCLUDES=(
  "--exclude=/root/*.iso"
  "--exclude=/root/*.img"
  "--exclude=/root/.cache"
)

########################################
# TIỀN KIỂM TRA
########################################
echo "[*] Checking PBS login..."
proxmox-backup-client login --repository "${PBS_REPOSITORY}"

########################################
# THỰC HIỆN BACKUP
########################################
echo "[*] Starting host-config backup (backup-id=${BACKUP_ID})"
CMD=(proxmox-backup-client backup)
CMD+=("${SOURCES[@]}")
CMD+=("${EXCLUDES[@]}")
CMD+=(--backup-id "${BACKUP_ID}")
CMD+=(--backup-type host)

if [[ "${DRY_RUN}" == "1" ]]; then
  CMD+=(--dry-run)
  echo "[*] DRY RUN enabled"
fi

# In câu lệnh (ẩn secret) rồi chạy
echo "[*] Running: ${CMD[*]/${PBS_TOKEN_SECRET}/***SECRET***}"
"${CMD[@]}"

echo "[✓] Backup finished"

# Dọn biến nhạy cảm
unset PBS_PASSWORD

Cấp quyền thực thi cho script chmod +x /usr/local/bin/pve-host-backup.sh

Dùng lệnh DRY_RUN=1 /usr/local/bin/pve-host-backup.sh để kiểm tra. Nếu kết thúc bằng [✓] Backup finished tức là thành công.

Dùng lệnh /usr/local/bin/pve-host-backup.sh để chạy script

Lập lịch tự động

Thực hiện trên console hoặc ssh của PVE

Kết quả kiểm tra
Mon 2025-11-10 00:00:00 +07 20h Sun 2025-11-09 00:00:43 +07 3h 47min ago dpkg-db-backup.timer dpkg-db-backup.service
Tue 2025-11-11 02:00:00 +07 1 day 22h - - backup.timer backup.service

Tự động Verify sau khi backup

Chuẩn bị ssh key trên PVE

Thêm code post-verify vào cuối script backup trên.

# ==== POST: VERIFY DATASTORE TRÊN PBS SAU KHI BACKUP THÀNH CÔNG ====
if [[ "${DRY_RUN}" != "1" ]]; then
  echo "[*] Trigger verify on PBS for datastore '${PBS_DATASTORE}' ..."
  # Chạy verify toàn datastore. Mặc định --ignore-verified=true nên chỉ verify snapshot mới/chưa verified
  # hoặc snapshot đã quá hạn theo --outdated-after
  ssh -o BatchMode=yes root@"${PBS_HOST}" \
    "proxmox-backup-manager verify '${PBS_DATASTORE}' --ignore-verified true --outdated-after 7" \
    && echo '[✓] Verify job started on PBS' \
    || echo '[!] Failed to start verify job on PBS'
fi

Sao lưu VM/CTs

Mục tiêu là lên lịch để PVE tự động chạy backup mỗi thứ 3, 5, 7 vào lúc 4h và thực hiện verify sau đó vào lúc 5h

Lên lịch backup

Lên lịch verify

Lưu ý

Với task backup cho DSM. PVE sẽ thực hiện backup tất cả các ổ đĩa được passthrough cho DSM nên dung lượng cần backup là rất lớn, và điều này cũng là không cần thiết vì DSM đã sử dụng Hyper Backup.

Để tắt backup cho 2 ổ cứng này PVE Web > pve > VM DSM > Hardware > Disk. Tick chọn Advanced > Bỏ tích Backup

image.png

Sao lưu với PBS (Advance)

Trong phần trước mình đã tìm hiểu về cách tạo backup với PBS. Phần này mình muốn nâng cấp thêm một chút.

Vì thời gian cần backup không nhiều. Việc để PBS chạy lên tục có thể gây lãng phí tài nguyên nên trong phần này mình hướng tới tự động hóa quy trình bật PBS > Backup > Verify > tắt PBS

Để thực hiện việc này thì sẽ sử dụng script và systemd timer.

Trước khi đến bới script chính. Thì thêm một điều cần chú ý. Script này sẽ vẫn sử dụng script backup pve host đã viết ở bài Sao lưu với PBS (Simple)

Ok, Lets go!

Quy trình backup

  1. Bật PBS
  2. Kiểm tra kết nối đảm bảo PBS sẵn sàng
  3. Backup PVE host
  4. Verify PVE host backup
  5. Backup các VM được khai báo
  6. Backup các CT được khai báo
  7. Verify datastore chứa cả VM và CT backup
  8. Prune
  9. GC
  10. Tắt PBS

Các bước trên được thực hiện tuần tự. Xong bước trên mới thực hiện bước dưới. Dừng ngay khi có lỗi.

Script backup

PVE Web > Shell

#!/usr/bin/env bash
set -euo pipefail

########################################
# CẤU HÌNH CHUNG
########################################
PBS_VMID=104

PBS_HOST='192.168.1.104'  # Địa chỉ IP của PBS
PBS_DATASTORE='DSM_NFS' # Datastore đã tạo trên PBS
PBS_USER_TOKEN='root@pam!PVE'   # API Token ID
PBS_TOKEN_SECRET='0218e32d-fefe-4bce-9042-c2dd89389856' # API Token Secret
PBS_FINGERPRINT='35:72:be:a5:08:5f:7a:d1:b1:50:0e:69:fa:6c:bf:78:c9:94:d1:9e:45:34:e6:a0:21:88:3d:ab:59:a6:d8:2d' # Fingerprinter

# Prune and GC job
PRUNE_JOB_ID=${PRUNE_JOB_ID:-default-prune} # Tên Prune job
PRUNE_SCHEDULE=${PRUNE_SCHEDULE:-'Sun 00:00'} # Lịch prune # Không quan trọng vì job này chạy bằng script chứ k chạy tự động
KEEP_BACKUPS=${KEEP_BACKUPS:-7} # số bản backup tối đã sẽ giữ

export PBS_REPOSITORY="${PBS_USER_TOKEN}@${PBS_HOST}:${PBS_DATASTORE}"
export PBS_PASSWORD="${PBS_TOKEN_SECRET}"
export PBS_FINGERPRINT="${PBS_FINGERPRINT}"

# Script backup host pve
# Đã viết ở phần trước
BACKUP_HOST_SCRIPT="/usr/local/bin/pve-self-backup.sh"

# Danh sách VM/CT cần backup
# Không cần dấu phẩy giữa các số
VM_LIST=(100 101 104)       # VM IDs
CT_LIST=(102 103)           # CT IDs

# Tuỳ chọn vzdump dùng cho VM/CT
VZDUMP_STORAGE="VM_PBS"
VZDUMP_OPTS=(--mode snapshot --compress zstd --ionice 7 --bwlimit 0 --storage "${VZDUMP_STORAGE}" --notes-template '{{cluster}} - {{guestname}} - {{node}} - {{vmid}}')

# Thời gian đợi PBS online tối đa (giây)
WAIT_PBS_READY_SEC=$((10*60))  # 10 phút
# Thời gian đợi PBS shutdown tối đa (giây)
WAIT_PBS_SHUT_SEC=$((5*60))    # 5 phút

########################################
# HÀM TIỆN ÍCH
########################################
# Lấy ngày giờ
# Thường dùng để in ra trong log
ts() { date '+%F %T'; }

# Kiểm tra xem lệnh có chính xác không
require_cmd() {
  command -v "$1" >/dev/null 2>&1 || { echo "[$(ts)] Missing command: $1"; exit 1; }
}

# Kiểm tra VM có chạy không
qm_is_running() {
  local vmid=$1
  qm status "$vmid" 2>/dev/null | grep -q 'status: running'
}

# Kiểm tra PBS sẵn sàng chưa bằng cách
# Thử TCP connect
# Đăng nhập PBS
# Kiểm tra dịch vụ proxmox-backup-proxy
wait_for_pbs_ready() {
  local deadline=$(( $(date +%s) + WAIT_PBS_READY_SEC ))
  local ok_streak=0
  echo "[$(ts)] Waiting for PBS services (up to ${WAIT_PBS_READY_SEC}s)..."

  while :; do
    # 1) Port 8007 mở?
    if ! timeout 2 bash -c "echo > /dev/tcp/${PBS_HOST}/8007" 2>/dev/null; then
      ok_streak=0; goto_sleep; continue
    fi
    # 2) Login repo thành công?
    if ! proxmox-backup-client login --repository "${PBS_REPOSITORY}" >/dev/null 2>&1; then
      ok_streak=0; goto_sleep; continue
    fi
    # 3) Dịch vụ proxy active bên trong PBS?
    if ! ssh -o BatchMode=yes -o ConnectTimeout=3 root@"${PBS_HOST}" \
         'systemctl is-active --quiet proxmox-backup-proxy'; then
      ok_streak=0; goto_sleep; continue
    fi

    ok_streak=$((ok_streak+1))
    if (( ok_streak >= 3 )); then
      echo "[$(ts)] PBS ready (proxy up, port 8007 OK, login OK)."
      sleep 5  # grace thêm vài giây
      return 0
    fi

    goto_sleep
  done

  echo "[$(ts)] ERROR: PBS not ready within ${WAIT_PBS_READY_SEC}s"; return 1
}

# ngủ 5s
goto_sleep() {
  if (( $(date +%s) > deadline )); then return; fi
  sleep 5
}

# Hàm không còn được sử dụng nữa
# Thử đăng nhập PBS
# Lặp lại đến khi thành công
wait_for_pbs_login() {
  local deadline=$(( $(date +%s) + WAIT_PBS_READY_SEC ))
  echo "[$(ts)] Waiting for PBS to be reachable (up to ${WAIT_PBS_READY_SEC}s)..."
  while :; do
    if proxmox-backup-client login --repository "${PBS_REPOSITORY}" >/dev/null 2>&1; then
      echo "[$(ts)] PBS login OK."
      return 0
    fi
    if (( $(date +%s) > deadline )); then
      echo "[$(ts)] ERROR: PBS not ready within ${WAIT_PBS_READY_SEC}s"
      return 1
    fi
    sleep 5
  done
}

# Verify data 
# Bỏ qua những phiên bản đã verify
verify_datastore() {
  echo "[$(ts)] Trigger verify on PBS datastore '${PBS_DATASTORE}'..."
  ssh -o BatchMode=yes root@"${PBS_HOST}" \
    "proxmox-backup-manager verify '${PBS_DATASTORE}' --ignore-verified true" \
    && echo "[$(ts)] Verify finished." \
    || { echo "[$(ts)] ERROR: Verify failed"; return 1; }
}

# Sao lưu PVE host sử dụng pve-self-backup.sh
backup_host() {
  echo "[$(ts)] === BACKUP HOST PVE ==="
  local tries=0
  local max=5
  local rc
  while ((tries<max)); do
    if DRY_RUN=0 "${BACKUP_HOST_SCRIPT}"; then
      echo "[$(ts)] Host backup DONE."; return 0
    fi
    rc=$?
    # Nếu lỗi kết nối thì retry, còn lỗi khác thì fail ngay
    if journalctl -u run-backup-pipeline.service -n 50 --no-pager 2>/dev/null | grep -qE 'No route to host|client error \(Connect\)'; then
      tries=$((tries+1))
      echo "[$(ts)] Connect error, retry ${tries}/${max}..."
      sleep $((5*tries))
    else
      return $rc
    fi
  done
  echo "[$(ts)] ERROR: Host backup failed after retries."; return 1
}

# Backup VMs
backup_vms() {
  echo "[$(ts)] === BACKUP VMs ==="
  for id in "${VM_LIST[@]}"; do
    echo "[$(ts)] vzdump VM ${id} ..."
    vzdump "${id}" "${VZDUMP_OPTS[@]}"
    echo "[$(ts)] VM ${id} backup DONE."
  done
}

# Backup CTs
backup_cts() {
  echo "[$(ts)] === BACKUP CTs ==="
  for id in "${CT_LIST[@]}"; do
    echo "[$(ts)] vzdump CT ${id} ..."
    vzdump "${id}" "${VZDUMP_OPTS[@]}"
    echo "[$(ts)] CT ${id} backup DONE."
  done
}

# Prune
prune_datastore() {
  local job="$PRUNE_JOB_ID"
  echo "[$(ts)] Ensure PRUNE job '${job}' exists (store='${PBS_DATASTORE}', keep-last=${KEEP_BACKUPS})..."

  # Nếu job đã tồn tại -> update (giữ nguyên lịch hoặc đặt lại), disable để không tự chạy
  if ssh -o BatchMode=yes root@"${PBS_HOST}" "proxmox-backup-manager prune-job show '${job}' >/dev/null 2>&1"; then
    ssh -o BatchMode=yes root@"${PBS_HOST}" \
      "proxmox-backup-manager prune-job update '${job}' \
         --store '${PBS_DATASTORE}' \
         --keep-last ${KEEP_BACKUPS} \
         --schedule '${PRUNE_SCHEDULE}' \
         --disable true"
  else
    # Tạo job mới: cần lịch hợp lệ, nhưng disable để không tự động chạy
    ssh -o BatchMode=yes root@"${PBS_HOST}" \
      "proxmox-backup-manager prune-job create '${job}' \
         --store '${PBS_DATASTORE}' \
         --schedule '${PRUNE_SCHEDULE}' \
         --keep-last ${KEEP_BACKUPS} \
         --disable true"
  fi || { echo "[$(ts)] ERROR: create/update prune job failed"; return 1; }

  echo "[$(ts)] Running PRUNE job '${job}'..."
  ssh -o BatchMode=yes root@"${PBS_HOST}" \
    "proxmox-backup-manager prune-job run '${job}'" \
    && echo "[$(ts)] Prune finished." \
    || { echo "[$(ts)] ERROR: Prune failed"; return 1; }
}

# GC
gc_datastore() {
  echo "[$(ts)] Running GARBAGE COLLECTION on datastore '${PBS_DATASTORE}'..."
  ssh -o BatchMode=yes root@"${PBS_HOST}" \
    "proxmox-backup-manager garbage-collection start '${PBS_DATASTORE}'" \
    && echo "[$(ts)] GC finished." \
    || { echo "[$(ts)] ERROR: GC failed"; return 1; }
}

# Bật PBS
pbs_start() {
  echo "[$(ts)] Starting PBS VM ${PBS_VMID}..."
  if qm_is_running "${PBS_VMID}"; then
    echo "[$(ts)] PBS already running."
  else
    qm start "${PBS_VMID}"
  fi
  wait_for_pbs_ready
}

# Tắt PBS
pbs_shutdown() {
  echo "[$(ts)] Shutting down PBS VM ${PBS_VMID}..."

  # 0) Nếu đã tắt thì thoát sớm
  if ! qm_is_running "${PBS_VMID}"; then
    echo "[$(ts)] PBS already off."
    return 0
  fi

  # 1) Ưu tiên tắt từ bên trong guest (an toàn nhất)
  #    Yêu cầu bạn đã cấu hình SSH key PVE -> PBS
  #    Dùng systemd để poweroff không chờ (fork ra nền)
  ssh -o BatchMode=yes -o ConnectTimeout=5 root@"${PBS_HOST}" \
    'nohup systemctl poweroff >/dev/null 2>&1 &' || true
  echo "[$(ts)] Sent 'systemctl poweroff' to PBS."

  # 2) Chờ PBS tắt hẳn (polling qm status)
  SHUT_WAIT=${WAIT_PBS_SHUT_SEC:-300} # 5 phút mặc định
  deadline=$(( $(date +%s) + SHUT_WAIT ))
  while qm_is_running "${PBS_VMID}"; do
    if (( $(date +%s) > deadline )); then
      echo "[$(ts)] Guest did not poweroff within ${SHUT_WAIT}s. Trying 'qm shutdown'..."
      break
    fi
    sleep 5
  done

  # 3) Nếu vẫn còn chạy -> thử 'qm shutdown' với timeout dài hơn
  if qm_is_running "${PBS_VMID}"; then
    qm shutdown "${PBS_VMID}" --timeout "${SHUT_WAIT}" || true
  fi

  # 4) Chờ thêm chút nữa
  end2=$(( $(date +%s) + 60 ))
  while qm_is_running "${PBS_VMID}"; do
    if (( $(date +%s) > end2 )); then
      echo "[$(ts)] Still running after graceful attempts. Forcing 'qm stop'..."
      qm stop "${PBS_VMID}" || true
      break
    fi
    sleep 5
  done

  # 5) Kết luận: coi như thành công nếu thực sự đã tắt
  if ! qm_is_running "${PBS_VMID}"; then
    echo "[$(ts)] PBS is off."
    return 0
  else
    echo "[$(ts)] WARNING: PBS looks stuck running. Please check 'qm monitor ${PBS_VMID}'."
    return 1
  fi
}


########################################
# MAIN
########################################
require_cmd qm
require_cmd proxmox-backup-client
require_cmd ssh
require_cmd vzdump

echo "[$(ts)] ===== BACKUP PIPELINE START ====="

# 1) Start PBS & wait
pbs_start

# 2) Backup host PVE
backup_host

# 3) Verify sau host backup
verify_datastore

# 4) Backup các VM/CT
backup_vms
backup_cts

# 5) Verify sau VM/CT backup
verify_datastore

# 6) PRUNE rồi GC
prune_datastore
gc_datastore

# 7) Shutdown PBS
pbs_shutdown

echo "[$(ts)] ===== BACKUP PIPELINE DONE ====="

Tự động hóa

Thực hiện trên console hoặc ssh của PVE