Backup MariaDB Otomatis untuk Homelab
Back to articles

Backup MariaDB Otomatis untuk Homelab

Praktik menjaga data homelab: dump otomatis seluruh database MariaDB dan auto-start semua container saat reboot. Sederhana, tapi menyelamatkan saat gagal.

Backup itu membosankan sampai hari kamu membutuhkannya. Skenario yang mengajarkan saya: satu malam, listrik rumah padam mendadak. UPS tidak ada. PC reboot. Semua container mati. Beberapa container dengan restart: unless-stopped kembali otomatis, tapi beberapa tidak — terutama yang bergantung pada urutan start (MariaDB harus siap sebelum WordPress). Dan ternyata, satu database corrupt karena write yang terputus di tengah.

Artikel ini bukan khotbah tentang pentingnya backup. Ini praktik operasional: dump otomatis seluruh database MariaDB dan auto-start stack saat reboot. Dua hal yang sederhana, tapi menyelamatkan data dan uptime homelab.

TL;DR:

  • Skrip backup men-dump tiap database ke file .sql.gz terpisah (bukan satu dump raksasa) supaya restore selektif
  • Tiga flag mysqldump wajib: --single-transaction, --routines, --events
  • Pakai .my.cnf, jangan password inline — credential tidak bocor di process list
  • Jadwal lewat Windows Task Scheduler, dan start-all.sh untuk auto-start stack dengan urutan dependensi yang benar
  • "Backup yang belum pernah direstore itu belum terbukti" — uji restore bulanan

Yang perlu di-backup, dan yang tidak

Database: ya. Semua database di MariaDB (db_kopi-surgent, db_blog, db_portfolio, dll). Ini data user, post, settings — tidak bisa direcover kalau hilang.

Volume uploads (wp-content/, storage/): ya. File upload user, gambar, attachment. Tidak ada di database, harus dibackup terpisah.

Image container: tidak. Image bisa di-pull ulang dari registry. Backing up image .tar membuang space dan tidak ada nilai (kecuali untuk image custom yang tidak ada di registry).

Container config (docker-compose.yml): tidak perlu backup terpisah. Config sudah ada di /app-stacks/<nama>/docker-compose.yml yang persist di filesystem. Cukup backup filesystem itu sendiri.

Dump semua database sekaligus

Skrip backup men-enumerasi semua database, skip system database, dan dump masing-masing ke file terpisah:

#!/bin/bash
# backup-mariadb.sh
set -euo pipefail

BACKUP_DIR="/opt/data/backups/mariadb/$(date +%Y%m%d)"
mkdir -p "$BACKUP_DIR"

# System database yang di-skip
SKIP_DBS="information_schema performance_schema mysql sys"

# Enumerasi semua database
DATABASES=$(docker exec mariadb mysql -u root -p<DB_PASSWORD> \
    -sN -e "SHOW DATABASES" 2>/dev/null)

for db in $DATABASES; do
    # Skip system database
    if echo "$SKIP_DBS" | grep -qw "$db"; then
        continue
    fi

    echo "Dumping ${db}..."
    docker exec mariadb mysqldump -u root -p<DB_PASSWORD> \
        --single-transaction \
        --routines \
        --events \
        "$db" > "${BACKUP_DIR}/${db}.sql" 2>/dev/null
done

echo "Backup selesai: ${BACKUP_DIR}"

Kenapa file terpisah lebih baik daripada satu dump raksasa? Restore selektif. Kalau hanya db_kopi-surgent yang corrupt, saya restore hanya database itu — bukan seluruh instance MariaDB. Satu dump --all-databases mengharuskan restore semua database sekaligus, yang lebih lambat dan lebih berisiko.

Tiga flag penting di mysqldump:

  • --single-transaction: dump konsisten tanpa lock table. Menggunakan transaction snapshot — database tetap bisa dibaca/tulis saat backup berjalan. Tanpa ini, InnoDB tables ter-lock selama backup.
  • --routines: include stored procedures dan functions. Tanpa ini, routine yang didefinisikan di database hilang saat restore.
  • events: include scheduled events. Beberapa aplikasi pakai MySQL event scheduler untuk task rutin.

Kompresi, penamaan, dan retensi

Backup mentah .sql bisa besar — database WordPress dengan plugin berat bisa 50-200MB per database. Kompresi mengurangi 5-10x:

# Kompresi
gzip "${BACKUP_DIR}/${db}.sql"

# Hasil: db_kopi-surgent.sql.gz (jauh lebih kecil)

Konvensi nama file dengan timestamp:

/opt/data/backups/mariadb/
├── 20260801/
│   ├── db_kopi-surgent.sql.gz
│   ├── db_blog.sql.gz
│   └── db_portfolio.sql.gz
├── 20260802/
│   ├── db_kopi-surgent.sql.gz
│   └── ...
└── 20260806/
    └── ...

Rotasi backup — hapus yang lebih tua dari 30 hari:

# Hapus backup lebih tua dari 30 hari
find /opt/data/backups/mariadb/ -maxdepth 1 -type d -mtime +30 -exec rm -rf {} \;

Retensi 30 hari cukup untuk homelab. Untuk production, pertimbangkan retensi tiered: harian 7 hari, mingguan 4 minggu, bulanan 12 bulan.

Menjadwalkan lewat Task Scheduler Windows

Skrip backup dijalankan lewat Windows Task Scheduler, bukan cron Linux. Kenapa? Karena MariaDB container bergantung pada Docker Desktop yang berjalan di Windows. Task Scheduler memastikan backup berjalan dalam konteks Windows yang benar.

Konfigurasi task (dengan redaksi):

# Register backup task
schtasks /create /tn "Hermes-MariaDB-Backup" \
    /tr "wsl.exe -d Ubuntu -e bash -lc '~/ai-stack/scripts/backup-mariadb.sh >> ~/ai-stack/logs/backup.log 2>&1'" \
    /sc daily /st 03:00 \
    /ru <USERNAME> /f

Catatan keamanan: password database tidak boleh inline di skrip atau command. Gunakan file .my.cnf atau environment variable:

# ~/.my.cnf (di container atau WSL)
[mysqldump]
user=root
password=<DB_PASSWORD>

[mysql]
user=root
password=<DB_PASSWORD>

Dengan .my.cnf, command menjadi mysqldump db_name tanpa flag password. Password tidak muncul di process list atau log. Kalau skrip asli kamu masih pakai password inline, itu adalah catatan perbaikan yang harus segera di-address.

Tiga jebakan Task Scheduler:

  • Task tidak jalan kalau user belum login. Default Task Scheduler butuh user session aktif. Fix: set "Run whether user is logged on or not" + simpan password.
  • Path relatif salah. Task berjalan dengan working directory yang berbeda dari shell interaktif. Gunakan absolute path di skrip.
  • Exit code tidak terpantau. Task "berhasil" bahkan kalau skrip internal gagal, karena exit code schtasks sendiri 0. Fix: skrip menulis ke log file, dan log file di-monitor terpisah.

Auto-start seluruh stack saat boot

Docker punya restart: unless-stopped yang auto-restart container kalau Docker daemon start. Tapi ini tidak cukup. Masalahnya: urutan dependensi.

WordPress butuh MariaDB siap sebelum connect. Next.js butuh API backend siap sebelum serve. Cloudflared butuh nginx siap sebelum tunnel connect. Kalau semua container start bersamaan, ada race condition: WordPress start sebelum MariaDB siap → "Error establishing database connection" → WordPress crash → restart → coba lagi → gagal lagi.

Solusi: skrip start-all.sh yang start container dalam urutan dependensi yang benar, dengan tunggu-siap:

#!/bin/bash
# start-all.sh — start full stack dengan urutan dependensi
set -euo pipefail

echo "Starting MariaDB..."
docker start mariadb 2>/dev/null || true

# Tunggu MariaDB siap
echo "Waiting for MariaDB..."
for i in $(seq 1 30); do
    if docker exec mariadb mysqladmin ping -u root -p<DB_PASSWORD> --silent 2>/dev/null; then
        echo "MariaDB ready"
        break
    fi
    sleep 2
done

echo "Starting site-server (nginx)..."
docker start site-server 2>/dev/null || true
sleep 3

echo "Starting cloudflared..."
docker start cloudflared 2>/dev/null || true
sleep 3

echo "Starting app containers..."
for container in $(docker ps -a --format "{{.Names}}" --filter "label=app=true" | sort); do
    echo "  Starting ${container}..."
    docker start "$container" 2>/dev/null || true
    sleep 5  # jeda antar container untuk menghindari resource spike
done

echo "All containers started"
docker ps --format "table {{.Names}}\t{{.Status}}"

Kenapa restart: unless-stopped saja tidak cukup? Karena Docker tidak tahu urutan dependensi antar container. Docker compose punya depends_on dengan health check, tapi depends_on hanya berlaku saat docker compose up — tidak saat Docker daemon restart dan container auto-restart individual.

Restore: bagian yang selalu dilupakan

Backup yang belum pernah direstore itu belum terbukti. Skenario: database corrupt, kamu butuh restore. Kamu buka backup .sql.gz, decompress, pipa ke mysql — dan ternyata ada error karena charset, atau routine tidak ter-restore, atau data tidak konsisten.

