Chapter Port dan Network ditutup dengan satu pengamatan yang menusuk: begitu docker compose down dijalankan dan stack dibangkitkan kembali, seluruh isi database katalog lenyap — service db menyala sebagai cluster PostgreSQL baru yang bersih, seolah tabel yang tadinya terisi tidak pernah ada. Kehilangan semacam itu mengganggu bahkan untuk latihan, apalagi bila yang hilang adalah data pelanggan yang terkumpul berbulan-bulan. Penyebabnya bukan ulah Compose yang jahat: perintah down memang hanya membuang container dan network, sementara selama ini tulisan PostgreSQL mendarat di penyimpanan yang mati bersama container. Chapter ini memindahkan urusan umur panjang data ke dalam compose.yaml — lewat kunci volumes untuk volume bernama, bind mount untuk source code, dan tmpfs untuk data sekali jalan.

Mengapa Data Musnah Saat down #

Setiap container menyimpan tulisannya pada writable layer — lapisan tipis di atas image yang bersifat read-only dan hidupnya persis selama container itu sendiri, mekanisme yang kita bedah tuntas pada Chapter Writable Container Layer. Perintah docker compose down membuang container web dan db beserta network katalog_default, dan tidak satu pun objek itu dijemput kembali: docker compose up berikutnya menciptakan semuanya dari nol. Container db yang baru berdiri di atas layer kosong, sehingga entry point PostgreSQL tidak menemukan cluster apa pun di /var/lib/postgresql/data dan memutuskan menjalankan initdb dari awal. Variabel POSTGRES_USER dan POSTGRES_DB tetap membangun user serta database katalog seperti biasa, tetapi isi tabel yang lama tidak pernah diundang datang kembali.

Obatnya adalah melepaskan umur data dari umur container — pola yang sudah kita praktikkan manual lewat docker volume create dan flag -v pada Chapter Volume. Compose memindahkan pola itu ke dunia deklaratif: cukup dua suntingan kecil pada compose.yaml, satu di dalam service db dan satu di level teratas berkas. Hasilnya berbunyi: “setiap container db pasti memasang volume bernama db-data di direktori datanya”, dan pernyataan itu berlaku untuk semua up selama-lamanya.

Volume Bernama untuk Service db #

Berikut compose.yaml katalog setelah volume bernama dipasang. Perhatikan bahwa tidak ada service baru yang ditambahkan — volume bukan service, melainkan objek penyimpanan yang dipasang ke service yang sudah ada.

yaml
services:
  web:
    build: .
    ports:
      - "8080:3000"

  db:
    image: postgres:18-alpine
    environment:
      POSTGRES_USER: katalog
      POSTGRES_PASSWORD: rahasia123
      POSTGRES_DB: katalog
    volumes:
      - db-data:/var/lib/postgresql/data

volumes:
  db-data:

Mari kita telaah tiga kunci yang bekerja sama pada berkas di atas:

  1. volumes di dalam service db — daftar mount yang dipasang ke setiap container hasil service ini; satu baris db-data:/var/lib/postgresql/data berarti: pasang volume bernama db-data ke path /var/lib/postgresql/data di dalam container, format SUMBER:TARGET yang sama dengan flag -v pada docker run.
  2. Path target /var/lib/postgresql/data — direktori data resmi image postgres; entry point image menjalankan initdb serta membaca cluster dari path inilah, jadi memasang volume di titik yang tepat membuat seluruh isi database — tabel, indeks, sampai konfigurasi cluster — tinggal di dalam volume, bukan di penyimpanan container.
  3. volumes: level teratas — mendeklarasikan db-data sebagai volume milik project katalog; Compose membuat objek tersebut sekali saat up pertama dan memakai ulang objek yang sama pada up berikutnya. Blok teratas juga kelak menjadi tempat menyetel opsi lanjutan seperti driver atau external tanpa menyentuh service.

Budi mematikan dulu stack warisan chapter sebelumnya dengan docker compose down, menyimpan suntingan di atas, lalu menyalakan kembali stack-nya:

bash
$ docker compose up -d

Output yang Dihasilkan:

text
[+] Running 3/3
 ✔ Network katalog_default   Created
 ✔ Container katalog-db-1    Started
 ✔ Container katalog-web-1   Started

Nama Volume dengan Prefix Project #

