SSD Nodes Learn Hosting plans →
คู่มือ Matt Connorโดย Matt Connor

สำรองและกู้คืน Immich บน VPS อย่างถูกต้อง

การสำรอง Immich ต้องมีไฟล์ใน UPLOAD_LOCATION, SQL dump ของ Postgres และ .env กับ docker-compose.yml ไม่ใช่การคัดลอก data directory ขณะทำงาน มิฉะนั้นกู้คืนแล้ว timeline อาจว่างเปล่า

สิ่งที่การสำรองข้อมูล Immich ต้องมี

การสำรองข้อมูล Immich คือการเก็บข้อมูล 3 ส่วนพร้อมกันในเวลาเดียว ได้แก่ ไฟล์ต้นฉบับภายใต้ UPLOAD_LOCATION, ไฟล์ SQL dump ของฐานข้อมูล Postgres และไฟล์ .env กับ docker-compose.yml ที่อธิบายโครงสร้างของ stack การกู้คืนข้อมูลหมายถึงการนำไฟล์ dump นั้นไปใส่ในฐานข้อมูลใหม่ขณะที่เซิร์ฟเวอร์ Immich หยุดทำงานอยู่ และเริ่ม stack ส่วนที่เหลือหลังจากนั้นเท่านั้น หากทำผิดลำดับ คุณจะได้ Immich ที่ทำงานได้ปกติแต่แสดง timeline ว่างเปล่าบนดิสก์ที่เต็มไปด้วยข้อมูล

การแยกส่วนข้อมูลมีความสำคัญเพราะ Immich เก็บสถานะไว้ในสองที่ที่ไม่รู้จักกัน Postgres เก็บข้อมูลอัลบั้ม, กลุ่มใบหน้า, ลิงก์ที่แชร์, บัญชีผู้ใช้, API key และ path ของไฟล์แต่ละรายการ ส่วนระบบไฟล์เก็บข้อมูลรูปภาพ หากกู้คืนไฟล์โดยไม่มีฐานข้อมูล Immich จะไม่แสดงอะไรเลย หากกู้คืนฐานข้อมูลโดยไม่มีไฟล์ รูปภาพทุกรายการจะเปิดไม่ได้

คำสั่งในที่นี้เขียนขึ้นสำหรับ Immich v3.1.0 ซึ่งเป็นรุ่นปัจจุบันในช่วงต้นเดือนสิงหาคม 2026 ณ วันที่ 2026-10-21 รุ่นปัจจุบันคือ v3.3.4 ซึ่งเผยแพร่เมื่อ 2026-10-20 โดยไฟล์ compose, อิมเมจฐานข้อมูล และขั้นตอนการสำรองและกู้คืนที่ระบุไว้ตรงกับที่ใช้ในหน้านี้ โปรเจกต์มีการอัปเดตเร็วมากและขั้นตอนการสำรองข้อมูลที่ระบุไว้มีการเปลี่ยนแปลงมากกว่าหนึ่งครั้ง ดังนั้นควรตรวจสอบเวอร์ชันที่คุณใช้งานจริงก่อนคัดลอกคำสั่งใดๆ หาก stack ยังไม่ทำงาน ให้เริ่มจาก คู่มือการติดตั้ง Immich แล้วค่อยกลับมาที่นี่

ทำความเข้าใจว่า path ของคุณชี้ไปที่ใด

ตัวแปรสองตัวใน .env เป็นตัวกำหนดทุกอย่างในหน้านี้ UPLOAD_LOCATION คือไดเรกทอรีหลักที่ Immich ใช้เขียนไฟล์สื่อทั้งหมดลงไป ส่วน DB_DATA_LOCATION คือไดเรกทอรีข้อมูลของ Postgres

ค่าเริ่มต้นใน example.env จะกำหนด UPLOAD_LOCATION=./library ซึ่งเป็นค่าเริ่มต้นที่ทำให้สับสนได้ง่าย เพราะ Immich จะสร้างโฟลเดอร์ชื่อ library ไว้ข้างใน นั้นอีกที ไฟล์ต้นฉบับของคุณจะไปอยู่ที่ ./library/library ให้กำหนดเป็น absolute path แทน เพื่อให้สคริปต์สำรองข้อมูลไม่ขึ้นอยู่กับไดเรกทอรีที่คุณสั่งรันสคริปต์นั้น

UPLOAD_LOCATION=/srv/immich/data
DB_DATA_LOCATION=/srv/immich/postgres
DB_USERNAME=postgres
DB_DATABASE_NAME=immich
IMMICH_VERSION=v3.1.0

ภายใน UPLOAD_LOCATION Immich จะสร้างโฟลเดอร์ขึ้นมาหลายรายการ โดยมีสามรายการที่เก็บข้อมูลซึ่งไม่มีงานใดสามารถสร้างขึ้นใหม่ได้:

  • library: ไฟล์ต้นฉบับที่จัดเรียงตามเทมเพลตการจัดเก็บข้อมูลของคุณ
  • upload: ไฟล์ต้นฉบับที่ยังไม่ได้ถูกย้ายเข้าสู่โครงสร้างเทมเพลต รวมถึงไฟล์ที่กำลังอัปโหลด
  • profile: รูปโปรไฟล์ของผู้ใช้

หากสูญเสีย library ไป รูปภาพนั้นจะหายไปทันที Immich ไม่มีการเก็บสำเนาชุดที่สองของไฟล์ต้นฉบับไว้ที่ใดทั้งสิ้น

เหตุใดการคัดลอกไดเรกทอรีข้อมูลของ Postgres จึงไม่ใช่การสำรองข้อมูล

