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 lapangan | Keluarga masalah | Alat diagnosis pertama |
|---|---|---|
no space left on device atau disk quota exceeded | Host penuh atau kuota terlampaui | docker system df lalu docker system df -v |
Is a directory / not a directory saat start | Bind mount tertukar file dan direktori | ls -l pada path host |
Permission denied di dalam container | Ketidakcocokan UID/GID | ls -ln, chapter Permission dan Ownership Volume |
Data hilang setelah down atau recreate | Anonymous volume atau down -v | docker volume ls |
| Container membaca versi berkas lama | Bind mount berkas tunggal berganti inode | bandingkan 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:
$ docker system dfOutput yang Dihasilkan:
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:
$ docker system df -vOutput yang Dihasilkan (dipotong):
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.9MBMari kita telaah bagaimana tabel tersebut membaca ruang disk:
- Kolom
SHARED SIZEdanUNIQUE SIZEmenjelaskan mengapa menghapus image tidak mengembalikan dugaan kita —worker:1.0berbagi 121.8MB denganpython:3.12-slimkarena image itu dibangun di atasnya; menghapusworkerhanya membebaskan 6.6MB miliknya sendiri selama base image-nya masih dipakai. - Kolom
CONTAINERSmenghitung penghuni —python:3.12-slimmenunjukkan 0 karena image itu hanya berperan sebagai base; container berjalan dariworker:1.0danlaporan:1.3. - Dua nama heksadesimal 64 karakter dengan
LINKS 0adalah volume anonim yatim — lahir diam-diam dari deklarasiVOLUMEdi Dockerfile atau flag-vtanpa nama, lalu ditinggalkan begitu container-nya dibuat ulang; keduanya menjumlah tepat 21.2MB yang tercatat di kolomRECLAIMABLEpada tabel ringkas. toko_db-datadenganLINKS 1adalah 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:
$ sudo du -sh /var/lib/docker/volumesOutput yang Dihasilkan:
1.9G /var/lib/docker/volumesAngka 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:
$ docker builder prune -fOutput yang Dihasilkan:
Total reclaimed space: 6.9GBSepupu “no space” menampakkan diri dengan pesan disk quota exceeded: df -h di host menunjukkan ruang masih tersisa, namun daemon menolak membuat volume baru:
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:
$ docker run --rm -v /home/budi/proyek/apa.yaml:/etc/app/app.yaml alpine:3.20 cat /etc/app/app.yamlOutput yang Dihasilkan:
cat: can't open '/etc/app/app.yaml': Is a directoryPerintah cat mengeluh karena targetnya ternyata direktori — padahal niat kita memasang berkas. Periksa sisi host:
$ ls -l /home/budi/proyek/apa.yamlOutput yang Dihasilkan:
drwxr-xr-x 2 budi budi 4096 Oct 14 11:42 apa.yamlHuruf 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:
$ docker run --rm -v /home/budi/proyek/data:/etc/hostname alpine:3.20 trueOutput yang Dihasilkan:
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:
sh: can't create /data/berkas.txt: Permission deniedDiagnosis 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:
$ docker volume lsOutput yang Dihasilkan:
DRIVER VOLUME NAME
local toko_db-data
local c8a91f2e7b3d4056829ab4cd7e1f0839a4b5c6d7e8f9012a3b4c5d6e7f8091a2
local 7f2c94d8e6a1b3c5d7f90a1b3c5d7f8a2c4e6b8d0f2a4c6e8b0d2f4a6c8e0b2dNama 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:
$ 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.yamlOutput yang Dihasilkan:
9f2c81b4d7e6a3c5b1d9f2a4c6e8b0d3f5a7c9e1b3d5f7a2c4e6b8d0f2a4c6e8
versi: 1Baris 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:
$ cat /home/budi/proyek/config/app.yamlOutput yang Dihasilkan:
versi: 2Host 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:
$ docker rm -f baca-configOutput yang Dihasilkan:
baca-configTabel Diagnosis Akhir #
Kelima keluarga tersebut kini bisa dirangkum menjadi kartu diagnosis saku:
| Pesan atau gejala | Keluarga | Tindakan pertama |
|---|---|---|
no space left on device | Host penuh | docker system df → docker builder prune / docker image prune |
disk quota exceeded padahal df -h longgar | Kuota filesystem host | Audit kuota; pindahkan data-root di daemon.json |
Is a directory atau not a directory | File dan direktori tertukar | ls -l path host; ganti -v dengan --mount type=bind |
Permission denied di dalam container | UID/GID tidak cocok | ls -ln dua sisi; lima pola solusi |
| Data hilang setelah recreate | Volume anonim yatim atau down -v | docker volume ls; pulihkan sebelum prune |
| Container membaca versi berkas lama | Inode berganti lewat atomic save | Mount 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 #
- Dokumentasi Storage Docker
— perilaku bind mount, volume, dan tipe objek yang tersimpan di
data-root. - Referensi docker system df
— flag
-vserta arti kolomSHARED SIZE,UNIQUE SIZE, danLINKS. - Referensi docker system prune
— cakupan flag
--volumesterhadap anonymous volume. - Referensi docker volume ls — daftar volume bernama dan anonim di host.