EST. 2026 — บันทึกงานเซิร์ฟเวอร์จริง SYSADMIN · SECURITY · AI · DIRECTADMIN เล่มที่ 01
THE SYSADMIN ALMANAC
หน้าหลัก / Backup / วางระบบ Backup DirectAdmin ใหม่ด้วย NFS และ POSIX ACL จากงานจริง
Backup — Backup DA [1]

วางระบบ Backup DirectAdmin ใหม่ด้วย NFS และ POSIX ACL จากงานจริง

โดย Ton-Or ·28 พ.ค. 2026·อ่าน 13 นาที
วางระบบ Backup DirectAdmin ใหม่ด้วย NFS และ POSIX ACL จากงานจริง

ผมไม่ได้ออกแบบระบบนี้บนกระดาษทีเดียวจบ — มันค่อยๆ โตจากปัญหาที่เจอจริงทีละจุด กว่าจะกลายเป็น architecture ที่ผมกล้าปล่อยไว้ใน production. บทความนี้คือบันทึกทั้งทางที่ผิดและทางที่ถูก.

หลังจากย้าย DirectAdmin server จากเครื่องเก่าไปเครื่องใหม่ ผมตัดสินใจรื้อระบบ backup ใหม่ทั้งหมด. ตอนแรกผมเลือกแนวทาง "คงของเดิมไว้ก่อน เดี๋ยวค่อยปรับ" แล้วค่อยๆ ปรับตามปัญหาที่โผล่มาจริง จนสุดท้ายมันกลายเป็น architecture ที่ผมใช้อยู่ทุกวันนี้.

บทความนี้ผมเขียนเพื่อบันทึกตัว architecture เอง บวกกับเหตุผลเบื้องหลังการตัดสินใจแต่ละจุด และปัญหาที่เจอตอนลงมือทำจริง. ใครจะหยิบไปปรับใช้กับระบบตัวเองก็ทำได้ เพราะทุกอย่างผ่านการใช้งานจริงใน production มาแล้ว ไม่ใช่แค่ทฤษฎีบนกระดาษ.

ค่าจำลองทั้งหมด

ทุก hostname / IP / UID / วัน ในตัวอย่างเป็นค่าจำลอง ไม่ตรงกับระบบจริง. โครงสร้างเลียนแบบได้ แต่อย่าก๊อปวางใส่เครื่องตัวเองตรงๆ — ปรับ path / UID / IP ให้ตรงกับระบบของคุณก่อนเสมอ.

Placeholder ที่ผมใช้ในบทความนี้:

  • web1 = DA server (เครื่องที่รัน DirectAdmin)
  • bak1 = backup host (Proxmox + NFS server)
  • 192.168.1.10 = IP ของ web1
  • 192.168.1.20 = IP ของ bak1
  • 1500 = UID ของ DA admin user บน web1
  • 1700 = UID ของ backup user บน bak1

Phase 1 — ของเก่าใช้ได้ ก็คงไว้ก่อน

ตอน migrate เสร็จใหม่ๆ DA backup config ของเครื่องเก่ายังเป็นแบบ FTP push ไปยัง backup host. ระบบนั้นรันมาเป็นปี ผมรู้ว่ามัน work — เลยตั้งใจคงไว้ก่อน ไม่อยากเพิ่ม risk ในจังหวะ cutover ที่ window สั้นและทุกอย่างกำลังลุ้นอยู่.

Setup ตอนนั้นเรียบง่ายมาก:

  • DA Admin Backup ทำงานทุกวัน ตั้งแต่จันทร์ถึงเสาร์ ตอนกลางคืน
  • Push ผ่าน FTP ที่ port <port> ไปยัง bak1
  • backup user บน bak1 รับไฟล์ลงไว้ที่ /mnt/backup/dabak/admin/<dayname>/

ของผมแบ่งเป็น folder รายวัน — Monday, Tuesday ไล่ไปเรื่อยๆ — เป็น rolling 7 วัน เก็บทุก user ทุกวัน. ส่วน VM snapshot ระดับ host รันคู่ขนานต่างหาก ซึ่งผมจะเล่าในส่วนท้าย.

เหตุผลที่ผมเลือก backup ทุก user ทุกวันโดยไม่แบ่งกลุ่ม คือ disk ที่ bak1 ใหญ่พอ (compressed แล้วทุก user รวมกันราว 200 GB) และผมอยากให้ขั้นตอน recovery มันง่าย — เวลาเกิดเรื่อง ผมไม่อยากต้องมานั่งไล่ว่า "user คนนี้อยู่กลุ่มไหน วันไหน backup ล่าสุด". ทุก user ทุกวัน จบ.

Reality check — DA บอกว่า FTP information invalid