DB_DATA_LOCATION ดูเหมือนจะเป็นเป้าหมายที่ทำได้ง่าย มันเป็นไดเรกทอรี และ rsync จะคัดลอกมัน และการคัดลอกจะเสร็จสิ้นโดยไม่มีข้อผิดพลาด หากคุณคัดลอกในขณะที่ Postgres กำลังทำงานอยู่ การคัดลอกนั้นไม่ใช่การสำรองข้อมูล หากคุณคัดลอกในขณะที่ Postgres หยุดทำงานอยู่ มันจะเป็นการสำรองข้อมูล แต่เป็นข้อมูลที่เปราะบาง ต่อไปนี้คือเหตุผลสองประการ และคุณสามารถเฝ้าดูความล้มเหลวของทั้งสองกรณีได้

ประการแรกคือการเกิด tearing (ข้อมูลไม่สอดคล้องกัน) Postgres จะเขียนการเปลี่ยนแปลงทุกอย่างลงใน write-ahead log (WAL) ก่อน จากนั้นจึงนำไปใช้กับไฟล์ตารางในภายหลัง ณ จุด checkpoint ดังนั้นในทุกขณะ ไฟล์บนดิสก์จึงอยู่ในสถานะระหว่างการเปลี่ยนแปลง และการคัดลอกแบบต่อเนื่องที่ใช้เวลาสี่นาทีจะอ่านไฟล์แรกที่เวลา 02:00 และไฟล์สุดท้ายที่ 02:04 ไฟล์ทั้งสองนั้นไม่ได้เป็นส่วนหนึ่งของธุรกรรมเดียวกัน เมื่อคุณเริ่มการทำงานของ Postgres บนผลลัพธ์ดังกล่าว มันจะปฏิเสธการเริ่มทำงานด้วย PANIC: could not locate a valid checkpoint record หรือมันอาจเริ่มทำงานได้แต่จะหยุดทำงานทันทีเมื่ออ่านหน้าข้อมูลที่เสียหายด้วย invalid page in block 1234 of relation base/16384/... ซึ่งทั้งสองกรณีไม่สามารถกู้คืนได้จากสำเนาชุดนั้น

ประการที่สองคือเรื่องของการคัดลอกที่คุณทำในขณะที่ทุกอย่างหยุดทำงาน PostgreSQL ระบุว่าการคัดลอกไดเรกทอรีข้อมูลที่ปิดการทำงานอย่างสมบูรณ์ถือเป็นการสำรองข้อมูลที่ถูกต้อง แต่ไดเรกทอรีนั้นจะเปิดได้เฉพาะภายใต้ไบนารีที่ใช้รูปแบบการจัดเก็บข้อมูลเดียวกันเท่านั้น Immich กำหนดอิมเมจฐานข้อมูลไว้ด้วย digest ซึ่งปัจจุบันคือ ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0 นั่นคือ Postgres 14 ที่มีการติดตั้งส่วนขยาย VectorChord, pgvector และ pgvecto.rs สำหรับการค้นหาแบบเวกเตอร์มาด้วย การอัปเดต Postgres 14 เวอร์ชันย่อยในภายหลังอาจเปิดไดเรกทอรีเดิมได้ แต่ Postgres เวอร์ชันหลักที่ต่างออกไปจะไม่สามารถเปิดได้ นอกจากนี้ ส่วนขยายที่ตารางของคุณใช้งานอยู่จะต้องมีอยู่และเป็นเวอร์ชันที่เข้ากันได้ด้วย โฮสต์ที่คุณใช้กู้คืนจะต้องรันอิมเมจที่เข้ากันได้ ในขณะที่ SQL dump ไม่ต้องกังวลเรื่องนี้ เพราะมันเป็นข้อความ และเซิร์ฟเวอร์ใดก็ตามที่เข้ากันได้สามารถนำไปประมวลผลใหม่ได้

pg_dump ช่วยหลีกเลี่ยงปัญหาเรื่อง tearing ได้โดยตรง มันจะอ่านฐานข้อมูลทั้งหมดภายใน snapshot เดียวของ MVCC (multi-version concurrency control) ดังนั้นมันจึงเห็นฐานข้อมูลในสถานะที่ตรงกับช่วงเวลาหนึ่งอย่างแม่นยำ ในขณะที่การเขียนข้อมูลอื่นๆ ยังคงดำเนินต่อไปรอบๆ นั่นคือเหตุผลที่คุณไม่จำเป็นต้องหยุดการทำงานของ Postgres เพื่อทำการ dump ข้อมูล

สิ่งที่ไม่จำเป็นต้องรวมในข้อมูลสำรอง

ส่วนต่อไปนี้สร้างใหม่ได้ จึงอาจข้ามได้:

  • thumbs: ภาพ preview และ thumbnail
  • encoded-video: วิดีโอที่ผ่านการ transcode
  • DB_DATA_LOCATION: สร้างใหม่จาก dump
  • Docker volume model-cache: machine learning model ซึ่งจะดาวน์โหลดใหม่เมื่อจำเป็น

การข้ามส่วนเหล่านี้เป็นการแลกเปลี่ยน ไม่ใช่ประโยชน์ที่ได้มาโดยไม่มีต้นทุน การสร้าง thumbnail และ transcode ใหม่สำหรับ library ขนาดใหญ่ใช้ CPU หลายชั่วโมงบน VPS ขนาดเล็ก และ timeline จะแสดง placeholder สีเทาตลอดช่วงเวลานั้น คุณสั่งทำซ้ำได้จาก Administration > Jobs โดยตั้งค่า "Generate Thumbnails" และ "Transcode Videos" ให้ทำงานกับ asset ที่ยังขาด หากปลายทางของข้อมูลสำรองมีพื้นที่ว่าง ให้รวมส่วนเหล่านี้ไว้เพื่อไม่ต้องรอ หากพื้นที่จัดเก็บใกล้เต็ม ให้ตัดส่วนเหล่านี้ออกและวางแผนสร้างใหม่ การประเมินขนาด library ของ Immich อธิบายว่า folder เหล่านี้จะมีขนาดเพิ่มขึ้นเมื่อเทียบกับไฟล์ต้นฉบับมากเพียงใด

