โปรเจกต์งานอดิเรก → เมื่อ NAS บูตทีหลัง
คู่มือ · Proxmox · Docker · systemd · กันยายน 2026

เมื่อ NAS บูตทีหลัง: ตัวเฝ้าการเมานต์สำหรับ Docker

ไฟดับ รีบูต แล้วกล่องเก็บข้อมูลก็ขึ้นมาช้ากว่าบริการที่พึ่งพามันหนึ่งนาที การเมานต์ NFS ล้มเหลวครั้งเดียวแล้วก็ล้มเหลวอยู่อย่างนั้น Docker ช่วยสร้างโฟลเดอร์ว่างไว้ตรงที่แชร์ควรอยู่ เซิร์ฟเวอร์รูปภาพไม่ยอมเริ่ม เซิร์ฟเวอร์ไฟล์ปิดตัว และเซิร์ฟเวอร์สื่อสแกนคลังว่างเปล่าแล้วเริ่มลืมสิ่งต่าง ๆ นี่คือวิธีแก้สี่ส่วนที่เราใช้บนเครื่อง AI ของเรา ปรับให้ใช้ทั่วไปจนวางลงบน Docker host ใดก็ได้ที่เมานต์จาก NAS

แนวทาง ก่อนดูโค้ด

รากของปัญหาคือจังหวะเวลา และจังหวะเวลาไม่เคยแก้ได้ด้วยการเปลี่ยนแปลงเดียว ไอเดียสี่ข้อรวมกันทำให้โฮสต์ไม่สนว่า NAS จะโผล่มาเมื่อไร:

ให้ NAS ออกตัวก่อน ถ้าเซิร์ฟเวอร์เก็บข้อมูลเป็น VM บนไฮเปอร์ไวเซอร์เดียวกัน ให้บอกไฮเปอร์ไวเซอร์ให้เริ่มมันก่อนและรอก่อนเริ่มอย่างอื่น NAS ไม่มี guest agent ที่จะบอกว่า “พร้อมแล้ว” การรอจึงเป็นจำนวนวินาทีตายตัว — นานพอให้อาร์เรย์ขึ้นมา

ลองซ้ำแทนการล้มเหลวครั้งเดียว การเมานต์เครือข่ายใน fstab ถูกพยายามตอนบูตแล้วไม่มีอีกเลย ตั้งค่า nofail ไว้เพื่อไม่ให้ NAS ที่ช้าทำให้การบูตแย่ลง แล้วให้ตัวจับเวลารายนาทีลองต่อไปเรื่อย ๆ จนสำเร็จ

ห้ามเริ่มบนจุดเมานต์เปล่าเด็ดขาด ช่วงเวลาอันตรายไม่ใช่ตอนเมานต์ล้มเหลว แต่คือตอนที่คอนเทนเนอร์เริ่มทำงานไปทั้งอย่างนั้น ไฟล์เครื่องหมายที่มีอยู่บน NAS เท่านั้น bind เข้าคอนเทนเนอร์เป็นไฟล์ที่ Compose ถูกห้ามสร้าง จะเปลี่ยน “แชร์หายไป” ให้เป็น “คอนเทนเนอร์จะไม่เริ่ม” — ซึ่งถูกต้องพอดี

บอกเมื่อมันเกิดขึ้น เมื่อตัวเฝ้าเมานต์ใหม่ มันจะรีสตาร์ตผู้ใช้งานและส่งหนึ่งบรรทัดไปยังช่องทางแจ้งเตือนของคุณ การกู้คืนแบบเงียบ ๆ คือบั๊กที่คุณไม่มีวันได้รู้

เคารพตัวป้องกันของแอปเอง Immich เขียนไฟล์เครื่องหมายซ่อน .immich ลงในทุกโฟลเดอร์ของมัน และไม่ยอมเริ่มถ้ามีอันไหนหายไป — การปฏิเสธนั้นคือสิ่งที่ดีที่สุดที่เกิดขึ้นในเหตุการณ์ของเรา เพราะมันหยุดเซิร์ฟเวอร์ไม่ให้เขียนไฟล์อัปโหลดลงดิสก์ผิดลูก อย่าตั้ง IMMICH_IGNORE_MOUNT_CHECK_ERRORS เพื่อให้ข้อความหายไป แก้การเมานต์

สิ่งที่ต้องมี

ขั้นที่ 1 — ให้ออกตัวก่อนในไฮเปอร์ไวเซอร์

