Technology

Directus CMS Backup Recovery Strategy — ระบบสำรองข้อมูลและกู้คืนสำหรับ Headless CMS

directus cms backup recovery strategy
Directus CMS Backup Recovery Strategy | SiamCafe Blog
2026-05-24· อ. บอม — SiamCafe.net· 8,575 คำ

Directus CMS คืออะไรและทำไมต้องมี Backup Strategy

Directus เป็น Headless CMS แบบ Open Source ที่สร้าง REST และ GraphQL API จาก Database Schema โดยอัตโนมัติ รองรับ Database หลายตัวทั้ง PostgreSQL, MySQL, MariaDB, SQLite, MS SQL Server และ Oracle ข้อดีของ Directus คือไม่สร้าง custom table แต่ใช้ database ของคุณโดยตรงทำให้ data portability สูงและไม่ lock-in

การมี Backup Strategy ที่ดีสำหรับ Directus มีความสำคัญเพราะ CMS เก็บทั้งข้อมูลเนื้อหา ข้อมูลผู้ใช้ ไฟล์สื่อ (รูปภาพ วิดีโอ เอกสาร) และ configuration ต่างๆ ถ้าข้อมูลสูญหายจะกระทบทั้งเว็บไซต์และ API ที่ downstream application ใช้งาน

Backup Strategy ที่สมบูรณ์สำหรับ Directus ต้องครอบคลุม 3 ส่วนหลักคือ Database Backup ที่เก็บข้อมูลทั้งหมดรวมถึง Directus system tables, File Storage Backup ที่เก็บไฟล์สื่อทั้งหมด และ Configuration Backup ที่เก็บ environment variables และ schema snapshot

แนวทาง 3-2-1 Backup Rule ยังคงเป็นมาตรฐานที่แนะนำคือเก็บ backup อย่างน้อย 3 ชุด บน storage อย่างน้อย 2 ประเภท โดยมีอย่างน้อย 1 ชุดอยู่ offsite เพื่อป้องกัน disaster ที่อาจเกิดขึ้นกับ data center หลัก

ติดตั้ง Directus และตั้งค่า Database

ติดตั้ง Directus ด้วย Docker Compose พร้อม PostgreSQL เป็น production database

# docker-compose.yml สำหรับ Directus Production
version: "3.9"
services:
  postgres:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: directus
      POSTGRES_USER: directus
      POSTGRES_PASSWORD: ""
    volumes:
      - pg_data:/var/lib/postgresql/data
      - ./backups/db:/backups
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U directus"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - directus-net

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    command: redis-server --requirepass ""
    volumes:
      - redis_data:/data
    networks:
      - directus-net

  directus:
    image: directus/directus:10.10.0
    restart: unless-stopped
    ports:
      - "8055:8055"
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_started
    environment:
      KEY: ""
      SECRET: ""
      DB_CLIENT: pg
      DB_HOST: postgres
      DB_PORT: 5432
      DB_DATABASE: directus
      DB_USER: directus
      DB_PASSWORD: ""
      CACHE_ENABLED: "true"
      CACHE_STORE: redis
      REDIS_HOST: redis
      REDIS_PORT: 6379
      REDIS_PASSWORD: ""
      STORAGE_LOCATIONS: "local, s3"
      STORAGE_LOCAL_ROOT: "/directus/uploads"
      STORAGE_S3_DRIVER: s3
      STORAGE_S3_KEY: ""
      STORAGE_S3_SECRET: ""
      STORAGE_S3_BUCKET: ""
      STORAGE_S3_REGION: "ap-southeast-1"
      ADMIN_EMAIL: "admin@example.com"
      ADMIN_PASSWORD: ""
    volumes:
      - uploads:/directus/uploads
      - ./extensions:/directus/extensions
      - ./snapshots:/directus/snapshots
    networks:
      - directus-net

volumes:
  pg_data:
  redis_data:
  uploads:

networks:
  directus-net:

# สร้าง .env file
# DB_PASSWORD=secure-db-password-here
# REDIS_PASSWORD=secure-redis-password
# DIRECTUS_KEY=random-key-here
# DIRECTUS_SECRET=random-secret-here
# ADMIN_PASSWORD=admin-password-here
# S3_KEY=your-s3-key
# S3_SECRET=your-s3-secret
# S3_BUCKET=directus-uploads

# เริ่มต้นระบบ
# docker compose up -d

สร้าง Schema Snapshot เพื่อเก็บ configuration ของ Directus

# สร้าง Schema Snapshot (เก็บ collections, fields, relations, permissions)
docker compose exec directus npx directus schema snapshot ./snapshots/snapshot-$(date +%Y%m%d-%H%M%S).yaml

