Database DevOps — DevOps

Mengelola Database di Production Database adalah komponen paling kritis — data loss tidak bisa di-undo. Database DevOps memastikan database aman, performant…

Database DevOps adalah cara menjalankan database aplikasi di production dengan aman: siapa yang mengurusnya, bagaimana strukturnya berubah saat deploy, bagaimana datanya dicadangkan dan dipulihkan, dan berapa banyak koneksi yang sanggup dia layani. Bayangin buku kas sebuah toko. Kasirnya bisa ganti shift kapan saja dan mesin kasirnya bisa diganti baru, tapi buku kasnya cuma satu: kalau hilang, semua catatan penjualan ikut hilang. Sepanjang track ini, container app catatan-api sudah sering kita hapus dan buat ulang tanpa masalah. Database catatan di Postgres adalah buku kasnya: dia yang gak boleh hilang.

🧪 Tebakan cepat: script backup catatan-api ditulis dengan set -eu, tanpa pipefail. Malam ini jam 02:00, container db sedang mati karena upgrade. Besok paginya, file backup malam itu ada di foldernya. Berapa ukurannya, dan apa exit code script-nya? Tebak dulu, jawabannya ada di bagian "Restore drill".

Managed atau self-hosted

Sekarang Postgres catatan-api jalan sebagai container di web-1 (self-hosted), dengan data di named volume db-data. Pilihan lainnya adalah managed database: Postgres yang dijalankan penyedia cloud (AWS RDS, DigitalOcean Managed Databases, Google Cloud SQL, Supabase, Neon), dan app-mu cukup menyambung ke alamatnya.

Self-hosted di web-1 Managed
Backup harian kamu tulis script dan cron-nya otomatis, biasanya plus point-in-time recovery
Server cadangan (standby) kamu yang menyiapkan tinggal centang, failover otomatis
Upgrade dan patch kamu yang menjalankan dijadwalkan penyedia
Kalau web-1 mati app dan database mati bersamaan database tetap hidup di tempat lain
Biaya gak ada tambahan bayar per bulan, lebih mahal dari container
Kontrol penuh, semua extension dan setting terbatas pada yang disediakan penyedia

Untuk app dengan pengguna nyata dan tim tanpa DBA, managed biasanya paling murah kalau waktu ikut dihitung. Self-hosted tetap sah, asal backup, restore drill, dan upgrade benar-benar dikerjakan. Bagian berikutnya berlaku untuk dua-duanya.

Migration di pipeline deploy

Database migration adalah perubahan skema (tambah tabel, tambah kolom) yang ditulis sebagai file di repo, ikut di-review dan di-version control seperti kode, lalu diterapkan otomatis saat deploy. Di pipeline catatan-api, migration jalan sekali, setelah image baru ditarik dan sebelum container baru menyala:

docker compose pull app
docker compose run --rm app node migrate.js   # container sekali pakai dengan image baru
docker compose up -d                          # baru ganti container app

Di Laravel, langkah yang sama adalah php artisan migrate --force. Di production Laravel menanyakan konfirmasi dulu, dan pipeline gak bisa menjawab, jadi --force melewati pertanyaan itu.

Selama beberapa detik, kode lama jalan di atas skema baru: migration sudah selesai, container lama belum diganti. Kalau migration mengganti nama kolom isi jadi konten dalam satu langkah, setiap SELECT isi dari kode lama gagal dengan column "isi" does not exist sampai container baru menyala. Karena itu migration di production memakai pola expand/contract: deploy pertama cuma menambah (kolom konten baru, kode menulis ke keduanya), deploy berikutnya baru menghapus yang lama setelah gak ada kode yang memakainya. Langkah lengkapnya ada di pelajaran Schema Migration Patterns, track Database.

Di CI, database test dibuat baru setiap run: migration dijalankan dari nol, lalu seeder mengisi data contoh yang sama setiap kali. Dengan data yang konsisten, test yang lulus hari ini juga lulus besok, bukan tergantung sisa data run sebelumnya.

Backup terjadwal

Ini script backup catatan-api, sama dengan yang ada di Lab:

#!/usr/bin/env bash
set -euo pipefail
cd /srv/catatan-api
BERKAS="/var/backups/catatan/catatan-$(date +%F).sql.gz"
docker compose exec -T db pg_dump -U catatan catatan | gzip > "$BERKAS"
find /var/backups/catatan -name 'catatan-*.sql.gz' -mtime +7 -delete
# crontab root: tiap hari jam 02:00
0 2 * * * /srv/catatan-api/backup.sh >> /var/log/backup-catatan.log 2>&1

