การสำรอง appdata มาตรฐานของ Unraid จะหยุดทุกคอนเทนเนอร์ คัดลอกโฟลเดอร์ แล้วเริ่มใหม่ เพราะสำเนาสดของฐานข้อมูล SQLite หรือ Postgres ที่กำลังทำงานคือสำเนาที่ขาดกลางคันและกู้คืนไม่ได้ ทางออกไม่ใช่ Unraid รุ่นใหม่ แต่คือสแนปช็อตของระบบไฟล์ ถ้า appdata ของคุณอยู่บนพูลแคช Btrfs คุณมีทุกอย่างที่ต้องใช้อยู่แล้ว: การแปลงโฟลเดอร์ appdata เป็น subvolume ครั้งเดียว สคริปต์ฮุกสองตัว และค่าตั้งไม่กี่อย่างในปลั๊กอิน Appdata.Backup ทุกอย่างด้านล่างทำจากหน้าเว็บ — User Scripts คือเชลล์ — และผลลัพธ์คือการสำรองรายวัน แบบบีบอัดและหมุนเวียนบนอาร์เรย์ หนึ่งไฟล์เก็บต่อหนึ่งคอนเทนเนอร์ กู้คืนได้ด้วย Zip Manager และตัวจัดการไฟล์ ขณะที่ทุกคอนเทนเนอร์ยังทำงานอยู่
อ่านก่อนเริ่ม — วิธีนี้รับประกันอะไรและไม่รับประกันอะไร การสำรองด้วยสแนปช็อตเป็นแบบ crash-consistent: ทุกคอนเทนเนอร์ถูกเก็บในสภาพเดียวกับหลังไฟดับ ไม่ใช่หลังการปิดเครื่องอย่างเรียบร้อย ฐานข้อมูลที่เปิด journaling ไว้ (SQLite ในโหมดเริ่มต้นหรือโหมด WAL, PostgreSQL, MariaDB) ถูกสร้างมาให้กู้คืนจากสภาพนั้นพอดี และกู้คืนได้จริงในการเริ่มครั้งถัดไป การรับประกันนี้มีขอบเขต และเป็นหน้าที่ของคุณที่จะตรวจกับคอนเทนเนอร์ของคุณเอง:
journal_mode=OFF หรือ MEMORY, synchronous=OFF — หายากในซอฟต์แวร์เซิร์ฟเวอร์ แต่มีอยู่) ไม่มีอะไรให้กู้คืน และอาจกลับมาในสภาพเสียหาย การหยุดอย่างนุ่มนวลคือการสำรองที่ปลอดภัยเพียงอย่างเดียวสำหรับมัน ตั้งให้คอนเทนเนอร์นั้นถูกหยุดในปลั๊กอินdocker unpause <name> คือวิธีแก้ทั้งหมดทดสอบการกู้คืนกับโฟลเดอร์ทดลองก่อนจะพึ่งพาสิ่งใดในนี้ และเปิดการสำรองแบบเดิมของคุณไว้จนกว่าคุณจะกู้คืนคอนเทนเนอร์จากระบบนี้ได้สำเร็จ นี่คือคอนเทนเนอร์และข้อมูลของคุณ ไม่มีอะไรในนี้ที่ถูกทดสอบกับของคุณ
สแนปช็อตเกิดขึ้นทันทีและเป็นอะตอมมิก Btrfs แช่แข็ง subvolume ณ จุดเวลาหนึ่งภายในไม่กี่มิลลิวินาทีแบบ copy-on-write คอนเทนเนอร์ที่ทำงานอยู่จึงเขียนลงต้นไม้สดต่อไป ขณะที่การสำรองอ่านต้นไม้ที่ถูกแช่แข็ง การสำรองใช้เวลานานเท่าไรก็ได้ ไม่มีอะไรต้องรอ
มีแค่ subvolume ที่ถ่ายสแนปช็อตได้ ใน Unraid ส่วนใหญ่ appdata เป็นโฟลเดอร์ธรรมดาบนพูล ไม่ใช่ subvolume และ btrfs subvolume snapshot ปฏิเสธมัน การแปลงเป็นงานครั้งเดียวที่ต้องหยุด Docker หนึ่งครั้ง — การหยุดครั้งสุดท้ายที่คุณจะทำเพื่อการสำรอง
คอนเทนเนอร์ถูกพักชั่วขณะที่ถ่ายสแนปช็อต นี่คือสิ่งที่ Proxmox ทำเมื่อสำรองคอนเทนเนอร์ LXC ในโหมดสแนปช็อต: พักคอนเทนเนอร์ ถ่ายสแนปช็อตพื้นที่เก็บข้อมูล ให้ทำงานต่อ แล้วเก็บสแนปช็อตเป็นไฟล์ Docker มีตัวควบคุมแบบเดียวกันคือ docker pause ซึ่งแช่แข็งทุกโปรเซสในคอนเทนเนอร์ระหว่าง syscall ในขณะที่สแนปช็อตบันทึก จึงไม่มีอะไรกำลังเขียนค้างอยู่ การพักนานเท่ากับที่สแนปช็อตใช้ — ปกติไม่ถึงสองวินาทีสำหรับทุกคอนเทนเนอร์รวมกัน — และคอนเทนเนอร์จะกลับมาไม่ว่าสแนปช็อตจะสำเร็จหรือไม่ (การ fsfreeze ทั้งพูลแบบ VM เป็นเครื่องมือที่ผิดตรงนี้: สร้างสแนปช็อต Btrfs บน Btrfs ที่ถูกแช่แข็งไม่ได้ และการแช่แข็งพูลจะแช่แข็ง docker.img ไปด้วย)
ปลั๊กอินจัดตารางเวลา ฮุกสองตัวทำงาน Appdata.Backup (ฟอร์กที่ยังดูแลอยู่ของ Commifreak) มีฮุกสคริปต์ก่อนรันและหลังรัน และส่งโฟลเดอร์ปลายทางของแต่ละรอบให้ ฮุกก่อนรันพักคอนเทนเนอร์ที่ทำงานอยู่ ถ่ายสแนปช็อต แล้วให้ทำงานต่อ ฮุกหลังรันเขียนไฟล์เก็บบีบอัดหนึ่งไฟล์ต่อโฟลเดอร์คอนเทนเนอร์จากสแนปช็อตลงโฟลเดอร์ของรอบนั้น แล้วลบสแนปช็อต ตัวปลั๊กอินเองสำรองไดรฟ์แฟลชและข้อมูลเมตาของ VM หมุนเวียนรอบเก่า และอัปเดตคอนเทนเนอร์หลังจากนั้น ตัวเลือก “skip stopping” ของมันเปิดอยู่ จึงไม่เคยหยุดคอนเทนเนอร์ที่ทำงานอยู่
docker exec <name> pg_dumpall -U postgres > /mnt/cache/appdata/<name>/dump.sql สำหรับ Postgres และ sqlite3 <db> ".backup <db>.bak" สำหรับ SQLite — ดัมป์จึงเดินทางไปในสแนปช็อตด้วย นั่นคือสิ่งที่เทียบเท่า VSS ของเครื่อง Windows ในฝั่ง Linux และทำรายแอปcache และ appdata อยู่ที่ /mnt/cache/appdata ถ้าของคุณชื่ออื่น ให้แทน /mnt/cache ทั้งหมด ZFS ทำงานแบบเดียวกันด้วย zfs snapshot และพาธ .zfs/snapshot ส่วน XFS ถ่ายสแนปช็อตไม่ได้เลย คู่มือนี้ใช้กับพูล XFS ไม่ได้.tar.zst ที่วิธีนี้สร้างได้ ซึ่งทำให้กู้คืนได้โดยไม่ต้องใช้เชลล์ การพัฒนาของมันหยุดไปแล้ว แต่ปลั๊กอินยังใช้ได้บน 7.x และ 7-Zip เป็น dependency ที่เสถียรUnraid รองรับหลายพูล และ SATA SSD ราคาประหยัดหรือ NVMe ตัวที่สองทำให้ปัญหานี้แก้ด้วยโครงสร้างแทนพฤติกรรม: appdata อยู่พูลหนึ่ง ทุกอย่างที่ชั่วคราวอยู่อีกพูล Main → Add Pool ตั้งชื่อว่า scratch (หรือ downloads) กำหนดไดรฟ์ แล้วฟอร์แมต จากนั้นชี้แชร์ชั่วคราวไปที่มัน: สำหรับแชร์ downloads, incomplete, การแปลงไฟล์ และกล้อง ให้ไปที่ Shares → แชร์ → Primary storage = scratch, Secondary = Array ถ้าคุณอยากให้ mover ย้ายไฟล์ที่เสร็จแล้วไปอาร์เรย์ หรือ None สำหรับพื้นที่ชั่วคราวล้วน พูล appdata จะเห็นแต่ appdata สแนปช็อตเล็ก และพูล scratch ที่เต็มก็พาฐานข้อมูลคอนเทนเนอร์ล่มไปด้วยไม่ได้
ขนาด: ข้อมูลทอร์เรนต์และ usenet ที่ยังไม่เสร็จแทบไม่เคยต้องการเกินไม่กี่ร้อยกิกะไบต์ ถ้า mover รันทุกคืน ส่วนวิดีโอจากกล้องและการแปลงไฟล์เป็นไปตามระยะการเก็บของคุณ ไดรฟ์ 1 TB ครอบคลุมบ้านส่วนใหญ่
#!/bin/bash
# what is on the pool, largest last, and how full the pool is
du -sh /mnt/cache/* 2>/dev/null | sort -h | tail -8
btrfs fi df /mnt/cache | grep -i "^Data"วิธีย้ายแชร์ผ่านหน้าเว็บคือ mover: Shares → แชร์ → Primary storage = Cache, Secondary storage = Array, mover action Cache → Array, Apply แล้ว Main → Move หยุดสิ่งที่เขียนลงแชร์ก่อน (คอนเทนเนอร์ ซอฟต์แวร์กล้อง) และอ่านกับดักสองข้อด้านล่างก่อนกด Apply
/mnt/user ในขณะนั้นจะตายด้วยข้อผิดพลาดเรื่องสิทธิ์ หยุดตัวเขียนก่อนกด Apply#!/bin/bash
SHARE=downloads # the share to move off the pool
# /mnt/user0 is the array without the cache overlay: same share, array tier
rsync -a --info=progress2 --remove-source-files "/mnt/cache/$SHARE/" "/mnt/user0/$SHARE/"
find "/mnt/cache/$SHARE" -type d -empty -delete
du -sh "/mnt/cache/$SHARE" 2>/dev/null || echo "cache copy gone"
du -sh "/mnt/user0/$SHARE"
echo "now set the share to Primary = Array, Secondary = None and Apply (nothing writing when you click)"mkdir /mnt/<share>: file exists บนเครื่องลูกข่าย: umount -l /mnt/<share> && mount /mnt/<share> แล้วเริ่มคอนเทนเนอร์ วิธีแก้เดียวกันใช้หลังการหยุด/เริ่มอาร์เรย์ของ Unraid ทุกครั้ง#!/bin/bash
btrfs subvolume show /mnt/cache/appdata 2>&1 | head -3
echo "--- size of appdata (step 2 needs this much free on the pool):"
du -sh /mnt/cache/appdata
echo "--- pool:"
btrfs fi df /mnt/cache | grep -i "^Data"#!/bin/bash
set -u
POOL=/mnt/cache
echo "$(date '+%T') stopping Docker"
/etc/rc.d/rc.docker stop
if btrfs subvolume show "$POOL/appdata" >/dev/null 2>&1; then
echo "appdata is already a subvolume; starting Docker and stopping here"; /etc/rc.d/rc.docker start; exit 0
fi
echo "$(date '+%T') size: $(du -sh "$POOL/appdata" | cut -f1)"
mv "$POOL/appdata" "$POOL/appdata.old" || exit 1
btrfs subvolume create "$POOL/appdata" || exit 1
echo "$(date '+%T') copying (this is the long part)"
cp -a "$POOL/appdata.old/." "$POOL/appdata/" 2> /root/appdata-convert.log
echo "$(date '+%T') copy done; cp errors: $(wc -l < /root/appdata-convert.log) (expect 0)"
btrfs subvolume show "$POOL/appdata" | head -2
echo "$(date '+%T') comparing old and new (dangling symlinks and sockets are expected noise)"
diff -qr "$POOL/appdata.old" "$POOL/appdata" > /root/appdata-diff.txt 2>&1
echo "diff lines: $(wc -l < /root/appdata-diff.txt) of which 'No such file' (symlinks): $(grep -c 'No such file or directory' /root/appdata-diff.txt)"
echo "--- lines that are NOT symlink or socket noise (a real difference would show here):"
grep -vE "No such file or directory|is a socket" /root/appdata-diff.txt | head -10
echo "$(date '+%T') starting Docker"
/etc/rc.d/rc.docker start
echo "done. Check the containers; delete $POOL/appdata.old tomorrow (User Script: appdata-cleanup)."cp -a คัดลอกเป็นซ็อกเก็ต บล็อกที่ขึ้นหัวว่า “lines that are NOT symlink or socket noise” คือสิ่งที่สำคัญ: บรรทัด Only in appdata.old หรือ Files … differ ใด ๆ ใต้โฟลเดอร์ฐานข้อมูล หมายถึงสำเนาที่ขาด บล็อกนั้นว่าง และข้อผิดพลาดของ cp เป็น 0 คือไฟเขียวappdata/<plex-folder>/Plex Media Server/plexmediaserver.pid แล้วรีสตาร์ตคอนเทนเนอร์ คาดว่าการเริ่มครั้งแรกจะช้าสำหรับอะไรก็ตามที่ไล่อ่านข้อมูลของมันตอนบูต#!/bin/bash
rm -rf /mnt/cache/appdata.old && echo "appdata.old removed"
btrfs fi df /mnt/cache | grep -i "^Data"appdata-snap-pre และ appdata-snap-post ตารางเวลา Scheduled Disabled — ปลั๊กอินสำรองข้อมูลเป็นผู้เรียก ไม่ควรมีอย่างอื่นเรียก ก่อนรันจะพักทุกคอนเทนเนอร์ที่ทำงานอยู่ ถ่ายสแนปช็อตแบบอ่านอย่างเดียวข้าง appdata (ไม่เคยไว้ข้างใน ไม่อย่างนั้นทุกสแนปช็อตจะมีตัวก่อนหน้าอยู่ข้างใน) แล้วให้ทำงานต่อ มันบันทึกสิ่งที่พักไว้ในไฟล์รายการ และ trap จะปล่อยพวกมันไม่ว่าสคริปต์จะจบแบบไหน หลังรันจะปล่อยอะไรก็ตามที่รายการนั้นยังมีชื่ออยู่ก่อน (กันไว้สองชั้น สำหรับกรณีฮุกก่อนรันถูกฆ่ากลางคัน) แล้วเขียนไฟล์เก็บหนึ่งไฟล์ต่อโฟลเดอร์คอนเทนเนอร์จากสแนปช็อตลงโฟลเดอร์ที่ลงวันที่ของรอบนั้น — containers/<name>.tar.zst โฟลเดอร์คอนเทนเนอร์อยู่ระดับบนสุด ไม่มีอะไรซ้อน — แล้วจึงลบสแนปช็อต#!/bin/bash
#description=Appdata.Backup PRE-run hook: pause running containers, read-only Btrfs snapshot of appdata beside it, unpause. Called by the plugin; do not schedule.
SNAP=/mnt/cache/.snapshots/appdata-backup
LIST=/mnt/cache/.snapshots/paused.list
mkdir -p /mnt/cache/.snapshots
btrfs subvolume delete "$SNAP" 2>/dev/null
# only containers that are running now; one you paused on purpose stays as it is
RUNNING=$(docker ps -q --filter status=running)
echo "$RUNNING" > "$LIST"
unpause() { [ -n "$RUNNING" ] && docker unpause $RUNNING >/dev/null 2>&1; rm -f "$LIST"; }
trap unpause EXIT # whatever happens below, they come back
t0=$(date +%s%3N)
[ -n "$RUNNING" ] && docker pause $RUNNING >/dev/null
sync -f /mnt/cache
btrfs subvolume snapshot -r /mnt/cache/appdata "$SNAP"
rc=$?
unpause; trap - EXIT
msg="paused $(echo $RUNNING | wc -w) containers for $(( $(date +%s%3N) - t0 )) ms; snapshot rc=$rc"
echo "$msg"; logger -t appdata-snap-pre "$msg" # the plugin discards hook output: the system log keeps it
exit $rc#!/bin/bash
#description=Appdata.Backup POST-run hook: one <container>.tar.zst per top-level appdata folder, from the snapshot, into the run's folder; then delete the snapshot. Called by the plugin with the destination as $2; do not schedule.
SNAP=/mnt/cache/.snapshots/appdata-backup
# belt and braces: if the pre hook was ever killed mid-pause, release exactly what it paused
LIST=/mnt/cache/.snapshots/paused.list
[ -s "$LIST" ] && docker unpause $(cat "$LIST") >/dev/null 2>&1; rm -f "$LIST"
DEST="${2:-}" # the plugin passes: post-run <destination> <success>
OUT="$DEST/containers"
if [ -z "$DEST" ] || [ ! -d "$DEST" ]; then echo "no destination given; snapshot left in place"; exit 1; fi
if [ ! -d "$SNAP" ]; then echo "no snapshot at $SNAP"; exit 1; fi
mkdir -p "$OUT"
fail=0; n=0
for d in "$SNAP"/*/; do
name=$(basename "$d")
if tar -I 'zstd -T0 -3' -cf "$OUT/$name.tar.zst" -C "$SNAP" "$name"; then n=$((n+1)); else fail=1; echo "FAILED: $name"; fi
done
{ echo "per-container archives $(date '+%F %T')"; ls -la "$OUT"; } > "$OUT/MANIFEST.txt"
btrfs subvolume delete "$SNAP"
echo "archived $n folders to $OUT (failures: $fail)"
logger -t appdata-snap-post "archived $n folders to $OUT (failures: $fail)" # the plugin discards hook output: the system log keeps it
if [ "$fail" = 1 ]; then
/usr/local/emhttp/webGui/scripts/notify -e "Appdata backup" -s "per-container archives: failures" -d "see $OUT/MANIFEST.txt" -i alert
exit 1
fibash User Scripts ยังเป็นต้นฉบับเดียว: แก้ที่นั่น ส่วน wrapper ไม่เคยเปลี่ยน User Script ตัวที่สามสร้าง wrapper ให้ รันมันครั้งเดียว#!/bin/bash
S=/boot/config/plugins/user.scripts/scripts
W=/mnt/cache/appdata/appdata.backup-scripts
mkdir -p "$W"
for h in pre post; do
[ -f "$S/appdata-snap-$h/script" ] || { echo "User Script appdata-snap-$h not found — create it first"; exit 1; }
printf '#!/bin/bash\nexec bash %s "$@"\n' "$S/appdata-snap-$h/script" > "$W/appdata-snap-$h.sh"
chmod 755 "$W/appdata-snap-$h.sh"
done
ls -la "$W"
echo "wrappers ready: $W/appdata-snap-pre.sh and appdata-snap-post.sh"#!/bin/bash
# dress rehearsal into a scratch folder: snapshot, one archive per container, snapshot deleted
W=/mnt/cache/appdata/appdata.backup-scripts
mkdir -p /mnt/user/backups/ab_test
"$W/appdata-snap-pre.sh" && "$W/appdata-snap-post.sh" post-run /mnt/user/backups/ab_test true
ls -la /mnt/user/backups/ab_test/containers | head
echo "now open one small archive in ab_test/containers with Zip Manager: it must extract to a <name>/ folder. Then delete /mnt/user/backups/ab_test in the file manager."docker pause คือ cgroup freezer: โปรเซสหยุดตรงที่มันอยู่ และทำงานต่อจากตรงที่มันเคยอยู่ ไม่มีสัญญาณถูกส่ง ไม่มีอะไรจบการทำงาน การเชื่อมต่อ TCP ที่เปิดอยู่จะรอ สตรีมสื่อบัฟเฟอร์หนึ่งวินาที healthcheck ที่ถึงกำหนดระหว่างการพักจะรันหลังจากนั้น แตะเฉพาะคอนเทนเนอร์ที่ทำงานอยู่ตอนฮุกเริ่ม — ตัวที่คุณพักไว้เองจะถูกปล่อยไว้แบบพัก — และไฟล์รายการระบุชื่อเฉพาะพวกนั้น การปล่อยจึงไม่เคยแตะอย่างอื่น| ค่าตั้ง | ค่า | เหตุผล |
|---|---|---|
| Backup type | Stop all containers, backup, start all | ไม่มีผลเมื่อเปิด skip-stopping ไว้ ปล่อยค่าเริ่มต้น |
| Appdata source(s) | /mnt/cache/.snapshots/appdata-backup | ต้นไม้ที่ถูกแช่แข็ง ไม่ใช่ต้นไม้สด |
| Backup destination | /mnt/user/backups/ | ปลั๊กอินสร้างโฟลเดอร์ ab_… ที่ลงวันที่ต่อรอบ ไฟล์เก็บอยู่ในนั้น |
| Use compression | Yes, multicore | ใช้กับไฟล์เก็บของปลั๊กอินเอง (แฟลช) ไม่มีพิษภัย |
| Backup the flash drive | Yes | ลงในโฟลเดอร์ที่ลงวันที่เดียวกัน ระยะการเก็บเดียวกัน |
| Backup VM meta | Yes | ใช้ทรัพยากรน้อย |
| Advanced → Skip stopping of containers? | Yes | นี่คือจุดประสงค์ทั้งหมด เป็นค่าเริ่มต้นส่วนกลาง ปรับแยกรายคอนเทนเนอร์ได้ |
| Pre-run script | /mnt/cache/appdata/appdata.backup-scripts/appdata-snap-pre.sh | Wrapper → ถ่ายสแนปช็อต |
| Post-run script | /mnt/cache/appdata/appdata.backup-scripts/appdata-snap-post.sh | Wrapper → เขียนไฟล์เก็บรายคอนเทนเนอร์ และลบสแนปช็อต |
| Include extra files/folders | ว่าง | ฮุกหลังรันทำการเก็บไฟล์ พาธตรงนี้จะเพิ่มสำเนาเต็มชุดที่สอง |
| Schedule | ทุกวัน ชั่วโมงไหนก็ได้ | มันไม่หยุดอะไรเลย จึงรันวันไหนเวลาไหนก็ได้ |
| Delete backups older than / keep at least | 5 / 5 | appdata และแฟลชของห้าวันล่าสุด ปรับขนาดตามอาร์เรย์ของคุณ |
| Update containers after backup? | ใช่ — มีข้อยกเว้น | ดูด้านล่าง |
[08:30:01][Main] Executing script '/mnt/cache/appdata/appdata.backup-scripts/appdata-snap-pre.sh' 'pre-run' ...
[08:30:04][Main] Script executed! (pause + snapshot + unpause: three seconds)
[08:30:04][Main] Backing up from: /mnt/cache/.snapshots/appdata-backup
[08:31:06][Main] Auto-Update for 'jellyfin' is enabled but no update is available.
... one such line per container ...
[08:31:06][jellyfin] Stopping jellyfin... NOT stopping jellyfin because it should be backed up WITHOUT stopping!
... one such line per running container ...
[08:31:06][jellyfin] jellyfin does not have any volume to back up! Skipping. Please consider ignoring this container.
... one such warning per container: expected, the source is the snapshot, not the live folder ...
[08:31:06][Main] Backing up the flash drive.
[08:33:31][Main] VM meta backup enabled! Backing up...
[08:33:31][Main] Checking retention...
[08:33:31][Main] Delete old backup: /mnt/user/backups/ab_20260916_083001
[08:33:31][Main] DONE! Thanks for using this plugin and have a safe day ;)
[08:33:32][Main] Executing script '/mnt/cache/appdata/appdata.backup-scripts/appdata-snap-post.sh' 'post-run' ...
[08:56:15][Main] Script executed! (the archives are finished here, not at DONE)appdata-snap-pre: paused 40 containers for <n> ms; snapshot rc=0 (a few hundred to a couple of thousand ms)
appdata-snap-post: archived 47 folders to /mnt/user/backups/ab_20260924_083001/containers (failures: 0)logger ด้วย — ที่นั่นคือที่อยู่ของเวลาที่พักและจำนวนไฟล์เก็บ และความล้มเหลวจะส่งการแจ้งเตือนของ Unraid เพิ่มด้วยจากนั้นดูในตัวจัดการไฟล์: backups/ab_<date>/ มี containers/ ที่มี <name>.tar.zst หนึ่งไฟล์ต่อโฟลเดอร์ appdata และ MANIFEST.txt อยู่ข้างไฟล์ zip สำรองแฟลช เทมเพลต XML ของคอนเทนเนอร์ และ vm_meta.tgz ส่วน /mnt/cache/.snapshots/ ต้องว่างระหว่างรอบ — ไม่มีสแนปช็อตและไม่มี paused.list — และหน้า Docker ต้องแสดงทุกคอนเทนเนอร์ทำงานอยู่ ด้วยเวลาทำงานต่อเนื่องเท่าเดิมก่อนสำรอง
appdata/<name> → appdata/<name>.broken เก็บไว้จนกว่าคอนเทนเนอร์ที่กู้คืนจะพิสูจน์ได้ว่าใช้งานได้backups/ab_<date>/containers/<name>.tar.zst เลือก Extract แล้วแตกไปยังที่ชั่วคราวเช่น backups/restore ผลที่ได้คือโฟลเดอร์ <name>/ เดียวที่มีไฟล์ของคอนเทนเนอร์ โดยเจ้าของไฟล์ยังครบ<name>.brokenการซ้อมคือขั้นที่ 3 และ 4 ลงโฟลเดอร์ทดลองเท่านั้น แล้วดูสองโฟลเดอร์เทียบกัน: จำนวนไฟล์และขนาดเท่ากัน และมีแค่ไฟล์ฐานข้อมูล (พร้อมไฟล์คู่ -wal/-shm) และล็อกที่ใหม่กว่าในโฟลเดอร์จริง — ทุกอย่างที่ถูกเขียนหลังสแนปช็อต
#!/bin/bash
NAME=sonarr
DATE=20260919_083002
mv "/mnt/cache/appdata/$NAME" "/mnt/cache/appdata/$NAME.broken"
tar --zstd -xf "/mnt/user/backups/ab_$DATE/containers/$NAME.tar.zst" -C /mnt/cache/appdata
ls -la "/mnt/cache/appdata/$NAME" | head
echo "start the container; delete $NAME.broken once it is proven"/mnt/user ในขณะนั้นจะตาย หยุดตัวเขียนก่อน แล้วค่อยคลิกnohup อย่างเดียวไม่รอด — setsid nohup … < /dev/null & รอด)bashdocker unpause <name> แก้ได้ และล็อกของรอบนั้นบอกว่าทำไมdocker volume ls แสดงให้เห็น Nextcloud AIO คือตัวที่พบบ่อย) อยู่นอกการสำรองนี้ทั้งหมด — และก็อยู่นอกตัวเก่าด้วย ล็อกระบุชื่อพวกมัน (“does NOT exist! Please check your mappings”) ให้แต่ละตัวมีการสำรองของมันเอง