ยังมี folder อีกหนึ่งรายการที่ควรรู้จัก UPLOAD_LOCATION/backups เก็บ database dump อัตโนมัติของ Immich ซึ่งเขียนทุกวันที่ 02:00 และเก็บรายการล่าสุดไว้ 14 รายการ โดยกำหนดค่าได้ที่ Administration > Settings > Database Dump Settings ไฟล์เหล่านี้ไม่เสียค่าใช้จ่ายเพิ่มเติมและมีประโยชน์จริง แต่จัดเก็บอยู่บนดิสก์เดียวกับ library ที่ไฟล์เหล่านี้ปกป้อง จึงช่วยกรณี migration ผิดพลาด แต่ไม่ช่วยกรณีเซิร์ฟเวอร์เสียหาย ควรทำ dump ของคุณเองด้วย เพราะ dump ที่คุณสั่งทำจะเกิดขึ้นในเวลาเดียวกับ file snapshot ที่สอดคล้องกัน

การสำรองข้อมูลฐานข้อมูล

docker exec -t immich_postgres pg_dump --clean --if-exists \
  --dbname=immich --username=postgres \
  | gzip > /srv/immich/backup/immich.sql.gz

ให้แทนที่ immich และ postgres ด้วย DB_DATABASE_NAME และ DB_USERNAME ของคุณหากมีการเปลี่ยนแปลง --clean --if-exists จะเพิ่ม DROP ... IF EXISTS ไว้หน้า CREATE ทุกรายการ เพื่อให้การนำข้อมูลสำรองกลับเข้าสู่ฐานข้อมูล (restore) สามารถดำเนินการต่อได้แม้จะมีออบเจกต์เดิมอยู่แล้ว แทนที่จะหยุดทำงานตั้งแต่รายการแรก

รายละเอียดที่มักทำให้สคริปต์สำรองข้อมูลล้มเหลวโดยไม่แจ้งเตือนคือ คำสั่งดังกล่าวเป็น pipeline ซึ่ง shell จะรายงานสถานะการทำงานของคำสั่ง สุดท้าย ใน pipeline เท่านั้น หาก pg_dump ล้มเหลวเนื่องจากรหัสผ่านผิดหรือคอนเทนเนอร์ไม่ได้ทำงาน gzip จะได้รับข้อมูลว่างเปล่าและสร้างไฟล์ gzip ที่สมบูรณ์ออกมาพร้อม exit status เป็น 0 สคริปต์ของคุณจะบันทึกว่าสำเร็จทั้งที่ได้ไฟล์สำรองขนาดเพียง 20-byte ให้ใส่ pipefail ไว้ที่ส่วนบนสุดของสคริปต์สำรองข้อมูลทุกฉบับ:

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

จากนั้นให้ตรวจสอบผลลัพธ์แทนการเชื่อถือ exit code เพียงอย่างเดียว:

ls -lh /srv/immich/backup/immich.sql.gz
gunzip -c /srv/immich/backup/immich.sql.gz | head -n 3

บรรทัดแรกของไฟล์สำรองข้อมูลที่สมบูรณ์ควรขึ้นต้นด้วย -- PostgreSQL database dump หากไฟล์มีขนาดเพียงไม่กี่ร้อย byte แสดงว่าการสำรองข้อมูลล้มเหลว ไม่ว่าสคริปต์จะรายงานอย่างไรก็ตาม

ให้บันทึกเวอร์ชันของ release ที่ใช้สร้างไฟล์สำรองข้อมูลไว้คู่กัน โดยสอบถามจากเซิร์ฟเวอร์ที่กำลังทำงานอยู่:

curl -fsS http://localhost:2283/api/server/version > /srv/immich/backup/immich-version.txt

endpoint ดังกล่าวไม่จำเป็นต้องเข้าสู่ระบบและจะส่งค่า release ที่กำลังทำงานอยู่กลับมาเป็นเลข major, minor และ patch ดังนั้น 3, 1 และ 0 จึงหมายถึง v3.1.0 อย่าพึ่งพา .env ในการตรวจสอบนี้ หรือใช้ docker inspect --format '{{.Config.Image}}' immich_server ซึ่งจะแสดงเพียง image reference ที่คุณตั้งค่าไว้เท่านั้น ไฟล์มาตรฐานจะกำหนด IMMICH_VERSION=v3 ซึ่งเป็น floating tag ที่ติดตามทุก release ในกลุ่ม 3.x ดังนั้นบันทึกที่ลงท้ายด้วย :v3 จะไม่สามารถระบุได้ว่า build ใดเป็นผู้สร้างไฟล์สำรองข้อมูลนั้น ให้ระบุ tag ที่แน่นอนไว้ใน .env ด้วยเช่นกัน

หยุดการทำงานของเซิร์ฟเวอร์แล้วทำ snapshot ด้วย restic

ไฟล์ภายใต้ UPLOAD_LOCATION จะไม่คงที่ในขณะที่ Immich ทำงานอยู่ เซิร์ฟเวอร์จะเขียนไฟล์อัปโหลดใหม่และงานจัดเก็บข้อมูลตามเทมเพลตจะย้ายไฟล์ไปมาระหว่างไดเรกทอรี หากเครื่องมือสำรองข้อมูลอ่านไฟล์ในขณะที่กำลังเขียนอยู่ เครื่องมือจะจัดเก็บไบต์เหล่านั้นเสมือนว่าเป็นไฟล์ที่สมบูรณ์โดยไม่มีการแจ้งเตือนข้อผิดพลาดใดๆ ให้หยุดคอนเทนเนอร์ของเซิร์ฟเวอร์ตลอดระยะเวลาที่ทำการสำรองข้อมูล:

docker stop immich_server

ให้ปล่อย immich_postgres ทำงานต่อไปเนื่องจากกระบวนการ dump ข้อมูลจำเป็นต้องใช้ อินเทอร์เฟซเว็บและแอปมือถือจะออฟไลน์จนกว่าคุณจะเริ่มเซิร์ฟเวอร์ใหม่อีกครั้ง ซึ่งโดยปกติแล้วเวลา 03:00 น. สำหรับการใช้งานในบ้านถือว่าไม่มีปัญหา

restic เหมาะสมกับงานนี้เพราะสามารถทำ deduplication และเข้ารหัสข้อมูลก่อนที่จะส่งออกจากเครื่อง ให้ชี้เป้าหมายไปยัง repository ที่ไม่ได้อยู่บนเซิร์ฟเวอร์นี้:

export RESTIC_REPOSITORY=sftp:backup@backup.example.com:/srv/restic/immich
export RESTIC_PASSWORD_FILE=/root/.restic-password
restic init

การใช้ object storage ก็มีหลักการเดียวกันและเป็นทางเลือกที่ดีกว่าหากคุณต้องการสำรองข้อมูลไว้นอกฮาร์ดแวร์ของคุณโดยสมบูรณ์:

export RESTIC_REPOSITORY=s3:https://s3.example.com/immich-backup
export AWS_ACCESS_KEY_ID=your-access-key
export AWS_SECRET_ACCESS_KEY=your-secret-key
restic init

endpoint ดังกล่าวสามารถเป็น MinIO bucket ที่คุณรันเอง บนเครื่องที่สอง หรือผู้ให้บริการที่รองรับ S3 รายใดก็ได้ การเก็บ repository ไว้บนดิสก์ลูกเดียวกับคลังข้อมูลจะช่วยป้องกันได้เพียงการลบไฟล์โดยไม่ตั้งใจเท่านั้น แต่ไม่สามารถป้องกันความเสียหายในกรณีอื่นได้

จากนั้นให้ทำ snapshot โดยระบุสิ่งที่สำคัญอย่างชัดเจน:

restic backup \
  /srv/immich/backup/immich.sql.gz \
  /srv/immich/backup/immich-version.txt \
  /srv/immich/data/library \
  /srv/immich/data/upload \
  /srv/immich/data/profile \
  /srv/immich/.env \
  /srv/immich/docker-compose.yml
docker start immich_server

restic จะอ่านโครงสร้างไฟล์ทั้งหมดในทุกครั้งที่รัน แต่จะอัปโหลดเฉพาะบล็อกข้อมูลที่ไม่เคยพบมาก่อน ดังนั้น snapshot แรกจะย้ายคลังข้อมูลทั้งหมดของคุณ และ snapshot หลังจากนั้นจะย้ายเฉพาะรูปภาพใหม่ของแต่ละวันเท่านั้น

การเก็บรักษาและกุญแจที่ต้องเก็บไว้ที่อื่น

restic forget --prune --keep-daily 7 --keep-weekly 5 --keep-monthly 12

forget จะลบ snapshot ออกจากดัชนี ส่วน --prune คือส่วนที่ทำหน้าที่ลบข้อมูลที่ snapshot เหล่านั้นเคยอ้างอิงเป็นลำดับสุดท้าย หากคุณรัน forget โดยไม่มี --prune ค่าใช้จ่ายในการจัดเก็บข้อมูลของคุณจะไม่ลดลง

การตรวจสอบโครงสร้างใช้ทรัพยากรน้อย ดังนั้นควรทำสัปดาห์ละครั้ง:

restic check

คำสั่งนี้จะตรวจสอบว่า metadata ของ repository มีความสอดคล้องกัน โดยไม่ได้อ่านข้อมูลจริงของคุณ เดือนละครั้ง ให้ลองอ่านข้อมูลตัวอย่างแล้วตรวจสอบเทียบกับค่า hash ที่บันทึกไว้:

restic check --read-data-subset=5%

นี่คือการตรวจสอบเพียงวิธีเดียวที่สามารถตรวจพบความเสียหายของข้อมูลที่เกิดขึ้นเงียบๆ บน storage backend ได้ เนื่องจากเป็นการดาวน์โหลดบล็อกข้อมูลจริงมาคำนวณ checksum ใหม่ การรัน --read-data เต็มรูปแบบกับคลังรูปภาพหมายถึงการดาวน์โหลด repository ทั้งหมด ซึ่งหากเป็น object storage ที่คิดค่าบริการตามปริมาณการใช้งาน จะทำให้เสียค่าใช้จ่ายจริง ดังนั้นการสุ่มตรวจสอบแบบหมุนเวียนจึงเป็นวิธีที่ผู้ใช้งานจริงนิยมทำกัน

มาถึงส่วนที่คนมักจะข้ามไป รหัสผ่านของ restic repository ไม่สามารถกู้คืนได้ ไม่มีการรีเซ็ตรหัสผ่านและไม่มีช่องทางสนับสนุนใดๆ หากสำเนาเพียงชุดเดียวอยู่ใน /root/.restic-password บนเซิร์ฟเวอร์ที่คุณกำลังพยายามกู้คืน ข้อมูลสำรองของคุณก็จะเป็นเพียงข้อมูลที่ถูกเข้ารหัสจนอ่านไม่ออก เช่นเดียวกับ access key ของ object storage และ DB_PASSWORD จาก .env ให้เก็บข้อมูลทั้งหมดนี้ไว้ในที่ที่ไม่ขึ้นอยู่กับการทำงานของเครื่องนี้ เช่น พิมพ์ออกมาเก็บไว้ในลิ้นชัก หรือเก็บไว้ในโปรแกรมจัดการรหัสผ่านที่รันบนฮาร์ดแวร์อื่น หากโปรแกรมจัดการรหัสผ่านนั้นเป็นแบบ self-hosted เช่นกัน ก็ต้องใช้วิธีการเดียวกัน และการ สำรองข้อมูล Vaultwarden ก็ถือเป็นงานที่ต้องทำแยกต่างหาก