# ดูรายการ snapshot
ls -la snapshots/
# snapshot-20260228-150000.yaml
# snapshot-20260227-150000.yaml

# Apply snapshot (สำหรับ restore)
docker compose exec directus npx directus schema apply ./snapshots/snapshot-20260228-150000.yaml

# เปรียบเทียบ schema กับ snapshot
docker compose exec directus npx directus schema diff ./snapshots/snapshot-20260228-150000.yaml

สร้างระบบ Automated Backup ด้วย Cron และ Script

สร้าง backup script ที่เก็บทั้ง database, files และ configuration

#!/bin/bash
# backup_directus.sh — Automated Backup Script สำหรับ Directus
set -euo pipefail

# Configuration
BACKUP_DIR="/opt/directus/backups"
RETENTION_DAYS=30
DATE=$(date +%Y%m%d-%H%M%S)
BACKUP_PATH="/"
DB_CONTAINER="directus-postgres-1"
DIRECTUS_CONTAINER="directus-directus-1"
LOG_FILE="/backup.log"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

# สร้าง directory
mkdir -p ""/{db, files, config}

log "Starting backup: "

# 1. Database Backup (pg_dump)
log "Backing up database..."
docker exec "$DB_CONTAINER" pg_dump -U directus -Fc directus \
    > "/db/directus.dump" 2>> "$LOG_FILE"

DB_SIZE=$(du -sh "/db/directus.dump" | cut -f1)
log "Database backup complete: "

# 2. Schema Snapshot
log "Creating schema snapshot..."
docker exec "$DIRECTUS_CONTAINER" npx directus schema snapshot \
    /directus/snapshots/backup-.yaml 2>> "$LOG_FILE"
cp "$(docker inspect --format='{{range .Mounts}}{{if eq .Destination "/directus/snapshots"}}{{.Source}}{{end}}{{end}}' $DIRECTUS_CONTAINER)/backup-.yaml" \
    "/config/" 2>> "$LOG_FILE" || true

# 3. File Storage Backup
log "Backing up uploaded files..."
docker cp ":/directus/uploads" "/files/"
FILES_SIZE=$(du -sh "/files/" | cut -f1)
log "Files backup complete: "

# 4. Environment Config
log "Backing up configuration..."
cp docker-compose.yml "/config/"
cp .env "/config/env.backup"
cp -r extensions/ "/config/extensions/" 2>/dev/null || true

# 5. สร้าง compressed archive
log "Compressing backup..."
cd "$BACKUP_DIR"
tar czf ".tar.gz" "/"
rm -rf "/"
ARCHIVE_SIZE=$(du -sh ".tar.gz" | cut -f1)
log "Archive created: "

# 6. Checksum
sha256sum ".tar.gz" > ".tar.gz.sha256"

# 7. Cleanup old backups
log "Cleaning up backups older than  days..."
find "$BACKUP_DIR" -name "*.tar.gz" -mtime + -delete
find "$BACKUP_DIR" -name "*.sha256" -mtime + -delete

log "Backup completed successfully: .tar.gz ()"

# 8. ส่ง notification
if [ -n "" ]; then
    curl -s -X POST "$SLACK_WEBHOOK" \
        -H "Content-Type: application/json" \
        -d "{\"text\":\"Directus Backup OK:  (DB: , Files: )\"}"
fi

ตั้ง cron job ให้ backup อัตโนมัติ

# แก้ไข crontab
sudo crontab -e

# Backup ทุกวันตี 2
0 2 * * * /opt/directus/backup_directus.sh >> /var/log/directus-backup.log 2>&1

# Schema snapshot ทุก 6 ชั่วโมง
0 */6 * * * docker exec directus-directus-1 npx directus schema snapshot /directus/snapshots/auto-$(date +\%Y\%m\%d-\%H\%M\%S).yaml

# ตรวจสอบ cron job
crontab -l
sudo systemctl status cron

ตั้งค่า Offsite Backup ไปยัง S3 และ Restic

ใช้ Restic สำหรับ incremental backup ที่ encrypted ไปยัง S3

# ติดตั้ง Restic
sudo apt install restic -y

# Initialize Restic Repository บน S3
export AWS_ACCESS_KEY_ID="your-access-key"
export AWS_SECRET_ACCESS_KEY="your-secret-key"
export RESTIC_REPOSITORY="s3:s3.amazonaws.com/directus-backups-prod"
export RESTIC_PASSWORD="strong-encryption-password"

restic init

