Gejala masalah storage jarang berbentuk galat Docker yang eksotis — lebih sering berupa kalimat pendek yang sudah akrab: no space left on device, Permission denied, not a directory, sampai kepanikan “datanya hilang!”. Padahal gejala-gejala itu berasal dari lima keluarga masalah yang berbeda akarnya: host yang penuh oleh image dan cache, path mount yang tertukar file-direktori, ketidakcocokan UID antara container dan host, siklus hidup volume yang disalahpahami, sampai bind mount berkas tunggal yang diam-diam membekukan versi lama. Hari ini mesin Budi mendapat semuanya sekaligus: build gagal di tengah jalan karena disk penuh, dan sesi investigasi yang menyusul menyingkap empat keluarga lain lewat berkas dan volume yang tidak berperilaku semestinya. Kunci membedakannya adalah mengukur sebelum menebak — dan docker system df adalah stetoskop pertama kita.

Lima Keluarga Masalah dan Cirinya #

Peta berikut menerjemahkan gejala di lapangan ke keluarga masalahnya dan alat diagnosis pertama yang paling relevan:

Gejala di lapanganKeluarga masalahAlat diagnosis pertama
no space left on device atau disk quota exceededHost penuh atau kuota terlampauidocker system df lalu docker system df -v
Is a directory / not a directory saat startBind mount tertukar file dan direktorils -l pada path host
Permission denied di dalam containerKetidakcocokan UID/GIDls -ln, chapter Permission dan Ownership Volume
Data hilang setelah down atau recreateAnonymous volume atau down -vdocker volume ls
Container membaca versi berkas lamaBind mount berkas tunggal berganti inodebandingkan cat host dan container

Host Penuh dan docker system df #

Skenario pembuka hari itu: docker compose up -d --build gagal di tengah layer, dan log aplikasi dipenuhi ENOSPC. Sebelum menghapus apa pun, ukur dulu isi rumah Docker secara keseluruhan:

bash
$ docker system df

Output yang Dihasilkan:

text
TYPE            TOTAL   ACTIVE  SIZE      RECLAIMABLE
Images          18      6       5.214GB   3.102GB (59%)
Containers      10      7       302.1MB   198.3MB (66%)
Local Volumes   7       5       1.842GB   21.2MB (1%)
Build Cache     92      0       6.9GB     6.9GB (100%)

Empat baris itu memetakan empat penghuni disk. Kolom ACTIVE menghitung objek yang masih dipakai minimal satu container, sedangkan RECLAIMABLE adalah porsi yang bisa direbut kembali tanpa mengganggu yang aktif. Angka paling mencolok ada di baris terakhir: 92 rekaman build cache sebesar 6,9GB tanpa satu pun yang aktif — jejak berbulan-bulan eksperimen build. Rincian per objeknya dilihat dengan flag verbose:

bash
$ docker system df -v

Output yang Dihasilkan (dipotong):

text
Images space usage:

REPOSITORY     TAG        IMAGE ID      CREATED       SIZE      SHARED SIZE   UNIQUE SIZE   CONTAINERS
toko-api       dev        3a9b41e7c2d5  3 days ago    174.6MB   96.8MB        77.8MB         1
worker         1.0        9b2e71c4a0d3  2 days ago    128.4MB   121.8MB       6.6MB          1
laporan        1.3        64c8f1a2e9b7  26 hours ago  126.9MB   121.8MB       5.1MB          1
python         3.12-slim  4a7c93d2b1e8  2 weeks ago   121.8MB   121.8MB       0B             0

Local Volumes space usage:

VOLUME NAME                                                       LINKS     SIZE
toko_db-data                                                       1         812.4MB
c8a91f2e7b3d4056829ab4cd7e1f0839a4b5c6d7e8f9012a3b4c5d6e7f8091a2   0         2.3MB
7f2c94d8e6a1b3c5d7f90a1b3c5d7f8a2c4e6b8d0f2a4c6e8b0d2f4a6c8e0b2d   0         18.9MB

Mari kita telaah bagaimana tabel tersebut membaca ruang disk:

  1. Kolom SHARED SIZE dan UNIQUE SIZE menjelaskan mengapa menghapus image tidak mengembalikan dugaan kita — worker:1.0 berbagi 121.8MB dengan python:3.12-slim karena image itu dibangun di atasnya; menghapus worker hanya membebaskan 6.6MB miliknya sendiri selama base image-nya masih dipakai.
  2. Kolom CONTAINERS menghitung penghuni — python:3.12-slim menunjukkan 0 karena image itu hanya berperan sebagai base; container berjalan dari worker:1.0 dan laporan:1.3.
  3. Dua nama heksadesimal 64 karakter dengan LINKS 0 adalah volume anonim yatim — lahir diam-diam dari deklarasi VOLUME di Dockerfile atau flag -v tanpa nama, lalu ditinggalkan begitu container-nya dibuat ulang; keduanya menjumlah tepat 21.2MB yang tercatat di kolom RECLAIMABLE pada tabel ringkas.
  4. toko_db-data dengan LINKS 1 adalah data hidup — volume bernama milik database toko yang sedang berjalan; ukurannya 812.4MB dan tidak boleh disentuh pembersihan apa pun.

