← กลับ
DatabaseBackupAutomationPostgreSQL

Backup Database อัตโนมัติทุกคืนด้วย Cron + S3

เคยลบตารางผิดไหม? ตั้ง cron + S3 ครั้งเดียว คืนๆ ระบบ backup ให้เอง พังเมื่อไหร่ก็กู้ได้

2026-04-28อ่าน 5 นาทีใหม่

เรื่องที่อยากให้คุณรู้ก่อนตั้ง backup

หลายคนคิดว่า "host บน managed service เช่น RDS, Supabase ก็ pointer-in-time recovery มีอยู่แล้ว ไม่ต้อง backup เอง"

ถูกครึ่งหนึ่ง — managed service มี backup ของเขา แต่:

  1. ถ้า account คุณโดน lock / โดน hack คุณเข้าไปกู้ไม่ได้
  2. ถ้า provider ปิดบริการ (เคยมีหลายเจ้า) — ข้อมูลอยู่กับเขา
  3. ถ้าเขาเรียกราคาเพิ่ม คุณก็ทำอะไรไม่ได้

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

อ่านต่อ

← ดูบทความอื่น