# Backup ไปยัง S3 (incremental)
restic backup /opt/directus/backups/ \
    --tag directus \
    --tag production \
    --exclude "*.log" \
    --verbose

# ดูรายการ snapshots
restic snapshots
# ID        Time                 Host       Tags
# abc123    2026-02-28 02:00:00  prod-srv   directus, production
# def456    2026-02-27 02:00:00  prod-srv   directus, production

# ตรวจสอบ integrity
restic check

# Cleanup old snapshots (เก็บ 7 daily, 4 weekly, 6 monthly)
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune

# สร้าง script สำหรับ offsite backup อัตโนมัติ
cat > /opt/directus/offsite_backup.sh << 'SCRIPT'
#!/bin/bash
set -euo pipefail

export AWS_ACCESS_KEY_ID="your-access-key"
export AWS_SECRET_ACCESS_KEY="your-secret-key"
export RESTIC_REPOSITORY="s3:s3.amazonaws.com/directus-backups-prod"
export RESTIC_PASSWORD_FILE="/opt/directus/.restic-password"

# Backup
restic backup /opt/directus/backups/ \
    --tag directus --tag "$(date +%A)" \
    --exclude "*.log"

# Prune old snapshots
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune

# Verify
restic check --read-data-subset=5%

echo "[$(date)] Offsite backup completed"
SCRIPT

chmod +x /opt/directus/offsite_backup.sh

# เพิ่ม cron job
# 0 4 * * * /opt/directus/offsite_backup.sh >> /var/log/restic-backup.log 2>&1

Recovery Procedures สำหรับทุกสถานการณ์

สร้าง Recovery Script สำหรับหลายสถานการณ์

#!/bin/bash
# restore_directus.sh — Restore Script สำหรับ Directus
set -euo pipefail

BACKUP_DIR="/opt/directus/backups"
RESTORE_DIR="/tmp/directus-restore"

usage() {
    echo "Usage: $0  [full|db|files|config]"
    echo "  full   - Restore ทั้งหมด (default)"
    echo "  db     - Restore เฉพาะ database"
    echo "  files  - Restore เฉพาะ uploaded files"
    echo "  config - Restore เฉพาะ configuration"
    exit 1
}

[ $# -lt 1 ] && usage

BACKUP_FILE="$1"
RESTORE_TYPE=""

# ตรวจสอบ backup file
if [ ! -f "$BACKUP_FILE" ]; then
    echo "ERROR: Backup file not found: $BACKUP_FILE"
    exit 1
fi

# ตรวจสอบ checksum
if [ -f ".sha256" ]; then
    echo "Verifying checksum..."
    sha256sum -c ".sha256" || { echo "CHECKSUM FAILED"; exit 1; }
fi

# Extract backup
echo "Extracting backup..."
mkdir -p "$RESTORE_DIR"
tar xzf "$BACKUP_FILE" -C "$RESTORE_DIR"
EXTRACTED=$(ls "$RESTORE_DIR")

restore_db() {
    echo "=== Restoring Database ==="
    echo "Stopping Directus..."
    docker compose stop directus

    echo "Dropping and recreating database..."
    docker exec directus-postgres-1 psql -U directus -c "DROP DATABASE IF EXISTS directus_old;"
    docker exec directus-postgres-1 psql -U directus -c "ALTER DATABASE directus RENAME TO directus_old;"
    docker exec directus-postgres-1 psql -U directus -c "CREATE DATABASE directus OWNER directus;"

    echo "Restoring from dump..."
    docker cp "//db/directus.dump" directus-postgres-1:/tmp/
    docker exec directus-postgres-1 pg_restore -U directus -d directus /tmp/directus.dump

    echo "Starting Directus..."
    docker compose start directus

    echo "Database restore complete"
}

restore_files() {
    echo "=== Restoring Files ==="
    docker cp "//files/uploads/." directus-directus-1:/directus/uploads/
    echo "Files restore complete"
}

restore_config() {
    echo "=== Restoring Configuration ==="
    SNAPSHOT=$(ls "//config/"*.yaml 2>/dev/null | head -1)
    if [ -n "$SNAPSHOT" ]; then
        docker cp "$SNAPSHOT" directus-directus-1:/directus/snapshots/restore.yaml
        docker exec directus-directus-1 npx directus schema apply /directus/snapshots/restore.yaml --yes
        echo "Schema snapshot applied"
    fi
    echo "Config restore complete"
}

case "$RESTORE_TYPE" in
    full)
        restore_db
        restore_files
        restore_config
        ;;
    db) restore_db ;;
    files) restore_files ;;
    config) restore_config ;;
    *) usage ;;