Untuk memastikan kontribusi volume terhadap disk secara fisik, ukur langsung dari host — direktori yang sama yang kita jelajahi pada chapter Volume:

bash
$ sudo du -sh /var/lib/docker/volumes

Output yang Dihasilkan:

text
1.9G    /var/lib/docker/volumes

Angka 1.9G sepadan dengan total tabel di atas, jadi volume bukan penyumbang utama — sasaran pembersihan yang benar adalah build cache dan image tua. Kebiasaan merawat image dengan docker image prune sudah kita bangun di chapter Membersihkan Resource Image; untuk build cache, perintahnya sejenis:

bash
$ docker builder prune -f

Output yang Dihasilkan:

text
Total reclaimed space: 6.9GB

Sepupu “no space” menampakkan diri dengan pesan disk quota exceeded: df -h di host menunjukkan ruang masih tersisa, namun daemon menolak membuat volume baru:

text
docker: Error response from daemon: error while creating mount source path '/var/lib/docker/volumes/laporan-data/_data': mkdir /var/lib/docker/volumes/laporan-data/_data: disk quota exceeded.

Diagnosisnya menegaskan kontras tadi: ruang disk ada, kuota per pengguna atau per grup pada filesystem host yang habis — lazim di server kantor bersama atau home direktori ber-NFS. Jalan keluarnya berada di luar Docker: koordinasi dengan pengelola kuota, atau memindahkan rumah data Docker lewat kunci data-root di daemon.json — konfigurasi yang kita bahas pada chapter Konfigurasi Docker Engine — menuju filesystem yang kuotanya lebih longgar.

Tertukar File dan Direktori #

Keluarga kedua menampakkan diri saat container start dengan pesan not a directory, atau lebih licik: container menyala normal lalu aplikasi bingung karena berkas yang diharapkan ternyata direktori. Skenario klasiknya dimulai dari satu path host yang lupa dibuat berkasnya:

bash
$ docker run --rm -v /home/budi/proyek/apa.yaml:/etc/app/app.yaml alpine:3.20 cat /etc/app/app.yaml

Output yang Dihasilkan:

text
cat: can't open '/etc/app/app.yaml': Is a directory

Perintah cat mengeluh karena targetnya ternyata direktori — padahal niat kita memasang berkas. Periksa sisi host:

bash
$ ls -l /home/budi/proyek/apa.yaml

Output yang Dihasilkan:

text
drwxr-xr-x 2 budi budi 4096 Oct 14 11:42 apa.yaml

Huruf d di awal mode membongkar kejadiannya: apa.yaml adalah direktori. Ketika sintaks pendek -v menemukan path host yang tidak ada, daemon diam-diam membuatnya sebagai direktori lalu memasangnya apa adanya — sehingga path target di container ikut menjadi direktori, dan cat yang mengharapkan berkas pun menyerah. Kebalikannya memunculkan pesan yang lebih jujur:

bash
$ docker run --rm -v /home/budi/proyek/data:/etc/hostname alpine:3.20 true

Output yang Dihasilkan:

text
docker: Error response from daemon: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: container init: mount "/home/budi/proyek/data" to "/etc/hostname": not a directory: unknown.

Direktori host dipaksakan ke path /etc/hostname yang di dalam image berupa berkas — runtime menolak dengan pesan not a directory sebelum proses sempat menyala.

Permission Denied: Identitas Berupa Angka #

Keluarga ketiga sudah kita bedah tuntas di section Penyimpanan Data: proses container berjalan sebagai UID tertentu, sementara volume atau bind mount dimiliki UID lain, dan kernel hanya mengakui angka — bukan nama. Gejalanya di dalam container berbunyi:

text
sh: can't create /data/berkas.txt: Permission denied

Diagnosis standarnya memakai ls -ln agar kepemilikan tampil sebagai angka UID/GID mentah, lalu lima pola solusi — dari RUN chown saat build sampai grup bersama dengan umask — sudah dipetakan pada chapter Permission dan Ownership Volume. Satu hal yang perlu diingat di sini: galat ini tidak pernah berarti “Docker rusak”; galat ini selalu berarti dua angka identitas belum diselaraskan.

Data Hilang Setelah down #