วันที่สองหลัง cutover ผมเปิด DA panel เข้าไปดู Admin Backup — status เป็น fail ทุก cron. UI โชว์ error สั้นๆ ว่า FTP information invalid.

ครั้งแรกผมคิดในใจว่า "เออ น่าจะแค่ password ผิด หรือไม่ก็ DNS / IP ที่เปลี่ยนตอน migrate". ตรวจครบทุกช่อง — ทุกอย่างถูกต้อง. ลอง manual FTP จาก command line ก็ login ผ่าน, ls ได้, upload ได้ตามปกติ.

แต่ DA ก็ยัง fail อยู่ดี. ผมสับสนอยู่หลายชั่วโมง.

สุดท้ายผมไปเจอว่ามันเป็นปัญหาสามชั้นซ้อนกัน — error message อันเดียวกำลังปกปิดสามสาเหตุพร้อมกัน. นี่คือเหตุผลที่ debug ยากมาก เพราะแก้ชั้นนึงแล้วยังไม่ผ่าน คนทั่วไปก็คิดว่าแก้ผิดทาง ทั้งที่จริงๆ แก้ถูกแล้ว แต่มันยังมีอีกสองชั้นรออยู่.

ชั้นที่ 1 — /home/tmp ไม่มี

DA backup process ใช้ /home/tmp เป็น scratch dir ตอนทำ repquota dump. DA installer รุ่นใหม่ไม่สร้าง dir นี้ให้ ในขณะที่รุ่นเก่าเคยสร้าง. ของผมเลยไม่มี.

error.log: Cannot find /home/tmp to use for temp repquota dump

วิธีแก้:

mkdir -p /home/tmp
chmod 1777 /home/tmp        # world-write + sticky bit

แก้แล้ว — ยังไม่ผ่าน. มีอีกสองชั้นรออยู่.

ชั้นที่ 2 — ProFTPD default ไม่ chroot

Debian ship ProFTPD config มาแบบที่ DefaultRoot ~ ถูก comment ออกไว้ (ขณะที่ CentOS ship มาแบบ enabled). พอ DA ส่ง login ผ่าน FTP เข้ามา user จะไปโผล่อยู่ใน path แบบ absolute ของ system filesystem แทนที่จะเป็น home dir ของตัวเอง. จากนั้น DA สั่ง CWD admin/ server ก็ไปเปิด /admin/ ที่ระดับ root filesystem — ซึ่งไม่มี — curl เลยจบด้วย exit code 9.

วิธีแก้คือ uncomment บรรทัด DefaultRoot ให้ chroot user เข้า home:

sed -i 's|^#\s*DefaultRoot\s*~|DefaultRoot ~|' /etc/proftpd/proftpd.conf
systemctl restart proftpd

ยังไม่ผ่านอีก — มีชั้นที่สาม.

ชั้นที่ 3 — CSF default OUTPUT DROP + DA spawn เป็น non-root

ชั้นนี้ลึกที่สุดและกินเวลาผมนานสุด. DA รุ่นใหม่เปลี่ยนวิธี spawn legacy-handler ของ backup task — มันรันผ่าน user diradmin (UID 999) และ nobody (UID 65534) แทนที่จะเป็น root อย่างที่รุ่นเก่าเคยทำ.

ตรงนี้ไปชนกับ CSF default policy ที่ตั้ง OUTPUT DROP สำหรับ non-root user (ซึ่งเป็น secure default ที่ดีนะ). ตัว curl ที่ DA spawn ออกไปยิง FTP เลยโดน drop packet ทิ้ง → timeout → curl จบด้วย exit code 28.

ของผมก่อนหน้านี้ใช้ DA รุ่นเก่าที่ spawn เป็น root จึงไม่เคยเจอปัญหานี้ — พอ DA ขยับมา defense in depth มากขึ้น มันก็เลยต้องการให้ CSF rule explicit ขึ้นด้วย.

วิธีแก้คือเปิด egress ให้ UID ทั้งสองนั้นออก port FTP ได้ ผ่าน csfpost hook:

# /etc/csf/csfpost.d/10-da-egress.sh
iptables -I OUTPUT -m owner --uid-owner 999  -p tcp --dport <port> -j ACCEPT
iptables -I OUTPUT -m owner --uid-owner 65534 -p tcp --dport <port> -j ACCEPT
csf -r

Debug strategy ที่ใช้ได้ผล

สิ่งที่ผมอยากให้ติดไม้ติดมือไปคือ — เวลาเจอ FTP information invalid บน DA รุ่นใหม่ๆ อย่าไปเชื่อข้อความนั้น. ให้เปิด error.log แล้วหา curl exit code มันบอกชั้นได้เลยว่าปัญหาอยู่ตรงไหน:

curl exit code → ชั้นที่พังcode 1ชั้น 1->ของในเครื่องหาย/home/tmp
curl exit code → ชั้นที่พังcode 9ชั้น 2->server-side pathProFTPD chroot
curl exit code → ชั้นที่พังcode 28ชั้น 3->network/firewall blockCSF egress

สรุปคือ UI ของ DA ตอนเจอปัญหา FTP ไม่บอกสาเหตุจริง. ดูแค่ generic error ไม่พอ ต้องลงไปดู error log ลึกถึงจะเห็นว่าเกิดอะไรขึ้นจริงๆ.

Phase 2 — Decision: ทำไมไม่เปลี่ยนเลย

หลังแก้ FTP ครบทั้งสามชั้น backup กลับมาทำงานปกติ. แต่พอเรื่องสงบ ผมก็เริ่มคิดมากกว่าเดิม:

  • Throughput อยู่ที่ราว 20 MB/s ทั้งที่ทั้งสองเครื่องอยู่บน LAN gigabit ด้วยกัน — มันต้องเร็วกว่านี้ได้
  • Config สามชั้นที่ผมเพิ่งแก้ไปมัน fragile — ครั้งหน้าที่ update DA หรือ Debian ก็มีโอกาสพังอีก
  • FTP เป็น protocol โบราณ — control กับ data channel แยกกัน, PASV, TLS เป็น optional — surface attack เยอะ

ผมเลยถามตัวเองคำเดียว: web1 กับ bak1 มันอยู่บน hardware เดียวกัน — มีทาง shared filesystem แทน network protocol ไหม?

ตัวเลือกที่มี

ผมเทียบสามทาง — FTP เดิม, NFS, virtiofs:

NFS

เลือกตัวนี้

  • Throughput ราว 100-150 MB/s (HDD-bound)
  • Overhead ปานกลาง (TCP + RPC)
  • ไม่ต้อง restart VM
  • Mature 30+ ปี — edge case รู้กันหมด
  • Rollback ง่าย (umount + ลบ fstab)
virtiofs

เก่งกว่าแต่ยังไม่กล้า

  • Throughput ราว 100-150 MB/s (HDD-bound เท่ากัน)
  • Overhead ต่ำสุด (shared mem IPC)
  • CPU cost ต่ำสุด
  • ยังเป็น tech preview บน PVE 8.x
  • ต้อง restart VM (downtime ราว 30s)

ส่วน FTP เดิมนั้น throughput ราว 20 MB/s, overhead สูงจากตัว PASV/auth/control, CPU cost สูง — มีข้อดีอย่างเดียวคือมันรันอยู่แล้ว.

จุดที่หลายคนเข้าใจผิดคือคิดว่า virtiofs จะ "เร็วกว่า" NFS มาก. ความจริงคือปลายทางผมเป็น HDD ที่เขียน sequential ได้ราว 100-150 MB/s. ไม่ว่าจะใช้ NFS หรือ virtiofs ทั้งคู่จะไป saturate HDD ก่อนตัวมันเอง — ความเร็วจริงที่ได้เลยเท่ากัน. virtiofs ชนะแค่เรื่อง CPU efficiency กับ latency ซึ่งในเคสของผมไม่ใช่ bottleneck.

Worry สำคัญ — vzdump recursion

ตอนนั้นผมกลุ้มอยู่จุดเดียว: ถ้า web1 ไป mount share จาก bak1 แล้ว vzdump ที่ backup ตัว VM web1 มันจะลาก content ของ share ติดไปด้วยไหม? ถ้าใช่ ก็เท่ากับ backup กำลังเขียนตัวเองเป็น infinite loop.

พอไปขุดดูจริง มันไม่ใช่. vzdump operate ที่ระดับ block — มันอ่าน VM disk ตาม qm config (scsi / virtio / sata / ide). มันไม่ได้ iterate เข้าไปใน VM filesystem และไม่ได้ dereference mount point.

ภาพในหัวที่ทำให้เข้าใจง่ายขึ้นคือ ใน VM disk มันมีแค่ inode ของ mount point ที่ว่างเปล่า ส่วน data จริงไปอยู่บน host:

/mnt/dabak/          ← inode มีอยู่ (empty dir mount point)
     └── (data)      ← ไม่ได้อยู่ใน VM disk จริง, อยู่บน host

vzdump อ่าน VM disk เห็นแค่ empty inode ส่วน content ที่มาจาก NFS/virtiofs ไม่ติดมาด้วย.

ก่อนทำจริงผม verify ก่อนเสมอ:

# บน Proxmox host
qm config <VMID> | grep -E '^(scsi|virtio|sata|ide)[0-9]'
# ควรเห็นแค่ scsi0 ของ VM — ไม่มี share entries ปะปน
เคสที่อันตรายจริง