esac

# Cleanup
rm -rf "$RESTORE_DIR"
echo "Restore completed successfully"

Restore จาก Restic (offsite backup)

# ดู snapshots ที่มี
restic snapshots --tag directus

# Restore snapshot ล่าสุด
restic restore latest --target /tmp/restic-restore --tag directus

# Restore snapshot เฉพาะ
restic restore abc123 --target /tmp/restic-restore

# Restore เฉพาะไฟล์บางตัว
restic restore latest --target /tmp/restic-restore \
    --include "/opt/directus/backups/20260228*.tar.gz"

# Mount snapshot เป็น filesystem (สำหรับดูข้อมูลก่อน restore)
restic mount /mnt/restic-mount &
ls /mnt/restic-mount/snapshots/latest/
fusermount -u /mnt/restic-mount

ทดสอบ Disaster Recovery ด้วย Automated Testing

สร้าง automated test ที่ทดสอบกระบวนการ backup และ restore ทุกสัปดาห์

#!/usr/bin/env python3
# dr_test.py — Disaster Recovery Test สำหรับ Directus
import subprocess
import requests
import json
import time
import sys
import os

DIRECTUS_URL = os.getenv("DIRECTUS_URL", "http://localhost:8055")
ADMIN_TOKEN = os.getenv("DIRECTUS_ADMIN_TOKEN", "")
BACKUP_DIR = "/opt/directus/backups"
TEST_DIR = "/tmp/dr-test"
SLACK_WEBHOOK = os.getenv("SLACK_WEBHOOK", "")

class DRTest:
    def __init__(self):
        self.results = []
        self.test_data_id = None

    def log(self, msg):
        print(f"[DR-TEST] {msg}")

    def run_cmd(self, cmd):
        result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
        return result.returncode == 0, result.stdout, result.stderr

    def test_create_test_data(self):
        """สร้างข้อมูลทดสอบ"""
        r = requests.post(f"{DIRECTUS_URL}/items/dr_test", json={
            "test_name": f"DR-Test-{int(time.time())}",
            "test_value": "backup-recovery-validation",
            "timestamp": int(time.time())
        }, headers={"Authorization": f"Bearer {ADMIN_TOKEN}"})
        if r.status_code in [200, 204]:
            self.test_data_id = r.json().get("data", {}).get("id")
            return True, f"Created test data: {self.test_data_id}"
        return False, f"Failed: {r.status_code} {r.text}"

    def test_backup_execution(self):
        """ทดสอบว่า backup script ทำงานได้"""
        ok, out, err = self.run_cmd("/opt/directus/backup_directus.sh")
        if ok:
            latest = max(
                [f for f in os.listdir(BACKUP_DIR) if f.endswith('.tar.gz')],
                key=lambda x: os.path.getmtime(os.path.join(BACKUP_DIR, x))
            )
            size = os.path.getsize(os.path.join(BACKUP_DIR, latest))
            return True, f"Backup OK: {latest} ({size} bytes)"
        return False, f"Backup failed: {err}"

    def test_backup_integrity(self):
        """ตรวจสอบ checksum ของ backup"""
        latest = max(
            [f for f in os.listdir(BACKUP_DIR) if f.endswith('.tar.gz')],
            key=lambda x: os.path.getmtime(os.path.join(BACKUP_DIR, x))
        )
        sha_file = os.path.join(BACKUP_DIR, f"{latest}.sha256")
        if os.path.exists(sha_file):
            ok, _, _ = self.run_cmd(f"cd {BACKUP_DIR} && sha256sum -c {latest}.sha256")
            return ok, f"Checksum {'OK' if ok else 'FAILED'}: {latest}"
        return False, "No checksum file found"

    def test_restore_db(self):
        """ทดสอบ restore database บน test environment"""
        latest = max(
            [f for f in os.listdir(BACKUP_DIR) if f.endswith('.tar.gz')],
            key=lambda x: os.path.getmtime(os.path.join(BACKUP_DIR, x))
        )
        ok, _, err = self.run_cmd(
            f"cd /opt/directus && /opt/directus/restore_directus.sh {BACKUP_DIR}/{latest} db"
        )
        return ok, f"DB Restore {'OK' if ok else 'FAILED'}"

    def test_verify_data_after_restore(self):
        """ตรวจสอบว่าข้อมูลทดสอบยังอยู่หลัง restore"""
        time.sleep(10)  # รอ Directus start
        try:
            r = requests.get(
                f"{DIRECTUS_URL}/items/dr_test/{self.test_data_id}",
                headers={"Authorization": f"Bearer {ADMIN_TOKEN}"},
                timeout=30
            )
            if r.status_code == 200:
                data = r.json().get("data", {})
                if data.get("test_value") == "backup-recovery-validation":
                    return True, "Test data verified after restore"
            return False, f"Data verification failed: {r.status_code}"
        except Exception as e:
            return False, f"Connection error: {e}"

    def run_all_tests(self):
        tests = [
            ("Create Test Data", self.test_create_test_data),
            ("Backup Execution", self.test_backup_execution),
            ("Backup Integrity", self.test_backup_integrity),
            ("Restore Database", self.test_restore_db),
            ("Verify Data", self.test_verify_data_after_restore),
        ]

        self.log("Starting DR Test Suite")
        for name, test_fn in tests:
            try:
                passed, detail = test_fn()
                status = "PASS" if passed else "FAIL"
                self.results.append((name, status, detail))
                self.log(f"  [{status}] {name}: {detail}")
            except Exception as e:
                self.results.append((name, "ERROR", str(e)))
                self.log(f"  [ERROR] {name}: {e}")

        failed = [r for r in self.results if r[1] != "PASS"]
        summary = f"DR Test: {len(self.results)-len(failed)}/{len(self.results)} passed"
        self.log(summary)

        if SLACK_WEBHOOK:
            emoji = "white_check_mark" if not failed else "x"
            requests.post(SLACK_WEBHOOK, json={"text": f":{emoji}: {summary}"})

        return len(failed) == 0