การกู้คืน Immich ตามลำดับที่ถูกต้อง

ลำดับการกู้คืนคือจุดที่การสำรองข้อมูลที่ดีจะกลายเป็น timeline ว่างเปล่า ให้ทำตามลำดับนี้บนโฮสต์ใหม่

กู้คืนไฟล์ config ก่อน ไฟล์นี้จะบอกคุณว่าต้องรันเวอร์ชันใดและ path ต่างๆ ชี้ไปที่ไหน

restic restore latest --target /restore \
  --include /srv/immich/.env \
  --include /srv/immich/docker-compose.yml \
  --include /srv/immich/backup

กำหนดเวอร์ชันให้คงที่ก่อนเริ่มทำงาน อ่าน immich-version.txt, ตั้งค่า IMMICH_VERSION ใน .env ให้เป็นรุ่นนั้น (v3.1.0 สำหรับ major 3, minor 1, patch 0) และอย่าเพิ่งใช้รุ่นใหม่ล่าสุดในตอนนี้ Immich ไม่รองรับการดาวน์เกรด แม้แต่ระหว่างรุ่น patch ดังนั้นหากเซิร์ฟเวอร์รุ่นใหม่เริ่มทำงานกับ dump รุ่นเก่าและรันการ migration แล้ว จะไม่สามารถย้อนกลับได้

กู้คืนสื่อบันทึกข้อมูล

restic restore latest --target /restore --include /srv/immich/data

จากนั้นย้าย library, upload และ profile ไปไว้ข้างในตำแหน่งที่ UPLOAD_LOCATION ชี้ไปบนโฮสต์นี้ ตัว path บนโฮสต์สามารถเปลี่ยนได้เพราะไฟล์ compose จะผูก directory นั้นเข้ากับ path คงที่ภายใน container แต่โครงสร้างภายในห้ามเปลี่ยน

เริ่มฐานข้อมูลแยกต่างหาก ปล่อยให้ DB_DATA_LOCATION ว่างเปล่าเพื่อให้ Postgres เริ่มต้น cluster ใหม่

cd /srv/immich
docker compose pull
docker compose create
docker start immich_postgres
docker exec immich_postgres pg_isready --username=postgres

pg_isready จะแสดง accepting connections เมื่อการตั้งค่าครั้งแรกเสร็จสิ้น ซึ่งใช้เวลาไม่กี่วินาที docker compose create จะสร้าง container ทุกตัวโดยไม่เริ่มทำงาน ซึ่งเป็นจุดประสงค์หลักของขั้นตอนนี้: เซิร์ฟเวอร์ Immich ต้องยังไม่ทำงาน เซิร์ฟเวอร์ที่เริ่มทำงานกับฐานข้อมูลว่างเปล่าจะรันการ migration, สร้าง schema ใหม่ และขอให้คุณสร้างบัญชีผู้ใช้ admin ใหม่ ซึ่งจะทำให้คุณกำลังนำ dump ไปใส่ทับแอปพลิเคชันที่กำลังทำงานอยู่

นำ dump กลับเข้าสู่ระบบ

gunzip --stdout /restore/srv/immich/backup/immich.sql.gz \
  | sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" \
  | docker exec -i immich_postgres psql --dbname=immich --username=postgres \
      --single-transaction --set ON_ERROR_STOP=on

คำสั่งสองส่วนนั้นกำลังทำงานจริง sed มีอยู่เพราะ pg_dump เขียน search_path ว่างเปล่าลงใน output เพื่อเป็นมาตรการความปลอดภัย ดังนั้นชื่อที่ไม่มีการระบุ schema ใน dump จะไม่สามารถอ้างอิงไปยัง schema ที่ไม่คาดคิดได้ ประเภทข้อมูล vector-search ของ Immich อยู่ใน public ดังนั้นหาก search path ว่างเปล่า การกู้คืนจะไปถึงคอลัมน์แรกที่ประกาศด้วยประเภท vector และ psql จะหยุดทำงานพร้อมกับ ERROR: type "vector" does not exist การใส่ public กลับเข้าไปใน path จะช่วยแก้ไขปัญหานี้

--single-transaction --set ON_ERROR_STOP=on จะรวมการกู้คืนทั้งหมดไว้ใน transaction เดียวที่จะยกเลิกหากเกิดข้อผิดพลาดแรก คุณจะได้ฐานข้อมูลที่สมบูรณ์หรือฐานข้อมูลที่ไม่ได้ถูกแตะต้อง หากไม่มีคำสั่งนี้ ความล้มเหลวระหว่างทางจะทำให้ได้ฐานข้อมูลที่เริ่มทำงานได้, ล็อกอินได้, แต่ขาดอัลบั้มจำนวนหนึ่งไปโดยไม่ทราบสาเหตุ ซึ่งคุณจะพบในอีกหลายสัปดาห์ต่อมา

เริ่มทำงานทุกอย่าง

docker compose up -d
docker compose ps
docker logs -f immich_server

รอจนกว่าจะมีบรรทัดสถานะการเริ่มทำงานเช่น Immich Server is listening on จากนั้นเปิดพอร์ต 2283 และล็อกอินด้วยข้อมูลเดิมของคุณ เพราะบัญชีผู้ใช้จะกลับมาพร้อมกับ dump หากหน้าล็อกอินเสนอให้สร้างบัญชี admin ใหม่ แสดงว่าการกู้คืนฐานข้อมูลไม่สำเร็จ ให้หยุดและอ่าน output ของ psql อีกครั้ง