ถ้าใครทำ block passthrough แบบ qm set --scsi1 /dev/sdd แบบนี้ vzdump จะลาก device นั้นไปด้วยจริงๆ. ของผมไม่ทำแบบนั้น mount point ผ่าน NFS เลยปลอดภัย.

ทำไมถึงเลือก NFS

หลังเทียบทุกอย่างจบ ผมเลือก NFS ด้วยเหตุผลพวกนี้:

  • Mature 30+ ปี — edge case ต่างๆ ถูกเจอและแก้กันมาหมดแล้ว
  • ไม่ต้อง restart VM — เปลี่ยนได้แบบ online
  • Rollback ง่าย แค่ umount แล้วลบบรรทัดใน fstab
  • CPU overhead ต่างจาก virtiofs นิดเดียว ในเมื่อ HDD ของผมเป็น bottleneck อยู่แล้ว ยังไงก็ saturate ก่อน

virtiofs เก่งสุดในกระดาษ แต่ตอนนั้นมันยังเป็น tech preview บน PVE 8.x ผมยังไม่กล้าเอาขึ้น production. ไว้รุ่นไหนมัน promote เป็น stable ผมค่อย migrate ไปก็ได้ ของพวกนี้เปลี่ยนได้.

NFS setup — ของจริง

ฝั่ง Server (bak1 — Proxmox host)

apt install nfs-kernel-server

mkdir -p /mnt/backup/dabak
chown root:root /mnt/backup/dabak
chmod 755 /mnt/backup/dabak

ไฟล์ /etc/exports:

/mnt/backup/dabak  192.168.1.10(rw,sync,no_root_squash,no_subtree_check,fsid=0)

แต่ละ option มีเหตุผล:

  • rw — DA ต้องเขียนได้
  • sync — เขียนแล้วยืนยันก่อนตอบ client. อย่าใช้ async กับ backup เป็นอันขาด ถ้าเครื่องดับกลางทางไฟล์จะเสีย
  • no_root_squash — รักษา root UID ไว้ เพราะ DA บางทีรัน mkdir เป็น root
  • no_subtree_check — ได้ performance และเลี่ยง edge case ตอน rename
  • fsid=0 — บอก NFSv4 ว่านี่คือ root export
exportfs -ra
systemctl restart nfs-server
systemctl enable nfs-server
showmount -e localhost

ฝั่ง Client (web1 — DA server)

apt install nfs-common
mkdir -p /mnt/dabak

ไฟล์ /etc/fstab:

192.168.1.20:/mnt/backup/dabak  /mnt/dabak  nfs  vers=4,hard,_netdev,nofail,timeo=600,retrans=2,x-systemd.automount  0 0

option ชุดนี้ผมตั้งหลังจากเจอปัญหาตอนแรกแล้ว แต่ละตัวมีที่มา:

  • vers=4 — บังคับใช้ NFSv4 ไม่ให้ fallback กลับไป v3 ที่ต้องการ portmapper เพิ่ม
  • hard — ถ้า server ดับ process จะรอจน server กลับมา. อย่าใช้ soft กับ backup เพราะ fail ครึ่งทางจะได้ไฟล์เสียแบบเงียบๆ
  • _netdev — บอก systemd ให้รอ network ก่อน mount
  • nofail — ถ้า mount fail ตอน boot จะไม่ทำให้เครื่องค้างเข้า emergency mode
  • timeo=600 — รอ 60 วินาทีต่อ retry (ค่านี้เป็นหน่วย tenths-of-second ของ NFS hard mount)
  • x-systemd.automount — lazy mount ค่อย mount ตอนใช้จริง
nofail ลืมไม่ได้

ผมเคยลืม nofail ตอนเทส แล้ว web1 boot ค้างเพราะ bak1 ดับอยู่พอดี. ตั้งแต่นั้น nofail คือ option บังคับสำหรับทุก network mount ที่ไม่อยากให้ลาก boot ทั้งเครื่องไปค้างด้วย.

systemctl daemon-reload
mount /mnt/dabak
df -h /mnt/dabak

เปลี่ยน DA backup config จาก FTP → Local

ใน DA panel ไปที่ Admin Backup แล้วแก้แต่ละ cron:

  • Where: เปลี่ยนจาก FTP เป็น Local Path
  • Local path: /mnt/dabak/admin/Monday (ตั้งตามวัน)

ถ้ามี cron หลายอันแก้ทีละอันนาน จะไปแก้ตรงไฟล์ /usr/local/directadmin/data/admin/backup_crons.list ก็ได้ (เป็น URL-encoded format) — แต่ระวัง / ต้อง encode เป็น %2F.

ACL trip — เคยใช้ได้ ตอนหลัง reboot ทำไม fail

เรื่องนี้คือความผิดพลาดที่ใหญ่ที่สุดในระบบนี้ และผมอยากเล่าให้ละเอียด เพราะ root cause ของมันลึกลงไปถึงระดับ kernel.