Proxmox เริ่มเครื่องเสมือนตามลำดับ startup และเมื่อมี up= จะรอตามจำนวนวินาทีนั้นก่อนไปเครื่องถัดไป ให้ VM ของ NAS ลำดับต่ำและการหน่วงเวลาจริงจัง ส่วน Docker VM มาทีหลัง
บนโฮสต์ Proxmox (NAS คือ VM 109, Docker host คือ VM 104)
qm set 109 --onboot 1 --startup order=2,up=180
qm set 104 --onboot 1 --startup order=5

# see the whole boot sequence at a glance
for c in /etc/pve/qemu-server/*.conf; do
  printf "%-10s %s\n" "$(basename $c)" "$(grep -E '^(name|onboot|startup):' $c | tr '\n' ' ')"
done
การหน่วงเวลาคือการเดาว่าอาร์เรย์ใช้เวลานานเท่าไรกว่าจะขึ้น ไม่ใช่คำสัญญา นั่นคือเหตุผลที่มันเป็นแค่ขั้นแรก ตรวจด้วยว่า NAS เองเริ่มอาร์เรย์อัตโนมัติ — บน Unraid คือ Settings → Disk Settings → Enable auto start และต้องใช้ไลเซนส์แบบจ่ายเงิน: คีย์ทดลองจะรอให้คนมากด

ขั้นที่ 2 — การเมานต์ที่ไม่ยอมแพ้

nofail กันไม่ให้แชร์ที่หายไปทำให้การบูตแย่ลง x-systemd.mount-timeout กันไม่ให้มันถ่วงการบูตด้วย การลองซ้ำอยู่ในขั้นที่ 3 ไม่ใช่ที่นี่ — fstab ไม่มีการลองซ้ำ
/etc/fstab บน Docker host
192.168.1.10:/mnt/user/photos /mnt/photos nfs rw,soft,timeo=150,_netdev,nofail,x-systemd.mount-timeout=30 0 0
192.168.1.10:/mnt/user/media  /mnt/media  nfs rw,soft,timeo=150,_netdev,nofail,x-systemd.mount-timeout=30 0 0
192.168.1.10:/mnt/user/files  /mnt/files  nfs rw,soft,timeo=150,_netdev,nofail,x-systemd.mount-timeout=30 0 0
soft หรือ hard? แชร์เหล่านี้เป็นแชร์สำหรับเดโมที่อยู่หลังแอปซึ่งตรวจการเขียนของตัวเอง soft พร้อม timeo ที่มีขอบเขตจึงเป็นตัวเลือกที่ซื่อตรง — คำขอที่ค้างจะคืนข้อผิดพลาดแทนการแขวนคอนเทนเนอร์ไว้ตลอดไป สำหรับแชร์ที่รับไฟล์อัปโหลดที่ไม่มีทางหาใหม่ได้และไม่มีการตรวจระดับแอป ให้เลือก hard และยอมรับว่า NAS ที่ตายจะทำให้ตัวเขียนค้าง

ขั้นที่ 3 — ตัวเฝ้ารายนาที

ตัวจับเวลาระดับ root ที่เมานต์ใหม่สิ่งที่หายไป ล้างโฟลเดอร์ว่างที่ Docker สร้างไว้ตอน NAS ไม่อยู่ รีสตาร์ตผู้ใช้งานเฉพาะเมื่อมันเมานต์อะไรใหม่จริง และบอกเรื่องนั้นครั้งเดียว
/usr/local/sbin/nfs-guard.sh
#!/bin/bash
# Remount lost NAS shares; restart their consumers only when a remount actually
# happened; say so once. Runs as root, every minute.
changed=""
for m in photos media files; do
  if ! mountpoint -q /mnt/$m; then
    # An unmounted mountpoint may hold directories Docker auto-created while the
    # NAS was away. They would be hidden under the mount later — clear them so the
    # state stays honest and nothing ever lands on the local disk by mistake.
    find /mnt/$m -mindepth 1 -maxdepth 1 -exec rm -rf {} + 2>/dev/null
    mount /mnt/$m 2>/dev/null && changed="$changed $m"
  fi
done
if [ -n "$changed" ]; then
  docker restart immich_server jellyfin >/dev/null 2>&1
  docker start opencloud >/dev/null 2>&1
  # swap for your notifier of choice
  sudo -u duncan -H /home/duncan/scripts/tg-send.sh \
    "🔁 nfs-guard remounted:$changed — consumers restarted (the NAS had been away)." \
    >/dev/null 2>&1 || true
  logger -t nfs-guard "remounted:$changed; consumers restarted"
fi
exit 0
/etc/systemd/system/nfs-guard.service และ nfs-guard.timer
# nfs-guard.service
[Unit]
Description=Remount NAS shares and restart their consumers if they were lost

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/nfs-guard.sh

# nfs-guard.timer
[Unit]
Description=Minutely NFS mount guard

[Timer]
OnBootSec=30s
OnUnitActiveSec=1min
AccuracySec=10s

[Install]
WantedBy=timers.target
ติดตั้งและเปิดใช้งาน
sudo chmod 755 /usr/local/sbin/nfs-guard.sh
sudo systemctl daemon-reload
sudo systemctl enable --now nfs-guard.timer
systemctl list-timers nfs-guard.timer --no-legend
ทำไมรายการรีสตาร์ตต้องระบุชัด bind mount เป็นของคอนเทนเนอร์ที่ถือมันอยู่เท่านั้น: เมื่อแชร์ถูกเมานต์ใหม่บนโฮสต์ คอนเทนเนอร์ที่รันอยู่ยังเห็นโฟลเดอร์ว่างที่มันเริ่มต้นด้วย การรีสตาร์ตคือทางเดียวที่จะส่งแชร์จริงให้มัน การระบุผู้ใช้งานตามชื่อทำให้ตัวเฝ้าไม่แตะอะไรที่ไม่ได้พึ่ง NAS — ในกรณีของเรา ไปป์ไลน์เสียงทั้งหมดรันบนเครื่องเดียวกันและต้องไม่ถูกรีสตาร์ตเพราะพื้นที่เก็บข้อมูลสะดุด

ขั้นที่ 4 — ไฟล์เครื่องหมายที่ไม่มีคอนเทนเนอร์ไหนเริ่มได้ถ้าไม่มีมัน

สร้างไฟล์เครื่องหมายเปล่าบนแต่ละแชร์ (มันอยู่บน NAS จึงหายไปพร้อมการเมานต์) แล้ว bind เข้าผู้ใช้งานทุกตัวเป็นไฟล์ ด้วย create_host_path: false Compose จะไม่ยอมสร้างไฟล์ที่หายไปขึ้นมาเอง คอนเทนเนอร์จึงเริ่มไม่ได้จนกว่าแชร์จะกลับมา — และการรีสตาร์ตของตัวเฝ้าจะพามันขึ้นมาทันทีที่แชร์กลับมา
ครั้งเดียว ขณะแชร์ถูกเมานต์อยู่
for m in photos media files; do sudo touch /mnt/$m/.on-nas; done
compose.yaml — ทุกเซอร์วิสที่อ่านจากแชร์ (แสดง Jellyfin เป็นตัวอย่าง)
services:
  jellyfin:
    image: jellyfin/jellyfin:latest
    container_name: jellyfin
    volumes:
      - ./config:/config
      - ./cache:/cache
      - /mnt/media:/media:ro
      # the gate: a file that exists only on the NAS, and that Compose may not create
      - type: bind
        source: /mnt/media/.on-nas
        target: /on-nas
        read_only: true
        bind:
          create_host_path: false
    restart: unless-stopped
Immich มาพร้อม docker-compose.yml ของตัวเองที่คุณไม่ควรแก้ ให้ใส่บล็อกเดียวกันไว้ใน docker-compose.override.yml ใต้ immich-server: — Compose จะรวมรายการ volume ให้

ตรวจสอบ

พิสูจน์ทีละชั้น แล้วถอดปลั๊กโดยตั้งใจ
บน Docker host
# exports appear only once the NAS array is up
showmount -e 192.168.1.10

# mounts, not directory listings — an empty local directory lists just fine
for m in photos media files; do mountpoint -q /mnt/$m && echo "/mnt/$m mounted" || echo "/mnt/$m MISSING"; done

# the gate is in place when every consumer shows the marker bind
for c in jellyfin opencloud immich_server; do
  docker inspect -f '{{.Name}}: {{range .Mounts}}{{if eq .Destination "/on-nas"}}{{.Source}}{{end}}{{end}}' $c
done

# the real test: stop the NAS array, watch the consumers refuse, start it, watch the guard
journalctl -t nfs-guard -f

บันทึกจากเหตุการณ์ที่จ่ายค่าบทเรียนนี้