วิธีสำรองข้อมูลและอัปเกรด Docker Compose อย่างปลอดภัย
คู่มือสำรองข้อมูล Docker Compose stack ที่ครบถ้วน ทั้งการดัมพ์ฐานข้อมูล การคัดลอก volume และขั้นตอนการอัปเกรดที่ถูกต้อง เพื่อป้องกันข้อมูลสูญหายจากการทำ schema migration
สิ่งที่การสำรองข้อมูล Docker Compose stack ต้องมี
การสำรองข้อมูล Docker Compose stack ต้องประกอบด้วย 4 ส่วนแยกกัน หากขาดส่วนใดส่วนหนึ่งไป แอปพลิเคชันจะไม่สามารถกู้คืนกลับมาได้ ได้แก่ ไฟล์ compose, ไฟล์ .env ที่อยู่ข้างกัน, เนื้อหาของ volume ทั้งหมด และไฟล์ dump ของฐานข้อมูลที่สร้างโดย client ของฐานข้อมูลนั้นโดยตรง การคัดลอกไฟล์ฐานข้อมูลในขณะที่ container กำลังทำงานอยู่ไม่ถือเป็นการสำรองข้อมูลที่ถูกต้อง การอัปเกรดต้องใช้รายการข้างต้นทั้งหมดบวกกับกฎอีกหนึ่งข้อคือ ต้องสำรองข้อมูลก่อนทำการ pull เสมอ เนื่องจากกระบวนการ schema migration ถูกออกแบบมาให้ทำงานไปข้างหน้า และโครงการส่วนใหญ่ไม่มีวิธีการย้อนกลับ
เนื้อหาทั้งหมดด้านล่างนี้ตั้งสมมติฐานว่า stack ได้ถูก deploy เรียบร้อยแล้ว และ docker compose ps แสดงสถานะว่ากำลังทำงานอยู่ ตัวอย่างใช้ไดเรกทอรีโครงการที่ /srv/myapp โดยมีบริการชื่อ app และ db ให้คุณแทนที่ด้วยชื่อของคุณเอง คำสั่งที่ใช้ถูกทำให้เป็นรูปแบบทั่วไปโดยเจตนา เนื่องจากส่วนประกอบสำคัญอย่าง volume และฐานข้อมูลนั้นมีวิธีการทำงานเหมือนกันไม่ว่าจะเป็นแอปพลิเคชันใดก็ตาม
ตรวจสอบว่า stack ของคุณจัดเก็บข้อมูลอะไรไว้จริง
cd /srv/myapp
docker compose ps
docker compose config --volumes
docker volume ls --filter label=com.docker.compose.project=myappdocker compose config --volumes จะแสดงชื่อย่อของ named volumes ที่ไฟล์ของคุณประกาศไว้ ส่วน docker volume ls จะแสดงชื่อที่ volumes เหล่านั้นใช้จริงบนดิสก์ รายการทั้งสองมีความแตกต่างกันเนื่องจาก Compose จะเติมชื่อโปรเจกต์ไว้ด้านหน้า: volume ที่เขียนว่า db_data ในไฟล์ จะมีตัวตนอยู่จริงในชื่อ myapp_db_data โดยค่าเริ่มต้นชื่อโปรเจกต์จะใช้ชื่อไดเรกทอรี ดังนั้นหากเปลี่ยนชื่อไดเรกทอรี stack จะชี้ไปยังชุดของ volumes ใหม่ที่ว่างเปล่า และทิ้ง volumes เดิมที่เต็มไปด้วยข้อมูลของคุณไว้ ทุกคำสั่งด้านล่างนี้จำเป็นต้องใช้ชื่อจริงจาก docker volume ls
Bind mounts จะไม่ปรากฏในรายการใดเลย ในไฟล์ compose รายการเหล่านี้คือรายการที่มี path ของโฮสต์อยู่ทางซ้ายของเครื่องหมายทวิภาค (colon) เช่น ./config:/app/config ซึ่งเป็นไดเรกทอรีปกติบนโฮสต์ ดังนั้นเครื่องมือทั่วไปจึงสามารถเข้าถึงได้ ส่วน named volumes จะอยู่ใน /var/lib/docker/volumes/ และ docker volume inspect --format '{{.Mountpoint}}' myapp_db_data จะแสดง path ที่แน่นอนของ volume นั้น ประเภทที่ stack ของคุณใช้งานจะส่งผลต่อวิธีการคัดลอกข้อมูล และ bind mounts เทียบกับ named volumes ได้อธิบายข้อดีข้อเสียไว้อย่างครบถ้วน
ตอนนี้ให้จัดกลุ่มสิ่งที่คุณพบออกเป็นสองกลุ่ม กลุ่มแรกคือ volumes ที่เก็บสถานะซึ่งไม่สามารถสร้างใหม่ได้ เช่น ไฟล์ที่อัปโหลด, คีย์ที่สร้างขึ้น, ฐานข้อมูล และข้อมูลใดๆ ที่ผู้ใช้พิมพ์ลงในแอป กลุ่มที่สองคือข้อมูลที่ได้มาจากการประมวลผล เช่น รูปย่อ (thumbnails) และดัชนีการค้นหา ซึ่งแอปสามารถสร้างใหม่ได้ด้วยตนเอง การสำรองข้อมูลกลุ่มที่สองจะสิ้นเปลืองพื้นที่ดิสก์และเวลาในการกู้คืนโดยไม่ได้รับประโยชน์ใดๆ ตัวอย่างที่ชัดเจนที่สุดคือ volume ของ Redis cache ซึ่งหากสูญเสียไป ก็เพียงแค่ทำให้คำขอแรกทำงานช้าลงเท่านั้น
สำรองไฟล์ compose และไฟล์ .env
ไฟล์ทั้งสองวางอยู่บนโฮสต์ข้างกัน และไม่มีไฟล์ใดอยู่ใน volume ใดๆ ไฟล์ .env เก็บทั้งรหัสผ่านฐานข้อมูล, secret ของแอปพลิเคชัน และ API token ต่างๆ ดังนั้นไฟล์นี้จึงเป็นสิ่งที่เปลี่ยนกลุ่มของ volume ให้กลับมาเป็นแอปที่ทำงานได้อีกครั้ง นอกจากนี้ไฟล์ดังกล่าวมักถูกระบุไว้ใน .gitignore ซึ่งหมายความว่าแผนการที่ว่า "การตั้งค่าของฉันอยู่ใน git" นั้นไม่ได้รวมไฟล์ที่สำคัญที่สุดเอาไว้ การเก็บ secret ไว้ในไฟล์ env เป็นรูปแบบที่ถูกต้อง และทำให้คุณมีหน้าที่ต้องสำรองไฟล์นี้ควบคู่ไปด้วย
sudo install -d -m 700 -o "$USER" -g "$(id -gn)" /srv/backups/myapp
cp -a compose.yaml .env /srv/backups/myapp/
chmod 600 /srv/backups/myapp/.envให้คัดลอกไฟล์ compose ทุกไฟล์ที่ stack ใช้งาน ไม่ใช่แค่ไฟล์แรกเพียงไฟล์เดียว stack ที่เริ่มต้นด้วย -f compose.yaml -f compose.prod.yaml จำเป็นต้องใช้ไฟล์ทั้งสองเพื่อให้กลับมาทำงานในลักษณะเดิม และ วิธีการรวมไฟล์ compose หลายไฟล์ จะเป็นตัวกำหนดว่าค่าใดที่ถูกส่งไปยังคอนเทนเนอร์จริง
คำเตือนประการหนึ่งคือความสัมพันธ์ระหว่าง .env กับ volume อิมเมจ Postgres อย่างเป็นทางการจะอ่านค่า POSTGRES_PASSWORD เฉพาะตอนที่เริ่มต้นไดเรกทอรีข้อมูลที่ว่างเปล่าเท่านั้น การเปลี่ยนค่าดังกล่าวในภายหลังจะไม่เปลี่ยนรหัสผ่านที่อยู่ภายในฐานข้อมูล หากคุณกู้คืน volume ของเดือนที่แล้วมาวางคู่กับ .env ของวันนี้ แอปจะเชื่อมต่อไม่สำเร็จด้วยข้อผิดพลาด FATAL: password authentication failed for user "appuser" ในขณะที่ไฟล์ทั้งสองดูถูกต้องเมื่อตรวจสอบด้วยสายตา ดังนั้นควรเก็บ .env และ volume ที่มาจากช่วงเวลาเดียวกันไว้ด้วยกันในการสำรองข้อมูลเสมอ
การดัมพ์ฐานข้อมูลด้วยไคลเอนต์ของตัวมันเอง
เซิร์ฟเวอร์ฐานข้อมูลมีการเขียนข้อมูลลงไฟล์อยู่ตลอดเวลา การทำ tar ของ /var/lib/postgresql/data ในขณะที่เซิร์ฟเวอร์กำลังทำงานอยู่ จะทำให้ได้ไฟล์ที่คัดลอกหน้าข้อมูลบางส่วนจากก่อนการเขียนและบางส่วนจากหลังการเขียน ส่งผลให้ไฟล์สำรองข้อมูลมีข้อมูลที่ไม่สอดคล้องกันและอาจไม่สามารถกู้คืนได้ เครื่องมือดัมพ์ข้อมูลจะอ่านข้อมูลภายในทรานแซกชันเดียว ทำให้ไฟล์ที่ได้มีสถานะที่สอดคล้องกัน ณ ช่วงเวลาใดเวลาหนึ่ง ความแตกต่างนี้คือสิ่งที่แยกการสำรองข้อมูลออกจากการคัดลอกไฟล์ทั่วไป
docker compose exec -T db sh -c \
'pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Fc' \
> /srv/backups/myapp/db-$(date +%F).dumpให้คง -T ไว้ เพราะมันจะปิดการจัดสรร TTY ซึ่งหากเปิด TTY ไว้ Docker จะทำการแปลงสตรีมข้อมูลระหว่างทางไปยังเชลล์ของคุณ ซึ่งจะทำให้ไฟล์ดัมพ์ที่เป็นไบนารีเสียหาย คุณจะไม่ทราบปัญหานี้จนกว่าจะกู้คืนข้อมูลไม่สำเร็จ การใช้เครื่องหมายคำพูดเดี่ยว (single quotes) ก็มีความสำคัญเช่นกัน เพราะจะป้องกันไม่ให้เชลล์บนโฮสต์ขยายค่า $POSTGRES_USER ทำให้เชลล์ภายในคอนเทนเนอร์เป็นผู้ขยายค่าดังกล่าวแทน โดยใช้ค่าที่กำหนดไว้ในไฟล์ compose อยู่แล้ว -Fc จะเขียนข้อมูลในรูปแบบ custom format ซึ่งจะบีบอัดข้อมูลระหว่างทำงานและช่วยให้ pg_restore สามารถเลือกดึงออบเจกต์ออกมาได้ในภายหลัง
บทบาท (roles) และรหัสผ่านของผู้ใช้จะถูกเก็บไว้นอกฐานข้อมูลใดฐานข้อมูลหนึ่ง ดังนั้นควรสำรองข้อมูลส่วนนี้ด้วย:
docker compose exec -T db sh -c 'pg_dumpall -U "$POSTGRES_USER" --globals-only' \
> /srv/backups/myapp/globals.sqlจากนั้นตรวจสอบว่าไฟล์ที่ได้เป็นไฟล์ดัมพ์จริง ไม่ใช่ข้อความแจ้งเตือนข้อผิดพลาด:
ls -lh /srv/backups/myapp/
head -c 5 /srv/backups/myapp/db-$(date +%F).dumpไฟล์ดัมพ์ในรูปแบบ custom format จะขึ้นต้นด้วยไบต์ 5 ตัวคือ PGDMP หากไฟล์มีขนาด 0 ไบต์ หรือขึ้นต้นด้วย pg_dump: แสดงว่าคำสั่งทำงานล้มเหลว เชลล์จะสร้างไฟล์เอาต์พุตขึ้นมาก่อนที่คำสั่งจะเริ่มทำงาน ดังนั้นการดัมพ์ที่ล้มเหลวจะยังคงทิ้งไฟล์ที่มีชื่อและเวลาที่ดูเหมือนปกติไว้ ซึ่งเป็นสาเหตุที่พบบ่อยที่สุดของการสำรองข้อมูลที่ล้มเหลวโดยไม่มีการแจ้งเตือน
สำหรับ MariaDB หรือ MySQL ไคลเอนต์จะเปลี่ยนไปแต่รูปแบบการทำงานยังคงเดิม:
docker compose exec -T db sh -c \
'mariadb-dump -u root -p"$MARIADB_ROOT_PASSWORD" --single-transaction --databases "$MARIADB_DATABASE"' \
> /srv/backups/myapp/db-$(date +%F).sql--single-transaction ช่วยให้สามารถดัมพ์ตาราง InnoDB ได้อย่างสอดคล้องกันโดยไม่ขัดขวางการเขียนข้อมูล ในอิมเมจ MySQL คำสั่งคือ mysqldump และตัวแปรคือ MYSQL_ROOT_PASSWORD และ MYSQL_DATABASE ในอิมเมจ MariaDB ปัจจุบัน mysqldump ยังคงใช้งานได้ในฐานะชื่อที่เข้ากันได้กับ mariadb-dump โปรดทราบว่ารหัสผ่านที่ระบุในบรรทัดคำสั่งจะสามารถมองเห็นได้ในรายการโพรเซสของคอนเทนเนอร์ตลอดระยะเวลาที่การดัมพ์ทำงาน
SQLite ต้องการการดูแลเป็นพิเศษ ฐานข้อมูลเป็นไฟล์เดียว แต่ทรานแซกชันล่าสุดอาจยังคงค้างอยู่ในไฟล์ -wal แยกต่างหากที่อยู่ข้างๆ ดังนั้นการคัดลอกเฉพาะ .db จะทำให้คุณได้ฐานข้อมูลที่ขาดข้อมูลล่าสุดไป หากอิมเมจมีไคลเอนต์มาให้ sqlite3 /data/app.db ".backup '/data/app-backup.db'" จะเขียนสำเนาที่สอดคล้องกันในขณะที่แอปทำงานอยู่ หากไม่มี ให้หยุดคอนเทนเนอร์แล้วคัดลอกไฟล์ .db พร้อมกับไฟล์คู่หูอย่าง -wal และ -shm
หากฐานข้อมูลของคุณทำงานบนโฮสต์แทนที่จะอยู่ในสแต็ก คำสั่งเดียวกันนี้สามารถใช้ได้โดยไม่ต้องมีคำนำหน้า docker compose exec และการอ่านเรื่อง การรันฐานข้อมูลใน Docker หรือบนโฮสต์ เป็นสิ่งที่ควรทำก่อนการสร้างระบบใหม่ครั้งถัดไปของคุณ
การสำรองข้อมูล Volume
Named volume ไม่มี path บน host ที่คุณควรเข้าไปแก้ไขด้วยตนเอง ดังนั้นให้ mount volume นั้นเข้ากับ container ชั่วคราวแล้วทำการบีบอัดข้อมูลจากภายใน container นั้นแทน
docker run --rm \
-v myapp_uploads:/data:ro \
-v /srv/backups/myapp:/backup \
alpine:3 tar czf /backup/uploads.tar.gz -C /data .Container ช่วยเหลือจะทำการ mount volume แบบอ่านอย่างเดียวที่ /data และ mount ไดเรกทอรีสำรองข้อมูลของคุณที่ /backup จากนั้นจึงเขียนไฟล์บีบอัดออกมายังฝั่ง host คำสั่ง --rm จะลบ container ช่วยเหลือทิ้งทันทีที่ tar ทำงานเสร็จสิ้น การใช้ :ro มีความสำคัญมาก เพราะหากพิมพ์คำสั่ง tar ผิดพลาด ข้อมูลต้นทางจะไม่ได้รับความเสียหาย -C /data . คือสิ่งที่ทำให้การกู้คืนข้อมูลลงในตำแหน่งที่ถูกต้อง เพราะมันจะจัดเก็บทุก path โดยอ้างอิงจาก root ของ volume หากคุณเขียน tar czf /backup/uploads.tar.gz /data แทน ทุก path จะมี data/ นำหน้า ส่งผลให้การกู้คืนข้อมูลสร้าง /data/data ขึ้นมาภายใน volume และแอปพลิเคชันจะมองเห็นเป็นเพียงไดเรกทอรีว่างเปล่า ไฟล์ที่บีบอัดจะมี root เป็นเจ้าของเนื่องจาก tar ทำงานในฐานะ root ภายใน container ให้รัน sudo chown "$USER" /srv/backups/myapp/uploads.tar.gz หากปัญหานี้เป็นอุปสรรคต่อการใช้งาน และอ่าน วิธีที่ PUID และ PGID กำหนดความเป็นเจ้าของไฟล์ หากไฟล์ที่กู้คืนมาไม่สามารถอ่านได้โดยแอปพลิเคชัน
ให้รันคำสั่งนี้หนึ่งครั้งต่อหนึ่ง named volume สำหรับ bind mount ไม่จำเป็นต้องใช้ container เลย: tar czf /srv/backups/myapp/config.tar.gz -C /srv/myapp/config . สามารถทำงานเดียวกันได้โดยตรงบน host
ตัดสินใจแยกตามแต่ละ volume ว่าจำเป็นต้องหยุดแอปพลิเคชันหรือไม่ การทำ tar ขณะที่แอปพลิเคชันกำลังเขียนข้อมูลลงใน volume อาจทำให้ได้ไฟล์ที่ถูกเขียนไม่สมบูรณ์ สำหรับไดเรกทอรีที่เก็บไฟล์อัปโหลดซึ่งมีการเขียนเพียงครั้งเดียวแล้วอ่านอย่างเดียว ความเสี่ยงนี้จะมีน้อย แต่สำหรับข้อมูลอื่น ให้หยุดบริการนั้นไว้ตลอดระยะเวลาที่คัดลอกด้วย docker compose stop app แล้วจึงใช้ docker compose start app คำสั่ง stop จะคงสถานะของ container และ volume ไว้ ซึ่งเป็นสิ่งที่คุณต้องการในกรณีนี้ และควรทำความเข้าใจ ความแตกต่างระหว่าง down และ stop ให้ชัดเจนก่อนที่จะพิมพ์คำสั่งใดคำสั่งหนึ่ง
อย่าถือว่าการทำ tar ของ volume ฐานข้อมูลคือการสำรองข้อมูลฐานข้อมูลของคุณ การทำ dump คือการสำรองข้อมูลที่แท้จริง การทำ archive ของ volume ในขณะที่หยุดฐานข้อมูลเป็นเพียงช่องทางสำหรับการสร้างระบบขึ้นมาใหม่ได้อย่างรวดเร็วเท่านั้น ไม่ใช่การสำรองข้อมูลหลัก
ลำดับขั้นตอนการทำงาน
- คัดลอกไฟล์ compose และ
.envไปยังไดเรกทอรีสำรองข้อมูล - ทำการ dump ฐานข้อมูลในขณะที่ระบบยังคงทำงานอยู่
- หยุด container ของแอปพลิเคชันหากมีการเปลี่ยนแปลง volume ในตำแหน่งเดิม
- จัดเก็บข้อมูลแบบ archive สำหรับ named volume แต่ละรายการและไดเรกทอรีแบบ bind-mount ทั้งหมด
- เริ่มการทำงานของบริการที่หยุดไว้ จากนั้นตรวจสอบความถูกต้องด้วย
docker compose ps - จดบันทึก image tags และ digests ที่ stack กำลังใช้งานอยู่
- คัดลอกไดเรกทอรีสำรองข้อมูลทั้งหมดออกจากเซิร์ฟเวอร์นี้
ขั้นตอนที่ 7 คือขั้นตอนที่ผู้ใช้งานมักจะละเลยไว้ทำในภายหลัง
การสำรองข้อมูลออกจากเซิร์ฟเวอร์
การสำรองข้อมูลไว้บนดิสก์ลูกเดียวกับที่เก็บข้อมูลหลักช่วยป้องกันได้เพียงความผิดพลาดที่เกิดจากตัวคุณเองเท่านั้น หากโวลุ่มเสียหาย เซิร์ฟเวอร์ถูกลบ หรือบัญชีผู้ใช้สูญหาย ข้อมูลทั้งสองชุดจะหายไปพร้อมกัน คุณควรส่งสำเนาข้อมูลไปยังพื้นที่จัดเก็บข้อมูลอื่นที่ไม่ใช่ VPS เครื่องนี้ โดยกำหนดตารางเวลาและนโยบายการเก็บรักษาข้อมูลไว้ การสำรองข้อมูลด้วย restic จาก VPS ได้อธิบายการตั้งค่า repository, flag สำหรับการเก็บรักษาข้อมูล และคำสั่งตรวจสอบไว้แล้ว จึงไม่จำเป็นต้องกล่าวซ้ำในที่นี้
restic สามารถอ่านข้อมูล dump ผ่าน pipe ได้โดยตรง ซึ่งช่วยให้ข้อมูลฐานข้อมูลที่เป็น plaintext ไม่ถูกเขียนลงบนดิสก์เลย:
docker compose exec -T db sh -c 'pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Fc' \
| restic backup --stdin --stdin-filename db.dumpไม่ว่าคุณจะใช้เครื่องมือใด ให้กำหนดตารางเวลาการทำงานไว้ใน systemd timer หรือ cron job และตั้งค่าให้งานดังกล่าวแจ้งเตือนเมื่อเกิดความล้มเหลวในช่องทางที่คุณตรวจสอบได้ สคริปต์สำรองข้อมูลที่ไม่มีการรายงานผลลัพธ์คือสคริปต์ที่อาจหยุดทำงานไปนานถึง 6 เดือนโดยที่คุณไม่ทราบเลย
พิสูจน์ว่าการสำรองข้อมูลใช้งานได้จริงด้วยการซ้อมกู้คืน
การสำรองข้อมูลที่ไม่มีการทดสอบกู้คืนถือเป็นเพียงสมมติฐาน การซ้อมด้านล่างนี้คือการกู้คืนลงใน stack ชุดที่สองที่ทำงานขนานไปกับชุดแรก เพื่อให้ระบบ production ยังคงให้บริการได้ตามปกติและไม่มีคำสั่งใดที่คุณพิมพ์ไปส่งผลกระทบต่อระบบหลัก
กลไกสำคัญคือชื่อโปรเจกต์ โดย Compose จะดึงชื่อจากชื่อไดเรกทอรีและนำไปประทับบนทุก container และ volume ที่สร้างขึ้น หากคุณคัดลอกไฟล์สำรองไปยังไดเรกทอรีใหม่ stack ที่กู้คืนมาจะมี volume เป็นของตัวเองโดยอัตโนมัติ
sudo install -d -m 700 -o "$USER" -g "$(id -gn)" /srv/myapp-restore
cd /srv/myapp-restore
cp /srv/backups/myapp/compose.yaml /srv/backups/myapp/.env .แก้ไขไฟล์ compose ที่คัดลอกมาเพื่อให้พอร์ตที่เปิดใช้งานบน host ไม่เกิดการชนกันกับ stack ที่กำลังทำงานอยู่ โดยใช้ 18080:8080 แทนที่ 8080:8080 หรือเปลี่ยนตัวแปรที่กำหนดค่าพอร์ตในไฟล์ .env ที่คัดลอกมา จากนั้นสร้าง container และ volume เปล่าโดยที่ยังไม่ต้องเริ่มการทำงานใดๆ:
docker compose create
docker volume ls --filter label=com.docker.compose.project=myapp-restoreคำสั่งที่สองควรแสดงรายการชื่อ volume เดียวกับในระบบ production โดยมี myapp-restore_ นำหน้า ให้เติมข้อมูลลงใน volume เหล่านั้น เริ่มการทำงานของฐานข้อมูลแยกต่างหาก แล้วโหลดไฟล์ dump เข้าไป:
docker run --rm -v myapp-restore_uploads:/data -v /srv/backups/myapp:/backup \
alpine:3 tar xzf /backup/uploads.tar.gz -C /data
docker compose up -d db
docker compose exec -T db sh -c \
'pg_restore -U "$POSTGRES_USER" -d "$POSTGRES_DB" --clean --if-exists' \
< /srv/backups/myapp/db-2026-08-16.dump--clean --if-exists จะลบ object แต่ละตัวก่อนสร้างใหม่ ซึ่งทำให้การกู้คืนสามารถทำซ้ำได้ หากไม่มีตัวเลือกนี้ การรันคำสั่งครั้งที่สองลงในฐานข้อมูลที่มีตารางเหล่านั้นอยู่แล้วจะหยุดทำงานพร้อมกับข้อผิดพลาด pg_restore: error: could not execute query: ERROR: relation "users" already exists
จากนั้นเริ่มบริการส่วนที่เหลือและตรวจสอบด้วยวิธีเดียวกับที่ผู้ใช้งานทั่วไปทำ:
docker compose up -d --wait
docker compose exec -T db sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "\dt"'
docker compose logs --tail=50docker compose up -d --wait จะรอจนกว่าทุกบริการจะรายงานสถานะว่าทำงานอยู่หรืออยู่ในสถานะ healthy และจะส่งค่า exit code ที่ไม่ใช่ศูนย์หากมีบริการใดไม่พร้อม ซึ่งเป็นสิ่งที่ทำให้ขั้นตอนนี้สามารถเขียนเป็นสคริปต์ได้ หากบริการใดไม่เคยเปลี่ยนสถานะเป็น healthy ให้ใช้ docker compose ps เพื่อดูสถานะของบริการนั้น และ Compose healthchecks จะอธิบายว่าคอลัมน์นั้นกำลังอ่านค่าอะไร จากนั้นเปิดแอปพลิเคชันผ่านพอร์ตสำรองและล็อกอินด้วยบัญชีผู้ใช้จริง เขียนข้อมูลหนึ่งรายการและเปิดไฟล์หนึ่งไฟล์ที่อยู่ใน volume คู่ข้อมูลนี้คือหลักฐานยืนยันว่า: ไฟล์ dump ถูกกู้คืนแล้ว, volume ถูกกู้คืนแล้ว และทั้งสองส่วนสอดคล้องกัน การซ้อมที่พิสูจน์เพียงว่าหน้าล็อกอินแสดงผลได้นั้น ไม่ได้พิสูจน์อะไรเลยเกี่ยวกับข้อมูลของคุณ
ลบระบบที่ใช้ซ้อมทิ้งเมื่อการทดสอบผ่าน:
docker compose down -vนี่เป็นกรณีเดียวที่ -v เป็น flag ที่ถูกต้องในการใช้งาน หากใช้คำสั่งเดียวกันในไดเรกทอรีของระบบ production มันจะลบ volume ที่คุณต้องการปกป้องทิ้งทั้งหมด
วิธีการอัปเกรด Compose stack
ให้อ่านบันทึกประจำรุ่น (release notes) ของทุกเวอร์ชันที่อยู่ระหว่างเวอร์ชันที่คุณใช้งานอยู่กับเวอร์ชันที่คุณต้องการอัปเกรด โดยให้ค้นหาคำว่า breaking และ migration โครงการที่ไม่รองรับการข้ามเวอร์ชันหลักหลายเวอร์ชันจะระบุไว้ในนั้น และการ migration ที่ล้มเหลวมักจะแจ้งเตือนคุณหลังจากที่ได้เปลี่ยนโครงสร้าง schema ไปบางส่วนแล้วเท่านั้น
จดบันทึกสิ่งที่คุณกำลังใช้งานอยู่ในปัจจุบันก่อนที่จะทำการเปลี่ยนแปลงใดๆ:
docker compose images
docker image inspect --format '{{index .RepoDigests 0}}' postgres:16.4docker compose images จะแสดงรายการ image และ tag ที่แต่ละบริการกำลังใช้งานอยู่ในขณะนี้ ค่า digest เป็นค่าเดียวที่ระบุ image ได้อย่างแม่นยำ เนื่องจาก tag สามารถถูกย้ายไปชี้ที่อื่นได้ตลอดเวลา
ให้สำรองข้อมูลตามส่วนต่างๆ ที่กล่าวมาข้างต้นและคัดลอกไฟล์สำรองข้อมูลออกจากเซิร์ฟเวอร์ ให้ทำเช่นนี้แม้จะเป็นการอัปเกรดเวอร์ชันย่อย (patch release) ก็ตาม การอัปเกรดที่ดูเหมือนไม่มีความเสี่ยงมักเป็นจุดที่ผู้คนละเลยการเตรียมตัว
จากนั้นให้ระบุเวอร์ชัน (pin) ในไฟล์ compose เนื่องจาก latest ไม่ใช่เวอร์ชันที่แน่นอน:
services:
db:
image: postgres:16.4หากใช้ image: postgres:latest คำสั่ง docker compose pull จะดึงข้อมูลล่าสุดที่ tag นั้นชี้ไปในปัจจุบัน ซึ่งทำให้คุณไม่มีทางทราบได้เลยว่าคุณกำลังใช้งานเวอร์ชันใดอยู่เมื่อวานนี้ การระบุ tag ที่แน่นอนจะเปลี่ยนการอัปเกรดให้เป็นการแก้ไขเพียงบรรทัดเดียวที่คุณสามารถตรวจสอบได้ใน git diff และย้อนกลับได้ด้วยการแก้ไขอีกเพียงครั้งเดียว ให้ระบุเวอร์ชันของ image แอปพลิเคชันในลักษณะเดียวกัน โดยใช้เวอร์ชันที่ถูกต้องจากหน้า release ของโครงการนั้นๆ
ดึงข้อมูลและสร้างคอนเทนเนอร์ใหม่:
docker compose pull
docker compose up -d --waitdocker compose up -d จะเปรียบเทียบไฟล์กับคอนเทนเนอร์ที่กำลังทำงานอยู่ และสร้างใหม่เฉพาะบริการที่มีการเปลี่ยนแปลง image หรือการตั้งค่าเท่านั้น คำสั่งนี้จะไม่ยุ่งกับ named volumes ดังนั้นคอนเทนเนอร์ใหม่จะเริ่มทำงานบนข้อมูลเดิมที่มีอยู่ ซึ่งเป็นจุดประสงค์หลักและเป็นความเสี่ยงในเวลาเดียวกัน เนื่องจากการเริ่มทำงานครั้งแรกของเวอร์ชันใหม่มักเป็นช่วงเวลาที่ระบบทำการ migration schema
เฝ้าดูการทำงาน:
docker compose ps
docker compose logs -f --tail=100 appคอนเทนเนอร์ที่ทำงานล้มเหลวจะแสดงสถานะ Exited (1) ในคอลัมน์ STATUS ของคำสั่ง docker compose ps โดยสาเหตุจะปรากฏอยู่ในบรรทัดท้ายๆ ของ log ข้อผิดพลาดในการ migration จะแสดงอย่างชัดเจนในส่วนนี้และไม่ปรากฏที่อื่น เมื่อ log นิ่งแล้ว ให้ล็อกอินเข้าไปและทดสอบใช้งานแอปสักครู่
หาก docker compose pull หยุดทำงานด้วยข้อผิดพลาด no space left on device สาเหตุส่วนใหญ่มักเกิดจาก image layers เก่าที่ค้างอยู่ และการ ลบ Docker images ที่ไม่ได้ใช้งาน จะช่วยคืนพื้นที่ว่างกลับมา ให้ทำการลบหลังจากที่การอัปเกรดผ่านการทดสอบแล้วเท่านั้น ไม่ควรทำก่อนหน้านั้น เนื่องจาก image layers เก่าเหล่านั้นคือสิ่งที่ใช้ในการ rollback อย่างรวดเร็วหากเกิดปัญหา
วิธีการย้อนกลับเมื่อการอัปเกรดเกิดข้อผิดพลาด
มีสองกรณีที่เกิดขึ้นและมีต้นทุนในการแก้ไขต่างกันมาก หากเวอร์ชันใหม่ไม่มีการเปลี่ยนแปลงโครงสร้างฐานข้อมูล (schema) การย้อนกลับทำได้ในบรรทัดเดียว: ให้เปลี่ยนกลับไปใช้แท็กเดิมในไฟล์ compose แล้วรัน docker compose up -d คอนเทนเนอร์จะถูกแทนที่โดยที่ volume ยังคงอยู่ที่เดิม และโค้ดเวอร์ชันเก่าจะสามารถอ่านข้อมูลที่เคยเขียนไว้ได้
หากเวอร์ชันใหม่มีการย้ายโครงสร้างฐานข้อมูล (migration) โค้ดเวอร์ชันเก่าจะไม่สามารถอ่านข้อมูลได้อีกต่อไป สคริปต์ migration มักถูกเขียนมาให้ทำงานไปข้างหน้าเท่านั้น และโครงการส่วนใหญ่ไม่มีสคริปต์สำหรับย้อนกลับ (downgrade) ดังนั้นเมื่อเวอร์ชันเก่าเริ่มทำงาน มันจะล้มเหลวทันทีที่รันคำสั่ง query แรกกับคอลัมน์ที่ถูกเปลี่ยนชื่อหรือลบไปแล้ว โดยจะแสดงข้อผิดพลาดในรูปแบบ ERROR: column "avatar_url" does not exist วิธีการแก้ไขคือการใช้ไฟล์ dump ที่คุณสำรองไว้ก่อนทำการดึงอิมเมจใหม่: ให้เปลี่ยนกลับไปใช้แท็กเดิม ลบ volume ของฐานข้อมูลทิ้ง สร้างใหม่ให้ว่างเปล่า นำไฟล์ dump กลับมาใส่ แล้วจึงเริ่มระบบ หากไม่มีไฟล์ dump ดังกล่าวจะไม่มีทางย้อนกลับได้เลย ซึ่งเป็นเหตุผลสำคัญว่าทำไมต้องสำรองข้อมูลก่อนทำการอัปเกรด
การอัปเกรด Postgres เวอร์ชันหลัก (major version) เป็นกรณีที่รุนแรงที่สุด และมักทำให้ผู้ใช้ประหลาดใจเพราะความล้มเหลวจะเกิดขึ้นในขั้นตอนการอัปเกรดแทนที่จะเป็นตอนย้อนกลับ รูปแบบการจัดเก็บข้อมูลบนดิสก์จะเปลี่ยนไปในทุกเวอร์ชันหลัก หากเปลี่ยน postgres:16.4 เป็น postgres:17.2 แล้วรัน docker compose up -d เซิร์ฟเวอร์ใหม่จะไม่สามารถเริ่มทำงานได้:
FATAL: database files are incompatible with server
DETAIL: The data directory was initialized by PostgreSQL version 16, which is not compatible with this version 17.2.อิมเมจจะไม่รัน pg_upgrade ให้คุณโดยอัตโนมัติ แนวทางที่รองรับภายใน Compose stack คือการ dump, แทนที่, และ restore: ให้ทำการ dump ข้อมูลขณะที่เวอร์ชันเก่ายังทำงานอยู่ จากนั้นรัน docker compose down, ลบ volume ของฐานข้อมูลทิ้ง, เปลี่ยนไปใช้แท็กใหม่, รัน docker compose create เพื่อสร้างไดเรกทอรีข้อมูลใหม่ที่ว่างเปล่า, เริ่มการทำงานของฐานข้อมูล, นำข้อมูลที่ dump ไว้กลับมาใส่, แล้วจึงเริ่มบริการส่วนที่เหลือ ให้เก็บไฟล์ dump เก่าไว้จนกว่าเวอร์ชันหลักใหม่จะรองรับการใช้งานจริงได้เป็นเวลาหนึ่งวัน สำหรับการอัปเกรดเวอร์ชันย่อยภายในเวอร์ชันหลักเดียวกัน เช่น จาก 16.4 ไป 16.9 ไม่จำเป็นต้องทำขั้นตอนเหล่านี้ เนื่องจากรูปแบบข้อมูลมีความเสถียรและคอนเทนเนอร์จะเริ่มทำงานได้ทันที
VPS snapshot ถือเป็นข้อมูลสำรอง (backup) หรือไม่?
มันเป็นเพียงส่วนเสริมของข้อมูลสำรองเท่านั้น และทั้งสองอย่างมีจุดอ่อนที่ต่างกัน Snapshot จะคัดลอกดิสก์ทั้งลูกในระดับ hypervisor ทำให้สามารถกู้คืนเครื่องทั้งเครื่องกลับมาได้ภายในไม่กี่นาที รวมถึงส่วนที่คุณลืมสำรองข้อมูลไว้ด้วย สิ่งนี้ทำให้มันเป็นเครื่องมือที่เหมาะสมสำหรับงานเฉพาะอย่างหนึ่ง คือเมื่อการอัปเกรดทำให้เซิร์ฟเวอร์พังและคุณต้องการย้อนกลับไปเป็นสถานะเมื่อ 20 นาทีก่อน
แต่สำหรับงานอื่น Snapshot ถือเป็นเครื่องมือที่ไม่ดีนัก เนื่องจากมีความละเอียดในระดับเครื่องทั้งหมด การกู้คืนตารางข้อมูลที่ถูกลบไปเพียงตารางเดียวจึงหมายถึงการต้องกู้คืนเซิร์ฟเวอร์ทั้งเครื่องไว้ที่อื่นแล้วค่อยไปดึงตารางนั้นออกมา นอกจากนี้ ระยะเวลาการเก็บรักษามักจะสั้น และสำเนาเหล่านี้มักจะอยู่ในบัญชีผู้ให้บริการเดียวกันกับเซิร์ฟเวอร์ ดังนั้นหากบัญชีถูกระงับหรือสูญหาย เซิร์ฟเวอร์และ Snapshot ทั้งหมดก็จะหายไปพร้อมกัน อีกทั้งการทำ Snapshot ของเครื่องที่กำลังทำงานอยู่ อาจทำให้ฐานข้อมูลถูกบันทึกในขณะที่กำลังเขียนข้อมูลอยู่ ส่งผลให้ฐานข้อมูลต้องทำ crash recovery ในการเริ่มทำงานครั้งแรก และธุรกรรมที่ยังค้างอยู่จะสูญหายไป
ควรใช้ทั้งสองอย่างควบคู่กัน Snapshot เปรียบเสมือนปุ่มย้อนกลับ (undo) สำหรับช่วงเวลาการอัปเกรด ส่วนการทำ dump คือสำเนาที่จะยังคงอยู่แม้บัญชีผู้ใช้งานจะถูกลบไปแล้ว ความแตกต่างระหว่าง snapshot และ backup จะอธิบายรายละเอียดว่าความล้มเหลวรูปแบบใดที่แต่ละวิธีสามารถรับมือได้ และไดเรกทอรีสำรองข้อมูลเดียวกันนั้นเองที่จะช่วยให้ การย้าย stack ไปยัง VPS ใหม่ กลายเป็นงานปกติที่ทำได้ง่าย แทนที่จะต้องมาสร้างใหม่จากความทรงจำ
สิ่งที่อาจผิดพลาดและสิ่งที่คุณจะพบ
แฟล็ก volumes ในคำสั่ง down docker compose down -v จะลบ named volumes ที่ประกาศไว้ในไฟล์ และ Compose จะยืนยันการทำงานด้วยบรรทัดที่ระบุว่า Volume myapp_db_data Removed การกระทำนี้ไม่สามารถย้อนกลับได้ การใช้เพียง docker compose down จะไม่ส่งผลกระทบต่อข้อมูลดังกล่าว ให้พิมพ์รูปแบบเต็มคือ docker compose down --volumes เพื่อให้แฟล็กที่ทำลายข้อมูลเป็นคำที่คุณต้องพิมพ์ระบุอย่างชัดเจน
การดัมพ์ข้อมูลที่ไม่มี magic string pg_restore: error: did not find magic string in file header หมายความว่าไฟล์ดังกล่าวไม่ใช่ไฟล์ archive สาเหตุที่พบบ่อยคือการขาด -T ในคำสั่ง docker compose exec เนื่องจากเมื่อมีการเชื่อมต่อ TTY สตรีมข้อมูลจะถูกแปลงระหว่างทางที่ส่งมายัง shell ของคุณ ทำให้ไฟล์ binary dump ที่ได้รับเสียหาย ให้ทำการดัมพ์ข้อมูลใหม่อีกครั้งด้วย -T จากนั้นตรวจสอบไบต์ 5 ตัวแรกด้วย head -c 5
รหัสผ่านที่ไม่ยอมเปลี่ยน FATAL: password authentication failed for user "appuser" หลังจากทำการ restore หมายความว่า .env และไดเรกทอรีข้อมูลมาจากช่วงเวลาที่ต่างกัน อิมเมจจะตั้งค่ารหัสผ่านนั้นเฉพาะตอนที่สร้างไดเรกทอรีข้อมูลเปล่าเท่านั้น ดังนั้นการแก้ไข .env ในภายหลังจึงไม่มีผลใดๆ ภายในฐานข้อมูล ให้ทำการ restore โดยใช้ .env ที่ตรงกัน หรือเปลี่ยนรหัสผ่านภายในฐานข้อมูลด้วย ALTER USER
volume ที่สองที่ว่างเปล่า Docker จะสร้าง volume ขึ้นมาเมื่อมีการเรียกใช้ ดังนั้น docker run -v myapp_upload:/data หากขาด s จะเป็นการเขียนข้อมูลลงใน volume ใหม่ที่ว่างเปล่าและรายงานว่าสำเร็จ docker volume ls จะแสดงชื่อทั้งสองชื่อ โดยชื่อหนึ่งจะไม่มีข้อมูลอยู่เลย ให้คัดลอกชื่อ volume จาก docker volume ls แทนการพิมพ์จากความจำ
การ restore ที่มุ่งเป้าไปยัง production การรันคำสั่ง restore ใน /srv/myapp แทนที่จะเป็น /srv/myapp-restore จะเป็นการเขียนทับข้อมูลจริงด้วยข้อมูลสำรอง และคำสั่งในทั้งสองตำแหน่งก็ดูเหมือนกันทุกประการ ให้ตรวจสอบ pwd ก่อนรันคำสั่ง restore ทุกครั้ง และแยกไดเรกทอรีสำหรับฝึกซ้อมออกจากส่วนอื่น
FAQ
docker compose down จะลบข้อมูลของฉันหรือไม่?
ไม่ docker compose down จะลบเฉพาะคอนเทนเนอร์และเน็ตเวิร์กเริ่มต้นเท่านั้น โดยจะคง named volumes และ bind mounts ไว้ ส่วน docker compose down -v จะลบ named volumes ที่ระบุไว้ในไฟล์ของคุณ ซึ่งเป็นการลบอย่างถาวร สำหรับ bind mounts นั้นเป็นไดเรกทอรีบนโฮสต์ ดังนั้น Compose จะไม่ลบไฟล์เหล่านี้ หากคุณต้องการหยุดบริการชั่วคราวเพื่อสำรองข้อมูลโดยไม่กระทบส่วนอื่น ให้ใช้ docker compose stop แทน
ฉันสามารถคัดลอกไดเรกทอรีข้อมูลของ Postgres แทนการใช้ pg_dump ได้หรือไม่?
ทำได้เฉพาะในขณะที่คอนเทนเนอร์หยุดทำงานเท่านั้น ในขณะที่เซิร์ฟเวอร์กำลังทำงาน ไฟล์ข้อมูลจะมีการเปลี่ยนแปลงตลอดเวลา ทำให้ไฟล์ที่คัดลอกมาอาจมีสถานะที่ไม่สอดคล้องกันและไม่สามารถกู้คืนได้ นอกจากนี้การคัดลอกระดับไฟล์ยังผูกติดกับ Postgres เวอร์ชันหลัก (major version) นั้นๆ จึงไม่สามารถนำไปใช้กับเวอร์ชันอื่นได้ ให้หยุดคอนเทนเนอร์ ทำการสำรองข้อมูล volume แล้วจึงเริ่มคอนเทนเนอร์ใหม่ โดยให้มองว่าวิธีนี้เป็นช่องทางสร้างระบบใหม่ที่รวดเร็ว ไม่ใช่การสำรองข้อมูลหลัก การทำ dump คือสำเนาที่ย้ายไปมาได้และเป็นวิธีที่ใช้สำหรับการกู้คืนข้อมูล
ฉันจะอัปเกรด Postgres เป็นเวอร์ชันหลักใหม่ใน Compose ได้อย่างไร?
การเปลี่ยน tag เพียงอย่างเดียวไม่เพียงพอ เซิร์ฟเวอร์เวอร์ชันใหม่จะไม่ยอมเริ่มทำงานบนไดเรกทอรีข้อมูลเดิมและจะแสดง log The data directory was initialized by PostgreSQL version 16, which is not compatible with this version 17.2 ให้รัน pg_dump ในขณะที่เวอร์ชันเก่ายังทำงานอยู่ จากนั้นรัน docker compose down ลบ volume ของฐานข้อมูล เปลี่ยนเป็น tag ใหม่ รัน docker compose create เพื่อสร้าง volume ใหม่ที่ว่างเปล่า เริ่มการทำงานของฐานข้อมูล แล้วจึงกู้คืนข้อมูลจาก dump เข้าไป ให้เก็บ dump เดิมไว้จนกว่าเวอร์ชันใหม่จะรองรับการใช้งานจริงได้ตามปกติ
ควรสำรองข้อมูลบ่อยแค่ไหน และควรเก็บไว้นานเท่าใด?
ให้กำหนดความถี่ตามปริมาณงานที่คุณยอมรับได้หากต้องทำใหม่ การสำรองข้อมูลทุกคืนเหมาะสำหรับ stack ของบุคคลหรือทีมขนาดเล็ก และควรสำรองข้อมูลด้วยตนเองเพิ่มอีกหนึ่งครั้งก่อนการอัปเกรดทุกครั้ง สำหรับระยะเวลาการเก็บรักษา ควรเก็บประวัติให้ครอบคลุมความเสียหายที่คุณอาจไม่ทันสังเกตเห็นในทันที เพราะหากพบตารางข้อมูลเสียหายในวันศุกร์ สำเนาของคืนวันพฤหัสบดีก็อาจช่วยไม่ได้ restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune เป็นนโยบายเริ่มต้นที่เหมาะสม ไม่ว่าตารางเวลาจะเป็นอย่างไร ให้ทดสอบกู้คืนข้อมูลทุกไตรมาส หากคุณยังไม่เคยทำเช่นนั้น แสดงว่าคุณยังไม่มีการสำรองข้อมูล คุณมีเพียงแค่ไฟล์เท่านั้น
ฉันต้องหยุดทั้ง stack เพื่อสำรองข้อมูลหรือไม่?
โดยปกติแล้วไม่จำเป็น การทำ database dump มีความสอดคล้องของข้อมูลในขณะที่เซิร์ฟเวอร์ทำงานอยู่ จึงไม่จำเป็นต้องหยุดบริการฐานข้อมูล ส่วนเรื่องของ volumes นั้น หากแอปพลิเคชันทำหน้าที่เพียงเพิ่มไฟล์ เช่น ไดเรกทอรีสำหรับอัปโหลด การสำรองข้อมูลขณะที่ระบบทำงานอยู่ก็มีความปลอดภัยเพียงพอ แต่หากมีการเขียนทับไฟล์เดิม ให้หยุดบริการนั้นชั่วคราวระหว่างการคัดลอกด้วย docker compose stop app แล้วจึงเริ่มใหม่หลังจากเสร็จสิ้น การหยุดเฉพาะแอปพลิเคชันในขณะที่ฐานข้อมูลยังทำงานอยู่ มักเป็นช่วงเวลาที่สั้นที่สุดและปลอดภัยที่สุดที่คุณสามารถทำได้