Empat berkas backup kini tergeletak di ~/katalog: dua dump PostgreSQL, satu arsip gzip, satu dump MySQL. Sayangnya, keberadaan berkas belum berkata apa-apa tentang isinya — kita belum pernah membukanya sekali pun, belum membuktikan bahwa baris-baris COPY di dalamnya benar-benar bisa menjadi tabel hidup kembali. Industri ini penuh dengan arsip backup yang gagal persis pada malam pemulihan: password yang salah, dump yang terpotong, format yang tidak kompatibel, atau pipe yang menelan data diam-diam. Chapter ini menagih janji setiap berkas itu satu per satu: membedah arah data yang berlawanan dengan backup, menyaksikan restore pertama menabrak data lama, membangun tiga jalur restore yang masing-masing punya tempatnya, lalu menutupnya dengan disaster drill mini yang benar-benar memusnahkan volume dan membangkitkan database dari satu berkas di folder proyek.
Arah yang Berlawanan: stdout Menjadi stdin #
Backup dan restore adalah dua perintah cermin, dan perbedaannya bukan pada alatnya melainkan pada arah datanya. Backup mengalir dari container ke host: pg_dump menulis ke stdout, redirect > milik shell menangkapnya. Restore membalik arah itu: berkas di host dikirim ke stdin proses di dalam container, dan itulah pekerjaan flag -i:
| Arah | Bentuk perintah | Peran flag |
|---|---|---|
| container → host (backup) | docker exec db pg_dump ... > backup.sql | redirect > menangkap stdout |
| host → container (restore) | docker exec -i db psql ... < backup.sql | -i + redirect < mengalirkan stdin |
Flag -i bukan hiasan. Tanpa -i, klien docker membuka sesi exec tanpa menyalurkan stdin dari terminal host — redirect < berkas tetap mengisi stdin milik klien docker, tapi klien tidak pernah meneruskannya ke dalam container. psql di sisi sana melihat stdin yang tertutup, membaca nol baris, lalu keluar dengan sopan: restore selesai dalam sekejap tanpa satu pun galat dan tanpa satu pun baris data. Kekeliruan tanpa suara semacam ini pernah kita jumpai pada pola docker exec -i di Chapter Migrasi Data Container — dan satu-satunya obatnya sama: pastikan -i selalu hadir setiap kali tanda < berdiri di ujung baris.
Restore Pertama: Menabrak Data Lama #
Insting pertama kita tentu mengembalikan backup ke database asalnya — dan insting itulah yang akan menabrak tembok pertama. Database katalog masih hidup dan berisi tiga baris produk hasil initdb, persis isi si dump:
$ docker exec -i db psql -U katalog -d katalog < backup-katalog.sqlOutput yang Dihasilkan:
SET
SET
SET
...
ERROR: relation "produk" already exists
ERROR: relation "produk_id_seq" already exists
ALTER TABLE
ALTER SEQUENCE
...
ERROR: duplicate key value violates unique constraint "produk_pkey"
DETAIL: Key (id)=(1) already exists.
CONTEXT: COPY produk, line 1
...
setval
------------------
3
(1 row)Baris-baris SET dan bagian akhir dipotong agar ringkas. Mari kita telaah apa yang sebenarnya terjadi:
- Bagian struktur menabrak objek yang sudah ada —
CREATE TABLEdanCREATE SEQUENCEditolak karena objek itu sudah berdiri sejak initdb; pg_dump plain tidak menuliskanDROPapa pun sebelumCREATE, jadi restore tidak pernah bermaksud membongkar yang lama. - Bagian data menabrak primary key —
COPYmencoba menanam barisid=1(Kopi Torabika) ke tabel yang barisnya sendiri sudah memegangid=1; kunci utama menolak penggandaan, danproduk_pkeyadalah nama constraint yang otomatis lahir bersamaPRIMARY KEYpada skema kita. - psql tidak berhenti pada galat — tanpa
-v ON_ERROR_STOP=1, psql mencetak galat lalu melanjutkan ke perintah berikutnya;ALTER TABLE,ALTER SEQUENCE, dansetvaldi baris akhir justru sukses, sehingga proses berakhir dengan exit code 0 — restore yang “berhasil” tanpa memulihkan apa pun. - Data lama tetap utuh — tidak satu baris pun berhasil ditulis, jadi tabel tidak menjadi berlipat; verifikasi cepat membuktikannya.
$ docker exec db psql -U katalog -d katalog -c "SELECT count(*) FROM produk;"Output yang Dihasilkan:
count
-------
3
(1 row)Perlawanan ini muncul dari niat baik: restore menolak menginjak data yang sudah ada. Konsekuensinya harus dipilih jalur yang disengaja:
TRUNCATE lalu Restore Ulang #
Jalur pertama untuk skenario “ganti isi, pertahankan skema”: kosongkan tabel, lalu ulangi restore. Galat-galat struktur akan tetap lewat — objeknya memang sudah ada dan bentuknya sama — tapi COPY kini mendarat di tanah kosong:
$ docker exec db psql -U katalog -d katalog -c "TRUNCATE produk;"
$ docker exec -i db psql -U katalog -d katalog < backup-katalog.sqlOutput yang Dihasilkan:
TRUNCATE TABLE
SET
SET
SET
...
ERROR: relation "produk" already exists
ERROR: relation "produk_id_seq" already exists
ALTER TABLE
ALTER SEQUENCE
COPY 3
...
setval
------------------
3
(1 row)Perhatikan dua hal: COPY 3 kini sukses, dan setval membenahi nomor urut sequence ke angka 3 sehingga INSERT berikutnya tidak menabrak id yang sudah dipakai. Verifikasinya:
$ docker exec db psql -U katalog -d katalog -c "SELECT count(*) FROM produk;"
$ docker exec db psql -U katalog -d katalog -c "SELECT nama, harga FROM produk ORDER BY id LIMIT 2;"Output yang Dihasilkan:
count
-------
3
(1 row)
nama | harga
--------------------+-------
Kopi Torabika | 15000
Kopi Robusta Lampung | 65000
(2 rows)Jalur ini bekerja, tapi verifikasi kita harus lebih cermat: galat already exists yang “boleh diabaikan” hanya aman bila skema yang ada memang identik dengan skema di dump. Bila dump dibuat dari versi skema yang berbeda, galat serupa bisa menyembunyikan tabel yang tidak kunjung dibuat. Karena itu praktik yang lebih sehat memakai kanvas yang benar-benar bersih.
Database Baru sebagai Kanvas Bersih #
Jalur kedua memisahkan tempat pulih dari tempat hidup: pulihkan ke database baru, di sisi server yang sama. PostgreSQL membawa pembantu kecilnya, createdb:
$ docker exec db createdb -U katalog katalog-pulih
$ docker exec -i db psql -U katalog -d katalog-pulih -v ON_ERROR_STOP=1 < backup-katalog.sqlOutput yang Dihasilkan:
SET
SET
SET
...
CREATE TABLE
ALTER TABLE
CREATE SEQUENCE
ALTER SEQUENCE
COPY 3
...
setval
------------------
3
(1 row)Kali ini tidak ada satu pun galat — dan kali ini pula kita berani memasang -v ON_ERROR_STOP=1, karena di kanvas kosong setiap galat adalah kabar buruk yang layak menghentikan proses. Verifikasi singkat lalu membongkar kanvas setelah selesai dipakai:
$ docker exec db psql -U katalog -d katalog-pulih -c "SELECT count(*) FROM produk;"
$ docker exec db psql -U katalog -d katalog -c "DROP DATABASE katalog-pulih;"Output yang Dihasilkan:
count
-------
3
(1 row)
DROP DATABASEPola katalog-pulih ini punya tempat yang tak tergantikan di dunia nyata: memeriksa isi backup tanpa menyentuh database produksi, membandingkan data lama dan baru berdampingan, atau migrasi antar mesin — server yang menerima pulihan tidak perlu menjadi server asal dump. Titik lemahnya justru kekuatannya: satu server dengan disk yang sama tetap satu titik kegagalan, sehingga pola ini melengkapi — bukan menggantikan — simpanan arsip di luar host.
Restore Format Kustom dengan pg_restore #
Arsip backup-katalog.dump tidak bisa dibaca psql — format kustom adalah bahasa pg_restore. Flag --clean --if-exists membuat pg_restore membuang objek lama sebelum membuat ulang, persis obat untuk database yang sudah berisi:
$ docker exec -i db pg_restore -U katalog -d katalog --clean --if-exists < backup-katalog.dump
$ echo $?Output yang Dihasilkan:
0pg_restore memang tidak berisik ketika berhasil — exit code 0 dari echo $? adalah seluruh laporannya, kebiasaan membaca exit code yang kita latih sejak Chapter Memahami Exit Code. Verifikasi datanya:
$ docker exec db psql -U katalog -d katalog -c "SELECT count(*), max(harga) FROM produk;"Output yang Dihasilkan:
count | max
-------+-------
3 | 45000
(1 row)Tiga baris kembali dengan harga tertinggi 45000 milik Gula Aren Cair. Mari kita telaah bentuk restore kustom ini:
--cleanmembuang sebelum membuat — pg_restore menjatuhkanDROPuntuk tiap objek yang akan dibuat ulang, sehingga restore ke database berisi menjadi bersih tanpa perlu TRUNCATE manual.--if-existsmeredam galat pembuangan — tanpa flag ini,DROP TABLEpada objek yang tidak ada memunculkan galat; dengan flag ini, pembuangan dilewati dengan sopan — penting saat restore pertama ke database kosong.- Restore selektif untuk insiden kecil —
pg_restore --table=produkmengembalikan satu tabel saja tanpa menyentuh sisanya, kemampuan yang hanya dimiliki format kustom; insiden “satu tabel terhapus oleh migration yang salah” pulih dalam hitungan menit. - Arsip besar lebih nyaman lewat
docker cp— menyalin dump ke dalam container (docker cp backup-katalog.dump db:/tmp/) lalu memulihkan dari path lokal container memotong jalur stdin; poladocker cpitu kita kenal dari Chapter Writable Container Layer.
Restore dari Arsip gzip #
Arsip terkompresi dipulihkan dengan membuka gulungannya lebih dulu — gunzip -c menulis isi arsip ke stdout, pipe mengalirkan ke psql di dalam container, dan -i tetap menjaga pintu stdin terbuka. Kanvas bersih kita pakai sekali lagi:
$ docker exec db createdb -U katalog katalog-pulih
$ gunzip -c backup-katalog.sql.gz | docker exec -i db psql -U katalog -d katalog-pulih -v ON_ERROR_STOP=1
$ docker exec db psql -U katalog -d katalog-pulih -c "SELECT count(*) FROM produk;"
$ docker exec db psql -U katalog -d katalog -c "DROP DATABASE katalog-pulih;"Output yang Dihasilkan:
SET
SET
SET
...
COPY 3
...
count
-------
3
(1 row)
DROP DATABASEEmpat perintah itu dijalankan bergantian dan keluarannya disatukan — bagian SET dipotong agar ringkas. Perhatikan arsitektur pipa yang terbentuk: alat dekompresi hidup di host, psql hidup di container, dan data mengalir tanpa satu berkas sementara pun ditulis ke disk — kelak, ketika arsip malamannya berukuran puluhan gigabyte, pola yang sama tetap berlaku tanpa memenuhi disk host dua kali.
Restore MySQL #
Mesin MySQL menuntaskan cerita dengan kejutan yang menyenangkan: dump mysqldump memuat DROP TABLE IF EXISTS di depan tiap CREATE TABLE, jadi restore ke database yang sudah berisi tidak menabrak apa pun — tabel lama dibongkar rapi lalu dibangun ulang. Mari kita buktikan dengan menanam baris penyusup lebih dulu:
$ docker exec db-mysql mysql -ukatalog -prahasia123 katalog \
-e "INSERT INTO produk (nama, harga) VALUES ('Teh Hijau Sencha', 55000);"
$ docker exec -i db-mysql mysql -ukatalog -prahasia123 katalog < backup-katalog-mysql.sql
$ docker exec db-mysql mysql -ukatalog -prahasia123 katalog -e "SELECT COUNT(*) FROM produk;"Output yang Dihasilkan:
mysql: [Warning] Using a password on the command line interface can be insecure.
mysql: [Warning] Using a password on the command line interface can be insecure.
mysql: [Warning] Using a password on the command line interface can be insecure.
+----------+
| COUNT(*) |
+----------+
| 2 |
+----------+Tiga baris peringatan itu muncul dari tiga perintah klien yang berbeda — sandi di command line masih kebiasaan latihan kita dari chapter sebelumnya. Mari kita telaah hasilnya:
- Tiga baris masuk, dua baris keluar — baris
Teh Hijau Senchalenyap bersama tabel lamanya, karenaDROP TABLE IF EXISTSmembongkarproduksebelumCREATE TABLEmembangun ulang danINSERTmenanam ulang dua baris dari dump. - Perilaku bawaan yang berbeda dari pg_dump — mysqldump menulis
DROPsecara default, pg_dump plain tidak; keduanya sah, hanya saja tiap tool menuntut kebiasaan verifikasi yang sesuai — di PostgreSQL, “menimpa rapi” harus diminta eksplisit lewatpg_restore --clean. -itetap wajib — redirect<di ujung baris kedua bekerja karena flag itu; tanpanya klien mysql membaca stdin yang tertutup dan restore berlalu tanpa peristiwa.
Ritual Verifikasi Menyeluruh #
Restore yang berhenti pada “tidak ada galat” belum selesai — galat bisa sunyi dan data bisa setengah. Ritual yang sehat memverifikasi dari tiga sisi, berurutan:
- Jumlah baris tiap tabel kunci — bandingkan dengan angka yang dicatat sebelum backup dibuat; untuk katalog malam ini angkanya 3 pada
produk, dan selisih satu baris pun layak diinterogasi. - Isi tabel yang paling dikenal — satu
SELECTpada tabel kunci dengan kolom yang dikenali manusia (nama produk, harga) memastikan bukan hanya jumlah yang benar tapi juga datanya —COUNT(*)tidak akan membedakan tiga baris kosong dari tiga baris Kopi Torabika. - Smoke test dari sisi aplikasi — jalankan kueri persis yang dipakai aplikasi, atau kalau stack Compose hidup, satu permintaan
curlke endpoint yang membaca database; keberhasilan restore diukur di mata aplikasi, bukan di mata DBA.
Disaster Drill Mini #
Inilah puncaknya: kita benar-benar memusnahkan volume dan membangkitkan database dari arsip. Ritualnya bernomor supaya bisa diulang seperti daftar periksa.
Pertama, musnahkan container dan volumenya — kalimat yang selama ini hanya kita ucapkan dengan hati-hati:
$ docker rm -f db && docker volume rm db-dataOutput yang Dihasilkan:
db
db-dataKedua, lahirkan container baru dengan volume baru yang kosong — dan perhatikan satu keputusan penting: kali ini kita tidak memasang folder init/:
$ docker run -d --name db \
-e POSTGRES_USER=katalog \
-e POSTGRES_PASSWORD=rahasia123 \
-e POSTGRES_DB=katalog \
-v db-data:/var/lib/postgresql/data \
postgres:18-alpineOutput yang Dihasilkan:
1c1a0e9775d33dca85b753e8090db172e95ff922213110e39eba4afbe2c5f9f7Tanpa folder init/ yang terpasang, entry point hanya menjalankan initdb polos tanpa skrip apa pun — cluster lahir dengan database katalog yang masih kosong, menunggu satu-satunya penulis yang kita izinkan malam ini: restore. Ketiga, tunggu server siap, lalu pulihkan:
$ docker exec db pg_isready -U katalog -d katalog
$ docker exec -i db psql -U katalog -d katalog -v ON_ERROR_STOP=1 < backup-katalog.sqlOutput yang Dihasilkan:
/var/run/postgresql:5432 - accepting connections
SET
SET
SET
...
CREATE TABLE
ALTER TABLE
CREATE SEQUENCE
ALTER SEQUENCE
COPY 3
...
setval
------------------
3
(1 row)Keempat — dan inilah bukti penuh yang kita cari:
$ docker exec db psql -U katalog -d katalog -c "SELECT * FROM produk;"Output yang Dihasilkan:
id | nama | harga
----+---------------------+------
1 | Kopi Torabika | 15000
2 | Kopi Robusta Lampung | 65000
3 | Gula Aren Cair | 45000
(3 rows)Tiga baris produk, primary key utuh, urutan sequence dipulihkan oleh setval — database yang barusan kita musnahkan kini hidup kembali dari satu berkas 18K di folder proyek. Mari kita telaah sisa detailnya:
init/sengaja tidak dipasang — bila folder itu ikut terpasang, initdb akan menulis skema dan data awal ke volume kosong, dan restore berikutnya akan menabrak duplikat primary key persis seperti percobaan pertama kita; pada drill, restore harus menjadi satu-satunya penulis.- Role
pelaporantidak ikut pulih — dump SQL milik satu database tidak memuat role, dan cluster yang barusan lahir hanya mengenalkatalog; skrip003_user.shkita tetap bisa dipanggil ulang lewat pola stdin yang sama, karena isinya hanyalah perintah psql yang dibungkus shell. pg_isreadymenjaga urutan — memberi server baru beberapa detik sebelum klien pertama mengetuk; pada drill berikutnya di dunia nyata, pola yang setara adalah gerbangservice_healthydari Chapter Healthcheck dan Service Readiness.
Sebelum menutup chapter, mari bereskan arena: mesin MySQL sudah menuntaskan perannya sejak restore terakhir, dan sisanya adalah urusan PostgreSQL pada chapter penutup section ini.
$ docker rm -f db-mysql && docker volume rm db-data-mysqlOutput yang Dihasilkan:
db-mysql
db-data-mysqlSeluruh skenario yang kita lalui malam ini terangkum pada satu tabel kecil:
| Kondisi database tujuan | Jalur restore yang tepat | Catatan |
|---|---|---|
| Kosong — volume baru, tanpa initdb | psql -d katalog < backup.sql dengan ON_ERROR_STOP=1 | jalur drill: hasil bersih tanpa galat |
| Berisi data lama, skema identik | TRUNCATE dulu, atau pg_restore --clean --if-exists | verifikasi galat struktur yang lolos |
| Butuh ruang aman untuk audit | createdb katalog-pulih lalu restore ke sana | data produksi tidak tersentuh |
Data katalog kini punya tiga lapis perlindungan: initdb yang melahirkannya ulang di mesin baru, volume yang menjaganya melewati kematian container, dan arsip yang membangkitkannya dari nol. Satu wilayah terakhir belum tersentuh: bagaimana skema itu sendiri berkembang dari bulan ke bulan tanpa merusak data yang hidup, dan bagaimana suatu hari seluruh cluster berpindah dari PostgreSQL 16 ke 18 tanpa meninggalkan sebaris data pun. Dua persoalan itu ternyata berbagi satu nama — migrasi — dan Chapter Migrasi Versi Database membedah keduanya sampai tuntas.
Referensi #
- PostgreSQL Documentation
— rujukan pg_restore, opsi
--clean,--if-exists, dan pemulihan selektif per tabel. - PostgreSQL — Docker Hub — perilaku klien psql di dalam image dan pola exec untuk dump/restore.
- MySQL — Docker Hub
— isi dump mysqldump dan perilaku
DROP TABLE IF EXISTSsaat restore. - docker container exec — CLI Reference
— flag
-iuntuk arah stdin pada restore.