Dua varian berbeda menyamar sebagai satu keluhan “datanya hilang”. Varian pertama adalah docker compose down -v yang memang menghapus seluruh volume stack — konsekuensi yang sudah kita peringatkan tegas di chapter Persistensi Database. Varian kedua lebih sunyi: image yang mendeklarasikan VOLUME /data di Dockerfile dijalankan tanpa nama volume, sehingga setiap container baru menerima anonymous volume segar yang kosong — data lama tidak benar-benar musnah, tetapi tergeletak tanpa pengguna:

bash
$ docker volume ls

Output yang Dihasilkan:

text
DRIVER    VOLUME NAME
local     toko_db-data
local     c8a91f2e7b3d4056829ab4cd7e1f0839a4b5c6d7e8f9012a3b4c5d6e7f8091a2
local     7f2c94d8e6a1b3c5d7f90a1b3c5d7f8a2c4e6b8d0f2a4c6e8b0d2f4a6c8e0b2d

Nama heksadesimal tanpa label itulah volume anonim tersebut. Sebelum docker volume prune menyapunya, datanya masih bisa dipulihkan — dipasang ke container sementara lalu disalin keluar, teknik dari chapter Backup dan Restore Volume. Setelah di-prune, tidak ada jalan kembali: itulah titik yang tidak bisa dibatalkan dalam siklus hidup volume anonim.

Bind Mount Berkas yang Berganti Inode #

Keluarga terakhir adalah yang paling membingungkan: aplikasi di container tampak membaca “versi lama” sebuah berkas padahal berkas di host sudah berkali-kali disunting, dan watcher hot reload berhenti terpicu. Akarnya adalah cara kernel memasang bind mount berkas tunggal — mount terikat ke inode berkas, bukan ke namanya. Percobaan singkat pada container baca-config membuktikannya:

bash
$ mkdir -p /home/budi/proyek/config
$ echo "versi: 1" > /home/budi/proyek/config/app.yaml
$ docker run -d --name baca-config --mount type=bind,source=/home/budi/proyek/config/app.yaml,target=/etc/app/app.yaml alpine:3.20 tail -f /dev/null
$ sed -i 's/versi: 1/versi: 2/' /home/budi/proyek/config/app.yaml
$ docker exec baca-config cat /etc/app/app.yaml

Output yang Dihasilkan:

text
9f2c81b4d7e6a3c5b1d9f2a4c6e8b0d3f5a7c9e1b3d5f7a2c4e6b8d0f2a4c6e8
versi: 1

Baris pertama adalah ID container cetakan docker run -d; baris kedua adalah isi berkas yang dibaca container. Konfirmasi dari sisi host menunjukkan berkas yang sudah berubah:

bash
$ cat /home/budi/proyek/config/app.yaml

Output yang Dihasilkan:

text
versi: 2

Host membaca versi: 2, container membaca versi: 1 — dua isi berbeda dari “berkas yang sama”. Penjelasnya ada pada sed -i: utilitas itu bekerja dengan pola atomic save — menulis berkas sementara lalu me-rename-nya menggantikan nama lama. Rename menghasilkan inode baru, sementara bind mount kita tetap menunjuk inode yang lama. Banyak editor melakukan hal yang sama secara bawaan; sebaliknya, echo "versi: 3" > app.yaml yang menulis in place mempertahankan inode dan perubahannya langsung terlihat di container. Bekas percobaan kita bereskan sesuai kebiasaan:

bash
$ docker rm -f baca-config

Output yang Dihasilkan:

text
baca-config

Tabel Diagnosis Akhir #

Kelima keluarga tersebut kini bisa dirangkum menjadi kartu diagnosis saku:

Pesan atau gejalaKeluargaTindakan pertama
no space left on deviceHost penuhdocker system df → docker builder prune / docker image prune
disk quota exceeded padahal df -h longgarKuota filesystem hostAudit kuota; pindahkan data-root di daemon.json
Is a directory atau not a directoryFile dan direktori tertukarls -l path host; ganti -v dengan --mount type=bind
Permission denied di dalam containerUID/GID tidak cocokls -ln dua sisi; lima pola solusi
Data hilang setelah recreateVolume anonim yatim atau down -vdocker volume ls; pulihkan sebelum prune
Container membaca versi berkas lamaInode berganti lewat atomic saveMount direktori induk, bukan berkas tunggal

Kelima keluarga itu punya satu titik temu yang nyaman: semuanya bisa menampakkan diri justru saat proses build — disk penuh di tengah layer, path COPY yang tidak ditemukan di build context, sampai berkas yang dikecualikan .dockerignore secara tak sengaja. Membaca bahasa galat BuildKit adalah keterampilan penutup trilogi debugging kita, dan chapter Mengatasi Build Gagal membedahnya dari baris pertama pesan galat sampai tabel pola kegagalan.

Referensi #