คำเตือนเกี่ยวกับคำสั่งกู้คืนผ่าน command-line อย่างเป็นทางการ ซึ่งขึ้นต้นด้วย docker compose down -v คำสั่ง -v จะลบ named volumes ในไฟล์ compose มาตรฐาน UPLOAD_LOCATION และ DB_DATA_LOCATION เป็น bind mounts จึงไม่ถูกลบ หากคุณเปลี่ยนส่วนใดส่วนหนึ่งเป็น named volume คำสั่งนั้นจะลบรูปภาพของคุณ ให้ตรวจสอบไฟล์ compose ของคุณก่อนพิมพ์คำสั่งดังกล่าว

เหตุใดเส้นเวลา (timeline) จึงว่างเปล่าหลังจากการกู้คืนข้อมูล

เส้นเวลาถูกสร้างขึ้นจากแถวข้อมูลในฐานข้อมูล Immich จะไม่ทำการสแกน upload/ ในขณะบูตเพื่อค้นหารูปภาพใหม่ เนื่องจากไฟล์ที่ไม่มีแถวข้อมูลในฐานข้อมูลจะไม่มีเจ้าของ ไม่มีวันที่ และไม่มีอัลบั้ม ดังนั้นข้อผิดพลาดที่พบบ่อยที่สุดในการกู้คืนคือการนำไฟล์กลับมาได้แต่ฐานข้อมูลหายไป Immich จะเริ่มทำงาน สร้าง schema เปล่าขึ้นมา และให้คุณใช้งาน instance ที่ว่างเปล่าในขณะที่ดิสก์เต็มไปด้วยรูปภาพของคุณ ข้อมูลไม่ได้สูญหายไปไหน แต่ไม่มีสิ่งใดแสดงขึ้นมา วิธีแก้ไขคือการนำไฟล์ dump กลับเข้าไปใหม่ในขณะที่หยุดการทำงานของเซิร์ฟเวอร์ไว้ ตามขั้นตอนที่ระบุไว้ข้างต้น

กรณีที่สองคือสถานการณ์ที่เงียบกว่า ฐานข้อมูลถูกกู้คืน เส้นเวลาแสดงรายการข้อมูลครบถ้วน แต่ไม่สามารถเปิดไฟล์รูปภาพได้ นั่นหมายความว่าแถวข้อมูลชี้ไปยังไฟล์ที่คอนเทนเนอร์มองไม่เห็น ซึ่งมักเกิดจาก library, upload และ profile อยู่ลึกเกินไปหนึ่งระดับหลังจากทำการ restic restore --target /restore โดยที่ไม่มีการย้ายไฟล์ไปยังตำแหน่งที่ถูกต้อง ให้ตรวจสอบจากภายในคอนเทนเนอร์แทนการคาดเดา:

docker exec immich_server ls /data

ไฟล์ compose มาตรฐานจะ mount UPLOAD_LOCATION ไว้ที่ /data ดังนั้นรายการที่แสดงควรปรากฏ library, upload และ profile หากแสดงผลเป็นไดเรกทอรีว่างหรือมีโฟลเดอร์ srv โผล่มา แสดงว่า bind mount ของคุณชี้ไปยังระดับที่ไม่ถูกต้อง แต่ข้อมูลในแถวฐานข้อมูลนั้นถูกต้องแล้ว

การจับคู่เวอร์ชันระหว่างการสำรองข้อมูลและการกู้คืน

Immich มีการออกรุ่นบ่อยครั้งและโครงสร้างฐานข้อมูล (schema) ก็เปลี่ยนแปลงตามไปด้วย ดังนั้นไฟล์ dump จึงมีโครงสร้างของเซิร์ฟเวอร์เวอร์ชันที่สร้างไฟล์นั้นขึ้นมา

การกู้คืนไฟล์ dump เวอร์ชันเก่าลงในเซิร์ฟเวอร์เวอร์ชันใหม่มักจะทำได้สำเร็จ เนื่องจากเซิร์ฟเวอร์จะทำการรัน migration ที่ค้างอยู่เมื่อเริ่มต้นระบบและปรับโครงสร้างฐานข้อมูลให้เป็นปัจจุบัน เส้นทางการอัปเกรดนี้ได้รับการทดสอบมาตลอดลำดับการออกรุ่น แต่การข้ามเวอร์ชันหลักหลายเวอร์ชันในคราวเดียวมักจะทำให้เกิดปัญหา ซึ่งโครงการได้ระบุการเปลี่ยนแปลงที่อาจส่งผลกระทบ (breaking changes) ไว้ในรุ่นหลักและบันทึกไว้ใน changelog แล้ว

การกู้คืนไฟล์ dump เวอร์ชันใหม่ลงในเซิร์ฟเวอร์เวอร์ชันเก่าไม่สามารถทำได้ เนื่องจากไฟล์ dump มีตารางและคอลัมน์ที่โค้ดเวอร์ชันเก่าไม่รู้จัก และ Immich ระบุว่าไม่รองรับการลดเวอร์ชัน (downgrade) แม้แต่ระหว่างรุ่นย่อย (patch releases) ทั้งนี้ไม่มีคำสั่ง rollback ให้ใช้งาน

ดังนั้น วิธีการกู้คืนที่ปลอดภัยคือวิธีที่เรียบง่าย ให้รัน Immich เวอร์ชันเดียวกับที่สร้างไฟล์ dump นั้นขึ้นมา ทำการกู้คืนข้อมูล ล็อกอินเข้าสู่ระบบ ตรวจสอบว่า timeline ข้อมูลครบถ้วน แล้วจึงค่อยดำเนินการอัปเกรดหลังจากนั้น โดยให้อัปเกรดทีละรุ่น พร้อมกับปรับเวอร์ชันใน IMMICH_VERSION และรัน docker compose pull && docker compose up -d หลังจากอัปเกรดแต่ละครั้ง การเก็บไฟล์ dump ย้อนหลังไว้หนึ่งสัปดาห์จะช่วยได้มากในกรณีนี้ หากพบว่าไฟล์ dump ล่าสุดถูกสร้างขึ้นในระหว่างการอัปเกรดที่ล้มเหลว คุณก็ยังมีไฟล์ของเมื่อวานอยู่ใน repository ให้ใช้งาน