หลัง NFS setup จบ backup รันได้ — log สะอาด 6 วันติด ผมก็นึกว่าเรียบร้อยแล้ว.

วันที่ 7 ผม reboot web1 เพื่อแพทช์ kernel. หลัง reboot syslog เริ่มโชว์:

mkdir(/mnt/dabak/admin) error: Permission denied

ผมงงมาก. folder /mnt/dabak/admin ก็มีอยู่, owner เป็น DA admin user ถูกต้อง, mount ก็เป็น rw ปกติ — แล้วมัน fail ตรงไหน?

Debug ทาง dead-end (ลองทุกอย่างก่อน)

ผมไล่เช็คทุกอย่างที่นึกออก:

ls -la /mnt/dabak/admin/        # folder ยังอยู่
mount | grep dabak              # mount ปกติ rw
getfacl /mnt/dabak/admin/       # owner DA admin user ครบ
sudo -u <admin> ls /mnt/dabak/admin/             # list ได้
sudo -u <admin> mkdir /mnt/dabak/admin/test      # Permission denied

ทั้งที่ folder admin/ มี ownership ถูกต้องทุกอย่าง — ทำไม mkdir ภายใต้ admin/ ถึงไม่ได้?

Root cause ที่ค้นเจอ

มันเป็นเรื่องพฤติกรรมของ mkdir(2) syscall ในระดับ kernel:

กฎ kernel ที่เป็นหัวใจของบทความนี้

Kernel จะตรวจ "write permission ของ parent dir" ก่อน แล้วค่อยไปตรวจว่า name นั้นมีอยู่แล้วหรือยัง. ถ้า parent เขียนไม่ได้ มันคืน EACCES ตั้งแต่ขั้นแรก ไม่ทันถึงขั้น EEXIST เลย.

DA backup logic จะเรียก mkdir ทุก path component (/mnt/dabak, /mnt/dabak/admin, ...) เพื่อ ensure ว่า path มีอยู่. ปัญหาคือ parent /mnt/dabak ของผมเป็น mode 755 owner root:root.

DA admin user (UID 1500) ตกอยู่ในกลุ่ม "other" ของ parent — ได้แค่ r-x ไม่ใช่ rwx. พอเรียก mkdir(/mnt/dabak/admin) kernel เลย return EACCES ตั้งแต่ขั้นตรวจ parent permission — ไม่ทันไปถึงขั้นที่จะรู้ว่า admin มีอยู่แล้ว. DA catch error นี้แล้วรายงานออกมาเป็น "Permission denied".

Trap ที่หลอกแทบทุกคนคือ — การไปเพิ่ม write permission ที่ตัว admin/ subdir ไม่ช่วยอะไรเลย เพราะ kernel ตรวจ parent ไปก่อนแล้ว.

mkdir ตรวจสิทธิ์ของ parent ก่อนเสมอmkdir(/mnt/dabak/admin)ขั้น 1 ตรวจ write ของ parent/mnt/dabak 755 root:rootขั้น 2 ตรวจว่าชื่อ adminมีอยู่แล้วหรือยังผ่านเมื่อมี ACLไม่มี ACLUID 1500 เป็น otherได้แค่ r-xคืน EACCES ทันทีทางแก้: POSIX ACLsetfacl -m u:1500:rwxที่ parentเพิ่มสิทธิ์ที่ admin/ ไม่ช่วย เพราะ kernel ตรวจ parent ไปก่อนแล้ว
EACCES เกิดที่ขั้นตรวจ parent ไม่เคยไปถึงขั้นตรวจว่าชื่อมีอยู่แล้ว ทางแก้คือให้สิทธิ์ที่ parent ด้วย ACL

แล้วทำไมก่อน reboot ถึงใช้ได้?

ตรงนี้ลึกลงไปอีกชั้น. NFSv4 client cache delegation จาก mount ครั้งก่อน. ตัว cached delegation มัน bypass การตรวจ parent ACL ใน fast path. หลัง reboot mount ใหม่ ไม่มี cache แล้ว kernel เลย evaluate ครบทุกขั้น และเปิดเผย ACL gap ที่จริงๆ มีมาตั้งแต่ต้น.

แปลว่า backup ที่ทำได้ 6 วันแรก มันผ่านได้แบบบังเอิญ เพราะ cache บัง. ของจริงคือ config ผิดมาตั้งแต่ setup แล้ว — รอแค่เวลาให้ reboot มาเปิดโปง.

บทเรียนใหญ่

อย่าวางใจการ test ครั้งเดียว. การ verify resilience ต้องทำบน fresh mount หลัง reboot เท่านั้น ถึงจะเจอ config gap ที่ cache ซ่อนไว้. "save แล้ว run ผ่านครั้งเดียว" ไม่ใช่การพิสูจน์ว่าระบบทน.