Volume yang barusan diciptakan Compose tidak bernama db-data persis. Daftar volume di mesin membuktikannya:

bash
$ docker volume ls

Output yang Dihasilkan:

text
DRIVER    VOLUME NAME
local     3f5c8a1e7b2d4f6a9c0e2b5d8f1a3c6e9b2d5f8a1c4e7b0d3f6a2c9e5b1d
local     7b2e9f4c1a8d3e6f0c5a2d7b9e1f4a6c8b3d0e7f2a5c9b1d4f8a6e3c0b2d
local     data-web
local     katalog_db-data
local     latihan-volume
local     pgdata
...

Mari kita telaah baris-baris di atas:

  1. katalog_db-data, bukan db-data — Compose memberi prefix nama project pada semua objek yang diciptakannya, dengan pola <project>_<nama>; nama project diambil dari nama direktori (katalog), persis pola yang sudah kita saksikan pada network katalog_default dan container katalog-db-1 di Chapter Port dan Network.
  2. Prefix mencegah tabrakan antar project — dua project berbeda di mesin yang sama boleh sama-sama punya volume bernama db-data; keduanya akan hidup berdampingan sebagai katalog_db-data dan kasir_db-data tanpa saling menginjak.
  3. data-web, pgdata, dan latihan-volume — kenalan lama dari Section Penyimpanan Data yang memang sengaja dibiarkan hidup; volume hanya mati bila dihapus eksplisit, dan docker volume ls jujur melaporkan semua penghuninya.
  4. Dua baris heksadesimal 64 karakter — volume anonim yang keberadaannya sengaja kita lewatkan sejenak; kisah di baliknya dibuka tuntas pada bagian akhir chapter ini.

Demo: Data yang Selamat dari down #

Inilah demonstrasi inti chapter ini sekaligus balasan langsung atas kehilangan data di penutup chapter sebelumnya. Kita isi database dengan data nyata, bongkar stack sampai ke akarnya, bangkitkan kembali, lalu bertanya kepada PostgreSQL: apakah tabel produk masih mengingat isinya?

bash
$ docker compose exec db psql -U katalog -d katalog -c "CREATE TABLE produk (nama TEXT, harga INTEGER);"
$ docker compose exec db psql -U katalog -d katalog -c "INSERT INTO produk VALUES ('Kopi Arabika Gayo', 85000), ('Teh Melati', 45000);"

Output yang Dihasilkan:

text
CREATE TABLE
INSERT 0 2

Dua baris produk kini tercatat di dalam volume katalog_db-data. Saatnya menjalankan perintah yang pada chapter sebelumnya memusnahkan segalanya:

bash
$ docker compose down

Output yang Dihasilkan:

text
[+] Running 3/3
 ✔ Container katalog-web-1  Removed
 ✔ Container katalog-db-1   Removed
 ✔ Network katalog_default  Removed
bash
$ docker compose up -d

Output yang Dihasilkan:

text
[+] Running 3/3
 ✔ Network katalog_default   Created
 ✔ Container katalog-db-1    Started
 ✔ Container katalog-web-1   Started
bash
$ docker compose exec db psql -U katalog -d katalog -c "SELECT * FROM produk;"

Output yang Dihasilkan:

text
       nama        | harga
-------------------+-------
 Kopi Arabika Gayo | 85000
 Teh Melati        | 45000
(2 rows)

Dua baris yang sama masih berdiri setelah siklus down dan up lengkap. Mari kita telaah mengapa kali ini datanya selamat:

  1. Output down tidak memuat baris Volume ... Removed — perintah down membuang container dan network saja; volume bernama sengaja tidak tersentuh karena volume adalah objek terpisah yang umurnya melebihi stack.
  2. Container db baru memasang volume yang sama — deklarasi db-data:/var/lib/postgresql/data membuat Compose mencari volume katalog_db-data yang sudah ada, bukan menciptakan yang baru; perintah up kedua dengan demikian tidak pernah mulai dari layer kosong.
  3. initdb dilewati — entry point PostgreSQL mendapati direktori data berisi cluster utuh, jadi inisialisasi tidak diulang dan service db siap lebih cepat; perilaku yang sama sudah kita saksikan pada Chapter Volume.
  4. SELECT mengembalikan data lama — tabel produk beserta isinya tidak pernah pindah rumah; sejak perintah INSERT pertama, alamatnya adalah mountpoint volume di disk host, bukan writable layer.
  5. Siklus boleh diulang sebanyak apa pun — down dan up bisa bergantian puluhan kali; selama nama volume dalam compose.yaml tidak berubah, data tetap di tempatnya.