Jadwal tiga backup pertamanya, dalam format latihan:

Backup Waktu Boleh dihapus setelah
1 Day 1 02:00 Day 8
2 Day 2 02:00 Day 9
3 Day 3 02:00 Day 10

Backup yang cuma ada di web-1 ikut hilang kalau disk web-1 rusak. Salin juga ke tempat lain, misalnya bucket object storage (pelajaran berikutnya). Seberapa sering backup dan seberapa cepat harus pulih (RPO dan RTO) dibahas di pelajaran Backup & Recovery, track Database.

Restore drill

Restore drill adalah latihan memulihkan backup ke database cadangan, secara rutin, untuk membuktikan backup-nya bisa dipakai. Ini drill untuk backup terbaru, persis seperti di Lab:

$ docker compose exec -T db createdb -U catatan catatan_drill
$ gunzip -c /var/backups/catatan/catatan-2026-10-03.sql.gz | docker compose exec -T db psql -q -U catatan -d catatan_drill
$ docker compose exec -T db psql -U catatan -d catatan_drill -c "SELECT count(*) FROM catatan;"
 count 
-------
  1358
(1 row)

$ docker compose exec -T db dropdb -U catatan catatan_drill

1.358 catatan, sama dengan production saat backup dibuat: backup ini terbukti. dropdb membuang database latihannya lagi. Sekarang malam Day 4, saat db mati. Tanpa pipefail, status sebuah pipe adalah status perintah terakhirnya (pelajaran Shell Scripting). docker compose exec gagal dengan service "db" is not running, tapi gzip sukses memampatkan input kosong, jadi script lanjut dan keluar dengan exit code 0. File-nya ada, ukurannya 20 byte. Itu jawaban tebakannya. Gak ada yang curiga, sampai drill:

$ docker compose exec -T db psql -U catatan -d catatan_drill -c "SELECT count(*) FROM catatan;"
ERROR:  relation "catatan" does not exist
LINE 1: SELECT count(*) FROM catatan;
                             ^

Restore-nya "berhasil" tanpa pesan apa pun, karena gak ada isinya. Dengan set -euo pipefail, script berhenti dengan exit code 1, cron mencatat error-nya, dan monitoring bisa memberi tahu. Tapi cuma drill yang membuktikan backup benar-benar bisa dipulihkan.

Batas koneksi

Postgres membatasi jumlah koneksi dengan max_connections, bawaannya 100. Tiap container catatan-api punya pool (kumpulan koneksi yang dipakai bergantian), dan Pool milik node-postgres membuka sampai 10 koneksi. Hitungannya sederhana:

Container app Pool per container Koneksi app + psql, backup, migration Total
1 10 10 3 13
3 10 30 3 33
10 10 100 3 103: lewat batas

Koneksi ke-101 ditolak, dan app mencatat Gagal konek ke database: sorry, too many clients already. Saat menambah replicas, hitung ulang: replicas × pool + koneksi lain harus di bawah max_connections. Kalau gak cukup, kecilkan pool per container atau pasang PgBouncer (pelajaran Connection Pooling, track Database). Sebagian managed database kecil punya batas jauh di bawah 100, jadi cek angkanya di dashboard penyedia.

Latihan: generateBackupSchedule

Latihan lesson ini menghitung tabel jadwal di atas. generateBackupSchedule(config) menerima { dbName, frequency, retention, startHour } dan mengembalikan 3 backup pertama, tiap item { database, time, deleteAfter }:

⚠️ Jebakan yang sering ketemu

TL;DR: Database adalah bagian production yang gak boleh hilang. Managed database (RDS, Cloud SQL, DigitalOcean) mengurus backup, standby, dan upgrade dengan biaya bulanan; self-hosted gratis tapi semua itu tugasmu. Migration adalah perubahan skema yang di-version control dan diterapkan otomatis saat deploy, sekali, sebelum container baru menyala (node migrate.js, atau php artisan migrate --force di Laravel); pakai expand/contract karena kode lama sempat jalan di atas skema baru. Seeding di CI memberi test data yang konsisten. Backup harian lewat cron (0 2 * * *), pg_dump | gzip, dan retention find -mtime +7 -delete, dengan set -euo pipefail dan salinan di luar server. Restore drill membuktikan backup-nya: file 20 byte cuma ketahuan saat di-restore. Jumlah koneksi = replicas × pool + lainnya, harus di bawah max_connections (bawaan 100).

Yang akan kamu pelajari