Prosedur restore satu database:

#!/bin/bash
# restore-database.sh
set -euo pipefail

BACKUP_FILE=$1
DB_NAME=$2

if [ -z "$BACKUP_FILE" ] || [ -z "$DB_NAME" ]; then
    echo "Usage: $0 <backup-file.sql.gz> <database-name>"
    exit 1
fi

echo "Restoring ${DB_NAME} from ${BACKUP_FILE}..."

# Decompress dan pipe ke mysql
gunzip -c "$BACKUP_FILE" | docker exec -i mariadb mysql -u root -p<DB_PASSWORD> "$DB_NAME"

echo "Restore selesai. Verifikasi:"
docker exec mariadb mysql -u root -p<DB_PASSWORD> "$DB_NAME" -e "SHOW TABLES"

Wajib: uji restore ke database sementara sebelum benar-benar butuh. Prosedur test restore:

# Buat database sementara
docker exec mariadb mysql -u root -p<DB_PASSWORD> -e \
    "CREATE DATABASE test_restore"

# Restore ke database sementara
gunzip -c /opt/data/backups/mariadb/20260806/db_kopi-surgent.sql.gz | \
    docker exec -i mariadb mysql -u root -p<DB_PASSWORD> test_restore

# Verifikasi: cek jumlah table dan row
docker exec mariadb mysql -u root -p<DB_PASSWORD> test_restore -e \
    "SELECT COUNT(*) FROM wp_posts"

# Bersihkan
docker exec mariadb mysql -u root -p<DB_PASSWORD> -e \
    "DROP DATABASE test_restore"

Kalau restore ke database sementara berhasil dan data cocok, kamu tahu backup itu valid. Lakukan test restore ini minimal sekali sebulan.

Yang masih kurang di setup saya

Off-site copy. Backup saat ini hanya di local filesystem (/opt/data/backups/). Kalau hard drive fisik rusak, backup hilang bersama data. Solusi: copy backup ke cloud storage (S3, Backblaze B2) atau external drive. Belum diimplementasikan.

Enkripsi backup. Backup .sql.gz tidak terenkripsi. Siapapun yang bisa akses filesystem bisa membaca semua data. Solusi: gpg --symmetric atau age encryption sebelum copy off-site.

Monitoring backup gagal diam-diam. "Backup gagal tanpa notifikasi" adalah mode kegagalan terburuk. Kamu pikir backup berjalan setiap hari, tapi skrip crash di hari ke-3 dan diam. Solusi: skrip backup mengirim notifikasi (Discord webhook, email) saat sukses DAN saat gagal. Kalau notifikasi sukses tidak datang, itu tanda ada masalah.

Langkah berikutnya

Backup melindungi infrastruktur self-hosting banyak website. Sistem lengkap yang dilindungi backup ini dibahas di AI agent yang deploy website otomatis.

FAQ

Seberapa sering backup harus dijalankan?

Harus untuk homelab. Database WordPress berubah setiap kali ada post baru, comment, atau setting change. Backup harian pada jam low-traffic (3 pagi) adalah minimum. Kalau situs aktif dengan transaksi, pertimbangkan backup lebih sering.

Berapa lama backup disimpan?

Retensi tergantung kebutuhan. Saya pakai 30 hari untuk homelab. Untuk compliance atau situs klien, pertimbangkan retensi yang lebih panjang (90 hari atau lebih). Hapus backup lama secara otomatis dengan find -mtime.

Apakah backup ini cukup untuk disaster recovery?

Tidak lengkap. Backup database saja tidak mengembalikan: file upload (wp-content), konfigurasi container, dan nginx rules. Untuk disaster recovery penuh, backup juga filesystem /app-stacks/ dan config nginx.

Bagaimana kalau MariaDB corrupt dan tidak bisa start?

Itu skenario terburuk. Dalam kasus ini, container MariaDB harus di-recreate dari image, lalu semua database di-restore dari backup. Ini butuh downtime penuh. Pastikan backup terbaru valid sebelum recreate.

Kesimpulan

Backup database MariaDB otomatis dan auto-start stack adalah dua prosedur operasional yang tidak glamorous, tapi menyelamatkan saat hal buruk terjadi. Tiga prinsip kunci: backup per-database (bukan satu dump raksasa), gunakan .my.cnf bukan password inline, dan uji restore secara berkala.

"Backup yang belum pernah direstore itu belum terbukti." Lakukan test restore bulanan. Satu skrip, lima menit, damai pikiran.

Kalau infrastruktur backup ini menarik dalam konteks self-hosting lengkap, lanjut baca self-host banyak website di satu server dengan Cloudflare Tunnel.

More Articles