ตรวจสอบข้อมูลสำรองทุกเดือน

ข้อมูลสำรองที่คุณไม่เคยทดสอบกู้คืนถือเป็นเพียงการคาดเดาเท่านั้น ให้ทำการกู้คืนข้อมูลลงใน instance ชั่วคราวเดือนละหนึ่งครั้งแล้วตรวจสอบรูปภาพ การซ้อมกู้คืนนี้ใช้เวลาประมาณยี่สิบนาที และเป็นสิ่งเดียวที่จะเปลี่ยนเนื้อหาทั้งหมดในหน้านี้ให้กลายเป็นแผนการกู้คืนระบบที่ใช้งานได้จริง

restic snapshots
restic stats latest

snapshots ควรแสดงรายการการทำงานของเมื่อคืนนี้ stats latest ควรรายงานขนาดที่ใกล้เคียงกับคลังข้อมูลของคุณ ไม่ใช่เพียงไม่กี่เมกะไบต์

ให้กู้คืนลงในไดเรกทอรีชั่วคราว โดยควรทำบนโฮสต์สำรอง:

restic restore latest --target /tmp/immich-drill

คัดลอก docker-compose.yml และ .env ออกมาจากชุดข้อมูลที่กู้คืนมา จากนั้นแก้ไขสามจุดในไฟล์สำเนา ให้ชี้ UPLOAD_LOCATION และ DB_DATA_LOCATION ไปยังไดเรกทอรีภายใต้ /tmp/immich-drill เปลี่ยนพอร์ตเว็บไปใช้พอร์ตอื่น เช่น 12283:2283 แทน 2283:2283 ลบบรรทัด container_name: ออก เนื่องจากไฟล์ compose มาตรฐานมีการระบุชื่อแบบ hard-code ไว้ เช่น immich_server ซึ่งจะทำให้ stack ที่สองบนโฮสต์เดียวกันเกิดความขัดแย้งกับ stack แรก และ Docker จะปฏิเสธการสร้างคอนเทนเนอร์

ดำเนินการตามขั้นตอนการกู้คืนจากด้านบน: กู้คืนเฉพาะฐานข้อมูล, replay ข้อมูล dump, แล้วตามด้วย docker compose up -d จากนั้นให้ทำการตรวจสอบสี่ประการเพื่อยืนยันความถูกต้อง:

  1. เข้าสู่ระบบด้วยรหัสผ่านที่คุณใช้ก่อนเริ่มการซ้อม หากเข้าสู่ระบบได้แสดงว่าการกู้คืนข้อมูล dump สำเร็จ
  2. เปิดไทม์ไลน์และเลื่อนไปยังเดือนที่เก่าที่สุด หากพบเนื้อหาตลอดช่วงเวลาทั้งหมด แสดงว่าข้อมูลทุกแถวถูกกู้คืนกลับมา ไม่ใช่แค่ข้อมูลล่าสุดเท่านั้น
  3. เปิดรูปภาพหนึ่งรูปที่ขนาดเต็มและดาวน์โหลดไฟล์ต้นฉบับออกมา
  4. เปรียบเทียบไฟล์นั้นกับไฟล์เดียวกันในคลังข้อมูลจริงของคุณด้วย sha256sum หากค่าแฮชตรงกัน แสดงว่าข้อมูลไบต์ทั้งหมดผ่านกระบวนการรับส่งผ่าน restic มาได้อย่างสมบูรณ์

จากนั้นให้ลบการซ้อมกู้คืนทิ้งด้วย docker compose down -v ในไดเรกทอรีที่ใช้ซ้อม และลบ /tmp/immich-drill บันทึกวันที่ไว้ในที่ที่คุณจะมองเห็นได้ชัดเจน เพราะคุณค่าของกระบวนการนี้อยู่ที่การทำซ้ำในเดือนถัดไป หากคุณยังตัดสินใจไม่ได้ว่าจะเลือกใช้เซิร์ฟเวอร์รูปภาพตัวใด การเปรียบเทียบระหว่าง PhotoPrism และ Immich จะครอบคลุมถึงความแตกต่างของทั้งสองตัวในประเด็นนี้โดยเฉพาะ

FAQ

ฉันจำเป็นต้องหยุดการทำงานของ Immich เพื่อสำรองข้อมูลหรือไม่?

ให้หยุด immich_server แต่ปล่อยให้ immich_postgres ทำงานต่อไป ฐานข้อมูลไม่จำเป็นต้องหยุดพักเนื่องจาก pg_dump จะอ่านข้อมูลภายใน MVCC snapshot ชุดเดียว ซึ่งจะเห็นสถานะที่สอดคล้องกัน ณ ขณะใดขณะหนึ่งโดยไม่สนใจว่าจะมีกระบวนการอื่นเขียนข้อมูลอยู่หรือไม่ สาเหตุที่ต้องหยุดการทำงานคือไฟล์ต่างๆ เนื่องจากเซิร์ฟเวอร์จะเขียนข้อมูลที่อัปโหลดเข้ามาใหม่ และงาน storage template จะย้ายไฟล์ระหว่างไดเรกทอรี ทำให้เครื่องมือสำรองข้อมูลอาจอ่านไฟล์ในขณะที่กำลังเขียนอยู่ และจัดเก็บไฟล์ที่ข้อมูลไม่ครบถ้วนโดยไม่แจ้งข้อผิดพลาด การใช้ docker stop immich_server ก่อนทำ snapshot และ docker start immich_server หลังจากนั้นจะช่วยป้องกันปัญหาความขัดแย้งนี้