Sintaks Pendek dan Sintaks Panjang #

Kunci volumes di dalam service menerima dua tata tulis, mengikuti pembagian yang sudah akrab antara flag -v dan --mount di dunia CLI. Sintaks pendek menuliskan mount sebagai satu string SUMBER:TARGET, sedangkan sintaks panjang memecahnya menjadi pasangan kunci yang menyebut jenis mount secara gamblang:

yaml
    volumes:
      - type: volume
        source: db-data
        target: /var/lib/postgresql/data
AspekSintaks pendekSintaks panjang
Bentukdb-data:/var/lib/postgresql/dataKunci type, source, target baris demi baris
Jenis mountDitebak dari bentuk sumber — tanpa / berarti volumeDinyatakan gamblang lewat type: volume
Mode aksesDitambah di ujung: db-data:/path:roKunci read_only: true
Opsi tmpfsTidak tersediaSub-blok tmpfs dengan size
Paling tepat untukMount sederhana yang terbaca sekilasDeklarasi yang butuh opsi tambahan

Kedua tata tulis menunjuk mount yang sama dan saling menggantikan; pilihannya urusan kejelasan, bukan fungsi. Bentuk pendek cukup untuk kebanyakan service data, sementara bentuk panjang membayar dirinya begitu opsi seperti read_only atau batasan ukuran tmpfs ikut bermain.

Bind Mount untuk Source Code #

Volume menempatkan data di wilayah yang dikelola Docker, tetapi saat mengembangkan aplikasi kita justru ingin sebaliknya: source code tinggal di host, dikerjakan dengan editor biasa, dan container cukup membacanya. Itu pekerjaan bind mount, dan di Compose mount jenis ini ditandai sumber yang berawalan / atau ./:

yaml
  web:
    build: .
    ports:
      - "8080:3000"
    volumes:
      - type: bind
        source: ./src
        target: /app

Bentuk pendeknya lebih ringkas: ./src:/app. Path ./src dihitung relatif terhadap lokasi compose.yaml, dan isi path host menutup path /app di dalam container apa adanya — tanpa pra-isi, dua arah, persis mekanisme Chapter Bind Mount.

Pola lain yang terbawa dari chapter CLI adalah bind berkas konfigurasi tunggal. Menambahkan read_only: true menjaga container agar tidak pernah menulis balik ke berkas milik host:

yaml
      - type: bind
        source: ./nginx.conf
        target: /etc/nginx/nginx.conf
        read_only: true

Mount read-only seperti ini akan kita bahas lebih dalam lagi pada Chapter Mount Read-Only di Section Penyimpanan Data; untuk saat ini cukup dicatat bahwa kuncinya sama, hanya pindah rumah ke compose.yaml.

tmpfs di dalam compose #

Kelas data terakhir justru ingin tidak pernah menyentuh disk sama sekali: cache yang boleh hilang kapan pun atau berkas sementara yang tidak boleh meninggalkan jejak. Compose menyediakan kunci tmpfs untuk kebutuhan itu:

yaml
  web:
    build: .
    ports:
      - "8080:3000"
    tmpfs:
      - /app/cache

Bentuk panjangnya kembali lewat kunci volumes dan membuka opsi batasan ukuran:

yaml
    volumes:
      - type: tmpfs
        target: /app/cache
        tmpfs:
          size: 16000000

Mari kita telaah dua blok di atas:

  1. tmpfs: /app/cache — memasang filesystem memori pada path tersebut di dalam container; isi hilang begitu container berhenti, persis perilaku Chapter tmpfs Mount.
  2. size: 16000000 — membatasi tmpfs pada 16 juta byte (sekitar 16 MB); angka ditulis dalam satuan byte, dan tanpa batasan eksplisit ukurannya diserahkan kepada kernel host.

Nasib Volume Saat down #

Kita sudah membuktikan bahwa docker compose down membiarkan volume bernama tetap berdiri. Perilaku ini disengaja: pembersihan stack harus aman untuk data, dan Compose memilih untuk tidak pernah menghapus sesuatu yang tidak bisa dikembalikan. Konsekuensinya, satu-satunya cara membuat down ikut membuang volume adalah menuliskan flag -v (bentuk panjangnya --volumes) secara eksplisit:

bash
$ docker compose down -v

Output yang Dihasilkan:

text
[+] Running 4/4
 ✔ Container katalog-web-1  Removed
 ✔ Container katalog-db-1   Removed
 ✔ Network katalog_default  Removed
 ✔ Volume katalog_db-data   Removed

Baris Volume katalog_db-data Removed itulah bedanya — dan baris tersebut baru muncul karena kita memintanya.

Volume Anonim dari Image #

Sekarang saatnya menepati janji tentang dua baris heksadesimal pada docker volume ls. Image postgres ternyata membawa instruksi VOLUME /var/lib/postgresql/data di Dockerfile-nya — instruksi yang kita kenal pada Chapter VOLUME dan LABEL sebagai penanda titik data. Efeknya: setiap kali container dari image itu dijalankan tanpa mount eksplisit pada path tersebut, daemon diam-diam melahirkan volume anonim dan memasangkannya.

Dua siklus up pada chapter-chapter sebelumnya dengan demikian meninggalkan dua volume anonim terlantar: container db pertama mengisi volume anonimnya dengan cluster hasil initdb, container db kedua mendapat volume anonim baru yang segar. Data yang kita kira musnah sebenarnya masih ada — tergeletak di volume anonim pertama yang tidak lagi dirujuk container mana pun. Inilah mengapa kehilangan data pada penutup Chapter Port dan Network terasa seperti ulah writable layer, padahal biang keroknya adalah volume anonim yang berganti-ganti tanpa diminta.

Membereskan dua baris sisa itu cukup dengan docker volume prune, karena sasaran bawaannya memang volume anonim yang tidak dipakai container mana pun:

bash
$ docker volume prune

Output yang Dihasilkan:

text
WARNING! This will remove all anonymous volumes not used by at least one container.
Are you sure you want to continue? [y/N] y
Deleted Volumes:
3f5c8a1e7b2d4f6a9c0e2b5d8f1a3c6e9b2d5f8a1c4e7b0d3f6a2c9e5b1d
7b2e9f4c1a8d3e6f0c5a2d7b9e1f4a6c8b3d0e7f2a5c9b1d4f8a6e3c0b2d

Total reclaimed space: 75.6MB

Mari kita telaah hasil pembersihan ini:

  1. Dua volume anonim terbuang — keduanya bekas container db dari siklus up sebelum kita memasang db-data; ruang disk 75.6 MB kembali ke host.
  2. katalog_db-data selamat — volume bernama berada di luar jangkauan prune bawaan; prinsipnya tetap sama dengan Chapter Volume: yang anonim dianggap sampah, yang bernama dianggap milik.
  3. Perintah konfirmasi [y/N] — pembersihan tidak bisa dibatalkan, jadi bacalah docker volume ls sekali lagi sebelum menekan y.

Volume, Bind Mount, dan tmpfs dalam compose #

Ketiga jenis mount kini lengkap di tangan kita dalam bentuk deklaratif. Tabel berikut merangkum posisi masing-masing di dalam compose.yaml:

AspekVolume bernamaBind mounttmpfs
Bentuk khasdb-data:/var/lib/postgresql/data./src:/app atau type: bindtmpfs: /app/cache atau type: tmpfs
Lokasi fisik/var/lib/docker/volumes/...Path host yang kita tunjukMemori host
Selamat dari downYa — volume tetap berdiriYa — berkas tetap milik hostTidak — hilang saat container berhenti
Pra-isi dari imageYa, bila volume masih kosongTidak — isi host menutup pathTidak
Kegunaan khasDatabase dan data aplikasiSource code development, konfigurasiCache, data sekali jalan

Data katalog kini berumur panjang: down boleh datang berkali-kali, tabel produk tetap setia di katalog_db-data. Namun stack kita masih menyimpan satu tugas yang belum terselesaikan — service web sampai detik ini belum tahu cara mencapai db: di alamat berapa, dengan kredensial apa, dan ke database yang bernama apa. Jawaban untuk kelas kebutuhan itu bukan mount, melainkan environment variable yang disuntikkan Compose ke setiap service — materi Chapter Environment dan env File.

Referensi #