ACL fix — POSIX ACL ด้วย numeric UID

วิธีแรกที่หลายคนนึกถึงคือ chmod 777 ที่ parent. ผมไม่ทำ เพราะมัน insecure — ใครก็เขียนได้.

วิธีที่สองคือ chown parent ให้เป็นของ DA admin. ผมก็ไม่ทำ เพราะ parent มี subdir อื่นที่ใช้ owner เดิมอยู่ — chown แล้วจะไปลบ ownership เก่าที่ยังต้องใช้.

สุดท้ายผมใช้ POSIX ACL ซึ่งเป็น additive grant — ให้สิทธิ์เพิ่มได้โดยไม่กระทบ ownership เดิม:

# บน bak1
setfacl -m u:1500:rwx     /mnt/backup/dabak/    # admin (UID 1500) มี rwx เพิ่ม
setfacl -m d:u:1500:rwx   /mnt/backup/dabak/    # default ACL → inherit ให้ subdir ใหม่

ตรวจผลด้วย:

getfacl /mnt/backup/dabak/

ต้องเห็นออกมาแบบนี้:

user::rwx
user:1500:rwx              ← ใหม่
group::r-x
mask::rwx
other::r-x
default:user::rwx
default:user:1500:rwx      ← inherit

ตัว default ACL (d:) สำคัญ — มันคือ template ที่ subdir และไฟล์ใหม่จะ inherit ไปอัตโนมัติ. ถ้าไม่ใส่ default DA สร้าง folder ลูกใหม่มาเมื่อไหร่ก็จะไม่มีสิทธิ์ตามอีก.

ทำไมต้องใช้ numeric UID

NFSv4 ที่ใช้ sec=sys มันส่ง numeric UID ไปยัง backend. ตัว bak1 ไม่มี user ชื่อ "admin" ของ web1 อยู่เลย — แต่ POSIX ACL รับ numeric UID ตรงๆ ได้ ไม่สนใจชื่อ.

ผมเพิ่งมารู้ตรงนี้เอง. ก่อนหน้านั้นผมใส่ setfacl -m u:admin:rwx แล้ว getfacl ฟ้องว่า user not found ทำเอาสับสนอยู่พักนึง เพราะมันมองหาชื่อ user บนเครื่องที่ไม่มีชื่อนั้น.

Health check script — กันของหายซ้ำ

หลังเจอ ACL trip ผมเริ่มกังวลขึ้นมาว่า — ถ้าวันไหน fsck รัน หรือ restore จาก snapshot ที่ไม่มี ACL ติดมา มันจะหายอีกรอบ แล้วผมจะรู้ได้ยังไง?

ผมเลยเขียน script ตรวจ ACL รายวัน พร้อม auto-repair และ alert. ตัว script รันบน bak1 ไม่ใช่ web1 — เพราะ ACL อยู่ที่ bak1 ตรวจที่ source ตรงๆ ไม่ต้อง SSH ข้ามเครื่องมา ซึ่งจะเป็นการเพิ่ม single point of failure โดยใช่เหตุ.

#!/bin/bash
# /usr/local/sbin/dabak-acl-health.sh
# Run daily before DA backup starts. Auto-repair if ACL missing.
set -u
TARGET=/mnt/backup/dabak
LOG=/var/log/dabak-acl-health.log
ADMIN_UID=1500

TS=$(date +%F-%H:%M:%S)
log() { echo "[$TS] $*" >>"$LOG"; }

need_repair=0
acl=$(getfacl --absolute-names "$TARGET" 2>/dev/null)
echo "$acl" | grep -qE "^user:${ADMIN_UID}:rwx$"         || need_repair=1
echo "$acl" | grep -qE "^default:user:${ADMIN_UID}:rwx$" || need_repair=1

if [ $need_repair -eq 1 ]; then
    log "ACL missing — repairing"
    setfacl -m u:${ADMIN_UID}:rwx     "$TARGET"
    setfacl -m d:u:${ADMIN_UID}:rwx   "$TARGET"
    # ส่ง alert ออกไป channel ของตัวเอง (ไม่ใส่ในตัวอย่าง เพราะแต่ละคนใช้ไม่เหมือนกัน)
    log "REPAIRED"
else
    log "OK"
fi

# Sanity ของ NFS service ด้วย
systemctl is-active --quiet nfs-server || log "FAIL: nfs-server inactive"

ตั้ง cron:

30 0 * * * root /usr/local/sbin/dabak-acl-health.sh

ผมตั้งให้รัน 30 นาทีก่อน DA backup จะเริ่ม — ถ้าตรวจแล้วเจอว่า ACL หาย script จะ repair ทันที พร้อม log และ alert.