if __name__ == "__main__":
    dr = DRTest()
    success = dr.run_all_tests()
    sys.exit(0 if success else 1)

FAQ คำถามที่พบบ่อย

Q: ควร backup Directus บ่อยแค่ไหน?

A: ขึ้นอยู่กับความถี่ในการอัพเดทเนื้อหา สำหรับเว็บที่อัพเดททุกวันแนะนำ database backup ทุก 6 ชั่วโมง file backup ทุกวัน และ schema snapshot ทุกครั้งที่เปลี่ยน schema สำหรับเว็บที่อัพเดทไม่บ่อย backup วันละ 1 ครั้งเพียงพอ

Q: pg_dump กับ pg_basebackup ต่างกันอย่างไร?

A: pg_dump สร้าง logical backup ที่เป็น SQL statements หรือ custom format สามารถ restore เฉพาะบาง table ได้ ส่วน pg_basebackup สร้าง physical backup ของทั้ง database cluster เร็วกว่าสำหรับ database ขนาดใหญ่และรองรับ Point-in-Time Recovery (PITR) ด้วย WAL archiving สำหรับ Directus ขนาดเล็กถึงกลาง pg_dump เพียงพอ

Q: Restic กับ Borg Backup ต่างกันอย่างไร?

A: ทั้งคู่เป็น incremental encrypted backup tools แต่ Restic เขียนด้วย Go รองรับ backend หลายตัวทั้ง S3 B2 Azure GCS และ SFTP ส่วน Borg เขียนด้วย Python/C รองรับเฉพาะ local และ SSH ข้อดีของ Borg คือ compression ดีกว่าและ deduplication มีประสิทธิภาพสูงกว่า แต่ Restic ใช้งานง่ายกว่าและรองรับ cloud storage โดยตรง

Q: ควรทดสอบ restore บ่อยแค่ไหน?

A: แนะนำทดสอบ restore อย่างน้อยเดือนละ 1 ครั้ง สำหรับระบบที่สำคัญมากควรทดสอบทุกสัปดาห์ด้วย automated DR test script การมี backup ที่ restore ไม่ได้ก็ไม่ต่างจากไม่มี backup เลย

Q: Directus Schema Snapshot คืออะไรและจำเป็นต้อง backup ไหม?

A: Schema Snapshot เก็บ metadata ของ Directus เช่น collections, fields, relations, permissions และ flows ในรูปแบบ YAML จำเป็นต้อง backup เพราะถ้า restore เฉพาะ database แล้วไม่มี snapshot อาจทำให้ Directus UI แสดงผลไม่ถูกต้อง Schema Snapshot ยังใช้สำหรับ migrate configuration ระหว่าง environment ได้อีกด้วย

📖 บทความที่เกี่ยวข้อง

Grafana Tempo Traces Backup Recovery Strategyอ่านบทความ → Terraform State Backup Recovery Strategyอ่านบทความ → Directus CMS Feature Flag Managementอ่านบทความ → Python FastAPI Backup Recovery Strategyอ่านบทความ → Directus CMS Automation Scriptอ่านบทความ →

📚 ดูบทความทั้งหมด →