ฉันสามารถคัดลอกโฟลเดอร์ข้อมูลของ Postgres แทนการใช้ pg_dump ได้หรือไม่?

ไม่ได้ในขณะที่ Postgres กำลังทำงานอยู่ การคัดลอกไดเรกทอรีข้อมูลแบบสดจะอ่านไฟล์ต่างๆ ในช่วงเวลาที่ต่างกัน ทำให้ผลลัพธ์ที่ได้ไม่มีสถานะที่สอดคล้องกัน และ Postgres จะปฏิเสธการทำงานเมื่อเริ่มระบบด้วยข้อผิดพลาด PANIC: could not locate a valid checkpoint record หรือล้มเหลวในภายหลังเนื่องจากหน้าข้อมูลเสียหาย การคัดลอกแบบสมบูรณ์ในขณะที่หยุด Postgres เป็นการสำรองข้อมูลที่ใช้ได้จริง แต่จะเปิดใช้งานได้เฉพาะกับ Postgres เวอร์ชันหลักเดิมที่มีส่วนขยายที่เข้ากันได้เท่านั้น Immich กำหนดให้ใช้ Postgres 14 image ที่มี VectorChord, pgvector และ pgvecto.rs ซึ่งไดเรกทอรีดังกล่าวจะไม่สามารถเปิดได้หากใช้เวอร์ชันหลักที่ต่างออกไป SQL dump เป็นข้อความธรรมดาและสามารถนำไปรันใหม่ในเซิร์ฟเวอร์ที่เข้ากันได้ ดังนั้นจึงยังคงเป็นสำเนาที่ควรเก็บรักษาไว้

ทำไมไทม์ไลน์ของ Immich ถึงว่างเปล่าหลังจากกู้คืนข้อมูล?

เนื่องจากไทม์ไลน์ถูกสร้างขึ้นจากแถวข้อมูลในฐานข้อมูล และคุณกู้คืนเฉพาะไฟล์โดยไม่มีฐานข้อมูล Immich จะไม่สแกน upload/ เพื่อค้นหารูปภาพใหม่ ดังนั้นไฟล์ที่ไม่มีแถวข้อมูลในฐานข้อมูลจะยังคงไม่ปรากฏให้เห็น ตัวรูปภาพเองนั้นไม่ได้ถูกแตะต้อง ให้หยุดเซิร์ฟเวอร์ นำ dump ไปรันใหม่ใน Postgres ที่เพิ่งเริ่มต้นใหม่ จากนั้นจึงเริ่ม stack หากไทม์ไลน์แสดงข้อมูลครบถ้วนแต่รูปภาพไม่สามารถเปิดได้ ปัญหาจะเป็นในทางกลับกัน คือ library, upload และ profile ไม่ได้อยู่ในไดเรกทอรีที่เชื่อมต่อ (bind) เข้ากับคอนเทนเนอร์โดยตรง ให้ตรวจสอบด้วย docker exec immich_server ls /data

โฟลเดอร์ใดของ Immich ที่ฉันสามารถข้ามไปไม่ต้องสำรองข้อมูลได้?

thumbs และ encoded-video สามารถสร้างใหม่ได้จากไฟล์ต้นฉบับ และ DB_DATA_LOCATION สามารถสร้างใหม่ได้จาก dump ดังนั้นจึงไม่จำเป็นต้องรวมโฟลเดอร์เหล่านี้ไว้ในชุดสำรองข้อมูล การข้ามโฟลเดอร์เหล่านี้จะทำให้ต้องเสียเวลาหลังจากกู้คืนข้อมูลแทนการประหยัดพื้นที่จัดเก็บก่อนสำรองข้อมูล เนื่องจากกระบวนการสร้างตัวอย่างภาพ (preview) และการแปลงไฟล์ (transcode) สำหรับคลังภาพขนาดใหญ่ต้องใช้เวลาประมวลผลหลายชั่วโมง โดยสั่งรันผ่าน Administration > Jobs สำหรับสินทรัพย์ที่ขาดหายไป สิ่งที่คุณห้ามข้ามเด็ดขาดคือ library, upload และ profile ซึ่งเป็นที่เก็บสำเนาเดียวของไฟล์ต้นฉบับทั้งหมด

ฉันสามารถกู้คืน Immich dump ไปยังเวอร์ชันที่ใหม่กว่าได้หรือไม่?

โดยปกติแล้วทำได้ เนื่องจากเซิร์ฟเวอร์จะใช้การย้ายข้อมูล (migration) ที่ค้างอยู่เมื่อเริ่มระบบและปรับปรุง schema ให้เป็นปัจจุบัน แต่ในทางกลับกันทำไม่ได้ Immich ไม่รองรับการลดเวอร์ชัน (downgrade) แม้แต่ระหว่างการอัปเดตย่อย ดังนั้น dump จากเวอร์ชันที่ใหม่กว่าจึงไม่สามารถโหลดลงในเซิร์ฟเวอร์ที่เก่ากว่าได้ ให้กู้คืนด้วย IMMICH_VERSION ที่ระบุเวอร์ชันเดียวกับที่สร้าง dump นั้น ตรวจสอบว่าไทม์ไลน์สมบูรณ์ แล้วจึงค่อยอัปเกรดในภายหลัง ให้บันทึกเวอร์ชันที่ใช้ควบคู่ไปกับ dump แต่ละชุดด้วย curl -fsS http://localhost:2283/api/server/version เนื่องจาก docker inspect จะแสดงเพียงค่าอ้างอิงของ image ที่ตั้งค่าไว้เท่านั้น และค่าเริ่มต้นของ IMMICH_VERSION=v3 เป็นแท็กแบบลอยตัวซึ่งไม่ได้ระบุข้อมูลเวอร์ชันที่ชัดเจนให้คุณทราบ