ตอน deploy ผมทำ disaster simulation ด้วย — ลองลบ ACL ทิ้งจริงๆ ด้วย setfacl -x u:1500 กับ d:u:1500 แล้วรอ cron tick. script detect ได้ → repair → alert ส่งออก. Pipeline ทำงานครบทุกขั้น.

ส่วน alert channel จะใช้อะไร — email, chat bot, monitoring service — ผมไม่ใส่ลงใน script ตัวอย่าง เพราะแต่ละคนใช้ไม่เหมือนกัน. ของผมเก็บไว้เล่าแยกในบทความอื่น.

Weekly VM-level snapshot

นอกจาก DA backup รายวันแล้ว ทุกวันพุธตอนตี 1 (ผมเลือกวันที่ user ใช้งานน้อยที่สุด) host bak1 จะทำ vzdump snapshot ของ VM web1 ทั้งก้อน:

# /etc/cron.d/vm-snapshot-weekly
0 1 * * 3 root /usr/sbin/vzdump <VMID> --mode snapshot --compress zstd --storage local-backup
  • --mode snapshot — ไม่ shutdown VM, ใช้ ZFS/LVM snapshot ทำ. รบกวน VM ตอน snapshot lock แค่ราว 30 วินาที
  • --compress zstd — เร็วและ ratio ดีกว่า gzip มาก
  • เก็บคนละ storage จาก DA backup (ผมใช้ local-backup ที่อยู่คนละ disk)

ผลคือทุกวันพุธจะมี VM ทั้งก้อนอีก 1 copy ที่ rollback ได้เต็มเครื่องถ้าซวยหนักจริงๆ. ส่วนรายวันก็มี per-user tar ที่ restore เลือกเฉพาะ user ได้.

อย่าทำ snapshot ชนกับ backup

อย่า snapshot วันเดียวกับที่ DA admin backup กำลังเขียน — I/O จะชนกัน และ snapshot อาจติดไฟล์ที่กำลังถูกเขียนอยู่ได้. แยกวันกันไว้ปลอดภัยกว่า.

Recovery checklist — เวลา backup ค้าง ตรวจอะไรบ้าง

# 1. mount ขึ้นไหม
ssh root@<server-ip> "mount | grep dabak"

# 2. NFS server ทำงานไหม
ssh root@<server-ip> "systemctl is-active nfs-server"

# 3. ACL ครบไหม
ssh root@<server-ip> "getfacl /mnt/backup/dabak/ | grep 1500"

# 4. admin user เขียนได้จริงไหม (test ที่จะ expose mkdir EACCES trap)
ssh root@<server-ip> "sudo -u <admin> mkdir /mnt/dabak/test-\$\$ && sudo -u <admin> rmdir /mnt/dabak/test-\$\$"

# 5. DA log ล่าสุด
ssh root@<server-ip> "grep 'backup task' /var/log/syslog | tail -5"

ถ้า test ข้อ 4 ผ่าน แสดงว่าปัญหาไม่ได้อยู่ที่ permission. ถ้า fail ก็ให้ไปดูที่ ACL หรือ mount option.

ตัว test ข้อ 4 นี่ผมยกขึ้นมาเป็น regression check โดยเฉพาะหลังเจอ ACL trip — ถ้าทุกอย่างปกติ command นี้ต้องเงียบสนิท. ถ้ามันโชว์ "Permission denied" ขึ้นมาเมื่อไหร่ = ACL หาย ให้ repair ทันที.

สรุป architecture ที่ลงตัวแล้ว

[ web1 (DirectAdmin) ]                [ bak1 (Proxmox + NFS) ]

  /home/<user>/                          /mnt/backup/dabak/
  /etc/...                                ├── admin/Monday/      ← daily DA tar.zst per user
  /var/named/...                          ├── admin/Tuesday/
                                          ├── admin/Wednesday/
       │                                  ├── admin/Thursday/
       │  push ผ่าน NFSv4 hard mount      ├── admin/Friday/
       └────────────────────────►         ├── admin/Saturday/
            mount /mnt/dabak              └── admin/Sunday/

                                          /mnt/backup/vmdump/    ← weekly VM snapshot
                                            └── (ทุก Wed กลางคืน)
7 วันrolling DA backup
~200 GBcompressed ทุก user
~100-150 MB/sNFSv4 (HDD-bound)
2 ระดับfile + VM restore
  • รายวัน: DA Admin Backup → NFSv4 → bak1 ext4 (7-day rolling, ทุก user ทุกวัน)
  • รายสัปดาห์: vzdump VM snapshot ทั้งก้อน ที่ bak1 host (วันพุธกลางคืน — วันที่ user น้อย)
  • Cross-UID write: POSIX ACL กับ numeric UID — additive, ไม่กระทบ ownership เดิม
  • Resilience: hard mount + _netdev,nofail + ACL health check รายวัน
  • Restore 2 ระดับ: file-level จาก DA backup, VM-level จาก Proxmox snapshot

