เรื่องที่อยากให้คุณรู้ก่อนตั้ง backup
หลายคนคิดว่า "host บน managed service เช่น RDS, Supabase ก็ pointer-in-time recovery มีอยู่แล้ว ไม่ต้อง backup เอง"
ถูกครึ่งหนึ่ง — managed service มี backup ของเขา แต่:
- ถ้า account คุณโดน lock / โดน hack คุณเข้าไปกู้ไม่ได้
- ถ้า provider ปิดบริการ (เคยมีหลายเจ้า) — ข้อมูลอยู่กับเขา
- ถ้าเขาเรียกราคาเพิ่ม คุณก็ทำอะไรไม่ได้
backup ที่ดีคือ เก็บนอก provider มี copy ที่อยู่ในมือเรา
หลักการ 3-2-1:
- เก็บ 3 copies (1 production + 2 backup)
- บน 2 media ต่างกัน (server, cloud, NAS)
- 1 copy อยู่นอกสถานที่ (offsite)
Tool ที่ใช้
ตัวอย่างนี้ใช้ PostgreSQL + AWS S3 / Backblaze B2 + cron แต่หลักการเดียวกันใช้ได้กับ MySQL/MongoDB/SQLite
Script Backup PostgreSQL
/home/deploy/backup-db.sh:
#!/bin/bash
set -euo pipefail
# === Config ===
DB_NAME="${DB_NAME:-myapp}"
DB_USER="${DB_USER:-postgres}"
DB_HOST="${DB_HOST:-localhost}"
DB_PORT="${DB_PORT:-5432}"
BACKUP_DIR="/var/backups/postgres"
S3_BUCKET="s3://my-app-backups/db"
DATE=$(date +%Y%m%d_%H%M%S)
FILENAME="${DB_NAME}_${DATE}.sql.gz"
LOCAL_PATH="$BACKUP_DIR/$FILENAME"
# === Pre-checks ===
mkdir -p "$BACKUP_DIR"
command -v pg_dump >/dev/null || { echo "pg_dump not found"; exit 1; }
command -v aws >/dev/null || { echo "aws cli not found"; exit 1; }
# === Backup ===
echo "[$(date)] starting backup of $DB_NAME"
PGPASSWORD="$DB_PASSWORD" pg_dump \
-h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" \
--format=plain --no-owner --no-acl \
"$DB_NAME" | gzip -9 > "$LOCAL_PATH"
SIZE=$(du -h "$LOCAL_PATH" | cut -f1)
echo "[$(date)] dump complete ($SIZE) → $LOCAL_PATH"
# === Upload to S3 ===
aws s3 cp "$LOCAL_PATH" "$S3_BUCKET/$FILENAME" \
--storage-class STANDARD_IA
echo "[$(date)] uploaded to $S3_BUCKET/$FILENAME"
# === Local cleanup (เก็บ 7 วัน) ===
find "$BACKUP_DIR" -name "*.sql.gz" -mtime +7 -delete
echo "[$(date)] local cleanup done"
echo "[$(date)] backup OK"
ตั้ง permission + ทดสอบรัน:
chmod +x /home/deploy/backup-db.sh
# สร้างไฟล์ env ที่มี password
cat > /home/deploy/.db-env <<EOF
DB_PASSWORD=your-strong-password
EOF
chmod 600 /home/deploy/.db-env
# ทดสอบ
set -a; source /home/deploy/.db-env; set +a
/home/deploy/backup-db.sh
ถ้าทำงานปกติ ดู file ที่ S3:
aws s3 ls s3://my-app-backups/db/
ตั้ง Cron ให้รันทุกคืน
crontab -e
เพิ่ม:
# รันทุกคืน 02:00
0 2 * * * source /home/deploy/.db-env && /home/deploy/backup-db.sh >> /var/log/db-backup.log 2>&1
ตรวจสอบ cron ทำงาน:
# ดู log
sudo tail -f /var/log/db-backup.log
# ดู cron service
systemctl status cron
ใช้ S3 Lifecycle จัดการ retention
แทนที่จะเขียน script ลบ backup เก่า ตั้ง S3 lifecycle rule ให้:
- เก็บ Standard 30 วัน
- ย้ายไป Glacier 30 วัน-1 ปี
- ลบทิ้งหลัง 1 ปี
{
"Rules": [{
"ID": "expire-old-backups",
"Status": "Enabled",
"Filter": { "Prefix": "db/" },
"Transitions": [
{ "Days": 30, "StorageClass": "GLACIER" }
],
"Expiration": { "Days": 365 }
}]
}
apply:
aws s3api put-bucket-lifecycle-configuration \
--bucket my-app-backups \
--lifecycle-configuration file://lifecycle.json
ทำให้ retention เป็นหน้าที่ของ S3 ไม่ใช่ของ script — error ใน cron ก็ไม่กระทบ
Encrypt ก่อน upload (ถ้าข้อมูลเซนซิทีฟ)
ใช้ gpg:
# encrypt ด้วย symmetric key
gpg --batch --yes --passphrase "$BACKUP_PASS" \
--cipher-algo AES256 -c "$LOCAL_PATH"
# ลบ original ไม่ encrypt
rm "$LOCAL_PATH"
# upload .gpg แทน
aws s3 cp "${LOCAL_PATH}.gpg" "$S3_BUCKET/$FILENAME.gpg"
ถ้า S3 ของ public หลุด หรือ AWS account ถูก hack ไฟล์ที่หลุดก็ decrypt ไม่ได้ถ้าไม่มี passphrase
เก็บ passphrase ไว้คนละที่กับ AWS credential เช่น 1Password, Bitwarden
ทางเลือกถูกกว่า S3: Backblaze B2
S3 standard ราคา $0.023/GB/เดือน + egress $0.09/GB ราคาเก็บข้อมูลยาวๆ จะแพง
Backblaze B2 $0.006/GB/เดือน (ถูกกว่า S3 4 เท่า) + 1GB egress ฟรี/วัน
ใช้ rclone แทน aws cli:
# ติดตั้ง
curl https://rclone.org/install.sh | sudo bash
# config (interactive)
rclone config
# ใส่ remote name "b2", type "b2", ใส่ keyID + applicationKey
# upload
rclone copy "$LOCAL_PATH" b2:my-backups/db/
หรือใช้ทั้ง 2 ที่: primary ที่ S3 (สำหรับเรียกใช้บ่อย), secondary ที่ B2 (long-term archive)
ทดสอบ Restore — สำคัญสุด
backup ที่ไม่เคย restore = ไม่ใช่ backup ที่ใช้ได้
ทดสอบ restore ใน server แยกหรือ container:
# Download backup ล่าสุด
aws s3 cp s3://my-app-backups/db/myapp_20260428_020000.sql.gz /tmp/
# สร้าง database ทดสอบ
psql -U postgres -c "CREATE DATABASE test_restore;"
# restore
gunzip -c /tmp/myapp_20260428_020000.sql.gz | psql -U postgres test_restore
# ตรวจสอบ
psql -U postgres test_restore -c "\\dt"
psql -U postgres test_restore -c "SELECT count(*) FROM users;"
# cleanup
psql -U postgres -c "DROP DATABASE test_restore;"
ตั้ง reminder ในปฏิทินทุกเดือนให้ทดสอบ restore
Monitoring ว่า backup ทำงาน
ใช้ Healthchecks.io ฟรี — ใส่ ping URL ใน script:
# ที่ตอนเริ่ม script
curl -fsS -m 10 https://hc-ping.com/<uuid>/start
# ที่ตอนจบ (success)
curl -fsS -m 10 https://hc-ping.com/<uuid>
# ถ้า fail ที่ trap
trap 'curl -fsS -m 10 https://hc-ping.com/<uuid>/fail' ERR
ถ้า script ไม่ ping ภายในเวลาที่ตั้งไว้ Healthchecks จะส่ง email/Slack แจ้ง — รู้ทันทีว่า backup เงียบไป
เช็คลิสต์
- [ ] script backup ทดสอบรันได้
- [ ] cron ตั้งแล้ว + รันสำเร็จอย่างน้อย 1 รอบ
- [ ] S3/B2 lifecycle ตั้งให้ลบ backup เก่าอัตโนมัติ
- [ ] encrypt ก่อน upload (ถ้าข้อมูลเซนซิทีฟ)
- [ ] ทดสอบ restore สำเร็จ
- [ ] monitoring ว่า backup รันทุกคืน
- [ ] credential ของ AWS / DB ไม่อยู่ใน git
สรุป
backup เป็นหนึ่งในงานที่หลายคนเลื่อนทำไปเรื่อยๆ — ขอใช้เวลา 30 นาทีตอนนี้ดีกว่าตอนต้องอธิบายลูกค้าว่า "ข้อมูล 6 เดือนหายไปทั้งหมด"
ตั้งครั้งเดียว ทำงานให้เราตลอดอายุ project
อ่านต่อ
- Monitoring App ด้วย Uptime Kuma — เพิ่ม layer monitoring ให้ครบ
- จัดการ Environment Variables — เก็บ DB password ให้ปลอดภัย