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, tanpapipefail. Malam ini jam 02:00, containerdbsedang 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
pg_dumpmenulis isi database sebagai SQL,gzipmemampatkannya.-Tmematikan terminal palsu didocker compose exec, supaya output-nya mengalir utuh ke pipe.date +%Fmenghasilkan2026-10-01, jadi tiap hari punya file sendiri.find ... -mtime +7 -deleteadalah retention: hapus backup yang umurnya lebih dari 7 hari penuh. Backup Day 1 masih ada sepanjang Day 8 dan terhapus di run Day 9.0 2 * * *artinya menit 0, jam 2, setiap hari. Untuk mingguan tiap Kamis:0 2 * * 4.
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 }:
daily: Day 1, Day 2, Day 3.weekly: Day 1, Day 8, Day 15.timeberformat"Day 1 02:00": jam ditulis 2 digit denganpadStart(2, "0").deleteAfteradalah"Day "+ (hari backup + retention). Dengan retention 30, backup Day 1 jadi"Day 31".
⚠️ Jebakan yang sering ketemu
- Backup yang belum pernah di-restore. File-nya ada belum tentu isinya ada. Jadwalkan restore drill, misalnya tiap bulan, dan catat berapa lama restore-nya.
- Pipe tanpa
pipefail.pg_dump | gzipdianggap sukses walaupg_dumpgagal, dan kamu dapat file 20 byte setiap malam. - Migration yang merusak kode lama. Ganti nama atau hapus kolom dalam satu deploy membuat kode lama error selama deploy. Pakai expand/contract.
- Backup di server yang sama. Disk mati, database dan backup-nya hilang bersamaan. Simpan salinan di luar
web-1.
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).