ของที่ผมเพิ่งเรียนรู้ระหว่างทำ

ระหว่างทำเรื่องนี้ผมก็มี AI ช่วย iterate หลายรอบ — แก้ผิดแก้ถูกหลายครั้งกว่าจะลงตัว. ส่วนตัวผมเริ่มชินกับการ debug แบบมี AI ช่วยคิด แต่ก็ต้องเช็คทุก step เพราะมันยัง hallucinate ได้.

บทเรียนที่ผมสรุปออกมาให้คนอื่นหยิบไปใช้ได้เลย:

  • DA migrate version เก่าไปใหม่ ไม่ได้ frictionless — minor version เปลี่ยน privilege model จาก root ไปเป็น diradmin/nobody. CSF กับ ProFTPD default ที่เคยใช้ได้จะพังตามมา
  • "FTP information invalid" ของ DA เป็น error ลวงตา — อย่าหยุดที่ UI ให้ดู error.log หา curl exit code (1/9/28 = สามชั้นที่อาจพัง)
  • vzdump operate ที่ block level ไม่ใช่ file level — VM ที่ mount NFS/virtiofs share จาก host เดียวกันไม่เกิด recursion. แต่ block passthrough (qm set --scsi1 /dev/...) เกิด
  • NFS hard + _netdev,nofail เป็น golden combo — hard กัน data corruption, _netdev รอ network, nofail กัน boot ค้าง
  • kernel mkdir(2) ตรวจ parent write ก่อน EEXIST — สำคัญที่สุดของบทความนี้. error "Permission denied" บนไฟล์ที่ "ดูเหมือนยังอยู่" ให้ check parent permissions ก่อนเสมอ
  • NFSv4 cached delegation อาจซ่อน config gap — test รอบสุดท้ายต้องเป็น fresh mount หลัง reboot ไม่ใช่ "save แล้ว run ผ่านครั้งเดียว"
  • POSIX ACL ดีกว่า chmod 777 สำหรับ cross-UID shared dir — additive grant, ไม่กระทบ ownership
  • POSIX ACL รับ numeric UID — ไม่ต้องสร้าง user ที่ remote side. NFSv4 sec=sys ส่ง numeric ตรงๆ
  • Health check ที่ source of truth — ACL อยู่ที่ bak1 ก็ตรวจที่ bak1 ตรงๆ อย่า SSH ข้ามไปมาให้เพิ่มจุดพัง
  • ของที่ "เงียบหาย" คือศัตรูสำคัญที่สุดของ backup — fsck, snapshot restore, manual edit ลบ ACL ได้หมด. Daily check จับได้

architecture ทั้งหมดที่เล่ามานี้เป็นผลจากการ implement และแก้ปัญหาจริงระหว่าง production. ผมเองก็พลาดและต้องกลับมาแก้หลายครั้งระหว่างทาง. ใครจะนำวิธีไปใช้กับระบบตัวเองก็ apply ได้ — เพียงปรับ path / UID / IP ให้ตรงกับระบบของคุณเอง.

ที่จริงระบบ backup ของผมยังมีอีกชั้นนอกเหนือจากนี้ — off-site backup ที่ดึงจาก bak1 ไปเก็บอีกเครื่องคนละสถานที่ เพื่อให้เข้าหลัก 3-2-1 backup. ผมจะเล่าใน Part 2 ที่ใช้ rclone + rsync daemon ผ่าน SFTP — code ที่ผมเขียนรันได้ทั้ง Linux/Mac/Windows.

ใครมี trick อื่นที่เก่งกว่านี้ เช่นใช้ Borg/Restic แทน DA admin backup, ZFS send/recv replication, หรือทำ 3-2-1 ด้วยวิธีอื่น ลง comment แชร์ได้นะครับ จะลองไปปรับใช้บ้าง. ตามสะดวก หลักการเหมือนกันหมด — ตรงไหนผิดพลาดผมจะกลับมาแก้ในภายหลังครับ.

อยากให้เราทำให้ไหม?

ให้เราช่วยติดตั้ง ตั้งค่า ย้ายระบบ หรือแก้ปัญหาเซิร์ฟเวอร์ เพื่อให้คุณมีเวลาไปทำงานที่สำคัญกว่า

คุยเรื่องงานกับเรา

เคยเจองานแบบนี้ไหม? ติดตรงไหน?

มาแลกวิธีกันต่อในบอร์ด LinuxThai

→ คุยต่อในบอร์ด

ความคิดเห็น 0

ยังไม่มีความคิดเห็น — เป็นคนแรกเลย