Container Docker bukan mesin virtual dan bukan satu fitur kernel khusus. Container adalah proses biasa yang dijalankan dengan delapan jenis namespace Linux dan batas sumber daya dari cgroup, lalu root filesystem-nya diganti ke direktori image. Pemahaman ini penting karena hampir semua perilaku aneh Docker, dari isolasi jaringan sampai hak akses root, berasal dari susunan mekanisme kernel tersebut, bukan dari Docker sebagai aplikasi.
TL;DR
- Container adalah proses biasa di kernel host, terlihat di daftar proses dan memakai kernel yang sama dengan sistem utama.
- Isolasinya berasal dari delapan jenis namespace Linux, ditambah cgroup untuk membatasi CPU dan memori.
- Image pada dasarnya adalah kumpulan direktori yang dijadikan root baru lewat pivot_root atau chroot.
- Runtime runc ditulis dengan Go, dan itu memaksa penanganan khusus untuk syscall yang butuh proses single-thread.
- Daemon Docker secara default berjalan sebagai root, dan mode rootless adalah pilihan yang harus disiapkan, bukan bawaan.
Apa sebenarnya yang dijalankan saat docker run?
Yang dijalankan adalah proses biasa dengan root filesystem yang sudah diganti dan pembatasan kernel yang sudah dipasang. Tidak ada hypervisor, tidak ada kernel tamu, dan tidak ada mesin virtual di antaranya.
Alurnya kira-kira begini. Klien docker mengirim permintaan ke daemon. Daemon menyiapkan root filesystem dari layer image, membuat namespace baru, memasang batas cgroup, lalu meminta runtime OCI untuk mengeksekusi binary di dalam lingkungan itu. Runtime yang paling umum adalah runc, yang implementasinya tersedia di repositori opencontainers/runc.
Karena itu, proses di dalam container bisa dilihat dari host. Kalau dijalankan dengan hak yang cukup, isi /proc di host memperlihatkan proses tersebut seperti proses lain. Bedanya hanya pada namespace yang dipakai dan batas sumber daya yang menempel. Ini juga alasan container tidak bisa memakai kernel berbeda dari host: semua container berbagi kernel yang sama.
Konsekuensi praktisnya penting untuk keamanan. Celah keamanan kernel berlaku untuk semua container di mesin itu. Satu container yang berhasil keluar dari isolasi namespace berpotensi menyentuh proses lain, karena tidak ada batas hypervisor yang memisahkan mereka. Itulah sebabnya container yang menjalankan kode tidak tepercaya sebaiknya tetap diberi pembatasan tambahan seperti seccomp, AppArmor, atau dijalankan di mesin terpisah.
Namespace apa saja yang dipakai container?
Ada delapan jenis namespace yang tercatat di manual Linux, dan container modern memakai sebagian besar di antaranya. Kedelapannya adalah cgroup, IPC, mount, network, PID, time, user, dan UTS, seperti didokumentasikan di manual namespaces(7).
Namespace PID membuat proses di dalam container melihat dirinya sebagai PID 1, padahal di host dia punya nomor lain. Namespace mount memberi pandangan filesystem sendiri sehingga perubahan di dalam container tidak merusak host. Namespace network memberi antarmuka jaringan sendiri, lengkap dengan alamat IP dan tabel routing sendiri, yang kemudian dijembatani oleh driver jaringan Docker.
Namespace user adalah yang paling sering disalahpahami. Dengan namespace ini, UID 0 di dalam container bisa dipetakan ke UID biasa di host. Pemetaan itulah dasar dari mode rootless, yang dijelaskan di dokumentasi rootless Docker. Tanpa pemetaan ini, root di container sama dengan root di host, dan itu masalah besar kalau ada kesalahan konfigurasi mount.
Namespace UTS mengatur hostname, namespace IPC memisahkan antrean pesan dan shared memory, sementara namespace cgroup dan time mengatur pandangan terhadap hierarki cgroup dan jam sistem. Tidak semua runtime memakai kedelapannya sekaligus, dan tidak semua kernel mendukung semuanya, tapi daftar ini yang menjadi acuan saat membaca konfigurasi container.
Bagaimana cgroup membatasi CPU dan memori container?
cgroup mengelompokkan proses lalu menetapkan batas sumber daya pada kelompok itu. Pada cgroup versi 2, hierarkinya tunggal dan setiap batas dinyatakan sebagai berkas kontrol, misalnya batas memori dan kuota CPU, dengan penjelasan lengkap di dokumentasi cgroup v2.
Batas memori bekerja dengan menghitung pemakaian halaman memori oleh semua proses dalam grup. Ketika batas tercapai, kernel akan mencoba mengambil kembali cache, dan kalau masih tidak cukup, proses di dalam grup dibunuh. Itu sebabnya container yang kena batas memori sering mati dengan kode keluar 137, bukan error aplikasi yang jelas. Angka 137 berasal dari sinyal SIGKILL, dan penyebabnya harus dicari di batas cgroup, bukan di log aplikasi.
Batas CPU bekerja berbeda. Kuota CPU tidak mematikan proses, tapi membatasi jatah waktu eksekusi per periode. Container yang kena batas CPU akan terlihat lambat, bukan mati. Di sinilah banyak tim salah menyimpulkan bahwa aplikasinya lemot karena kode, padahal yang terjadi adalah throttling. Melihat berkas statistik cgroup langsung memberi jawaban yang jauh lebih cepat daripada menebak dari metrik aplikasi.
Yang perlu diingat, cgroup tidak mengisolasi. Dia hanya membatasi dan menghitung. Isolasi tetap tugas namespace. Dua mekanisme ini sering disebut sebagai satu paket, padahal perannya berbeda dan masalahnya juga berbeda saat terjadi gangguan.
Contoh konkretnya begini. Sebuah container dengan batas memori 512 MB akan dibunuh kernel ketika pemakaiannya melewati angka itu dan cache tidak lagi bisa dikembalikan. Container dengan kuota CPU setara 0,5 core akan mendapat jatah 50 ms per periode 100 ms, dan sisanya menunggu. Dua contoh ini menjelaskan kenapa gejala yang muncul berbeda: yang pertama mati mendadak, yang kedua melambat tanpa pesan error. Membaca nilai batas sebelum menebak penyebab menghemat banyak waktu.
Kenapa image disebut hanya kumpulan folder?
Karena pada akhirnya image adalah susunan direktori dan metadata yang dijadikan root filesystem baru untuk proses container. Layer image ditumpuk, lalu titik gabungannya dipakai sebagai root lewat pivot_root atau chroot.
Saat menjalankan image Ubuntu, Docker mengunduh layer-layer image tersebut, menyimpannya di direktori internal, lalu mengeksekusi binary di dalamnya dengan root yang sudah diganti. Binary yang dieksekusi adalah path di dalam root baru itu, bukan path di host. Kalau root baru tidak berisi shell, perintah untuk masuk ke container juga gagal, meskipun image-nya sehat.
Karena layer bersifat read-only dan ditumpuk, dua container dari image yang sama berbagi layer dasar. Perubahan yang ditulis container masuk ke layer atas yang bisa dibuang. Ini menjelaskan dua hal sekaligus: kenapa container baru selalu bersih, dan kenapa menulis banyak data ke filesystem container memperlambat serta memperbesar pemakaian disk.
Untuk data yang harus bertahan, gunakan volume atau bind mount. Menyimpan data penting di layer container adalah kesalahan klasik yang baru terasa saat container dijalankan ulang atau dipindahkan ke node lain.
Kenapa runc ditulis dengan Go dan apa efeknya?
runc adalah runtime OCI yang mengeksekusi container, dan Go dipilih karena ekosistem serta kemudahan distribusi binary. Efek sampingnya, Go punya runtime multi-thread yang tidak cocok untuk sebagian syscall yang harus dijalankan pada proses single-thread.
Banyak syscall yang dibutuhkan runtime container, terutama yang menyangkut namespace dan pemasangan filesystem, harus dijalankan dalam konteks single-thread. Untuk mengakalinya, runc memuat modul C yang berjalan sebelum runtime Go aktif, sehingga syscall tersebut bisa dieksekusi lebih dulu. Ini bukan bug, tapi konsekuensi desain yang perlu diketahui siapa pun yang membaca kode runtime container.
Go juga tidak menyediakan fork dengan cara yang sama seperti C. Fork hanya menggandakan thread utama, sementara runtime container justru butuh menggandakan proses, memasang batasan, lalu mengeksekusi program yang diminta. Jalan keluarnya adalah mengeksekusi ulang /proc/self/exe dengan argumen khusus yang menandai proses anak. Pola ini bekerja, tapi menjelaskan kenapa membaca kode runc terasa berbeda dari membaca kode sistem biasa.
Container atau mesin virtual, kapan memilih masing-masing?
Pilih container untuk kepadatan dan kecepatan mulai, pilih mesin virtual ketika isolasi kernel menjadi syarat. Container berbagi kernel host, sedangkan mesin virtual membawa kernel sendiri, dan perbedaan itu menentukan model ancaman yang masuk akal.
| Aspek | Container | Mesin virtual |
|---|---|---|
| Kernel | Berbagi kernel host | Kernel sendiri |
| Waktu mulai | Hitungan detik atau kurang | Hitungan detik sampai menit |
| Isolasi | Namespace dan cgroup | Hypervisor |
| Overhead memori | Rendah, tanpa kernel tamu | Lebih tinggi |
| Cocok untuk | Layanan dengan kode tepercaya | Kode tidak tepercaya, tenant terpisah |
| Risiko utama | Celah kernel dan salah konfigurasi | Permukaan serangan hypervisor |
Untuk sebagian besar layanan web yang kodenya ditulis sendiri, container adalah pilihan yang wajar. Untuk menjalankan kode pihak ketiga yang tidak diaudit, atau memisahkan pelanggan yang berbeda, mesin virtual masih lebih mudah dipertanggungjawabkan. Banyak platform besar memakai keduanya: container di dalam mesin virtual, supaya isolasi kernel tetap ada.
Bagaimana cara memeriksa namespace container yang sedang berjalan?
Periksa dari host, bukan dari dalam container. Setiap proses punya tautan di /proc yang menunjuk ke namespace yang dipakainya, sehingga perbandingan antar proses bisa dilakukan langsung tanpa alat tambahan.
Langkah pertama, cari PID proses container di host. Perintah untuk melihat proses beserta PID-nya cukup untuk itu. Setelah PID didapat, lihat isi direktori namespace milik proses tersebut dan bandingkan dengan namespace proses lain di host. Kalau tautannya berbeda, berarti isolasi namespace bekerja. Kalau sama, container tersebut berbagi namespace dengan host, dan itu biasanya tanda ada opsi tambahan yang tidak diinginkan.
Langkah kedua, periksa batas cgroup. Setiap proses mencatat grup cgroup-nya, dan dari situ batas memori serta kuota CPU bisa dibaca langsung. Cara ini lebih dapat dipercaya daripada membaca konfigurasi compose atau manifest Kubernetes, karena yang terlihat adalah nilai yang benar-benar dipasang kernel.
Langkah ketiga, periksa root filesystem. Membandingkan direktori root proses container dengan direktori root host menunjukkan apakah pivot_root berjalan sebagaimana mestinya. Kalau root proses masih menunjuk ke root host, container tersebut tidak terisolasi pada level filesystem, meskipun tampak berjalan normal dari sisi aplikasi.
Pemeriksaan ini berguna saat men-debug masalah izin, karena penyebab paling umum dari error permission di container adalah pemetaan UID yang tidak sesuai harapan. Melihat UID nyata di host sering langsung menjelaskan kenapa berkas yang sama bisa dibaca di satu container dan ditolak di container lain.
FAQ
Apakah container butuh root di host?
Secara bawaan, daemon Docker berjalan sebagai root karena banyak operasi kernel yang dibutuhkannya menuntut hak istimewa. Mode rootless menghilangkan kebutuhan itu dengan memetakan UID di dalam namespace user, tapi ada batasan pada jaringan dan beberapa driver penyimpanan.
Kenapa container saya mati dengan kode 137?
Kode 137 berarti proses dihentikan sinyal SIGKILL, dan penyebab paling umum adalah batas memori cgroup terlampaui. Periksa batas memori container dan pemakaian puncaknya, bukan hanya log aplikasi, karena kernel mematikan proses tanpa memberi kesempatan menulis log.
Apakah container bisa melihat proses container lain?
Tidak secara default, karena masing-masing punya namespace PID sendiri. Proses di host tetap bisa melihat semuanya, dan container dengan akses ke namespace host atau hak istimewa bisa kehilangan isolasi itu sepenuhnya.
Apakah image container berisi kernel?
Tidak. Image hanya berisi userspace, library, dan aplikasi. Kernel yang dipakai selalu kernel host, sehingga container tidak bisa memakai versi kernel berbeda dan tidak perlu booting saat dijalankan.
💬 Komentar (0)
Belum ada komentar. Jadilah yang pertama! 💬