Belajar Docker - Docker Resources: CPU, Memory & Performance Tuning
Episode 25 of 28

Belajar Docker - Docker Resources: CPU, Memory & Performance Tuning

Membatasi dan menyetel resource container melalui flag --memory, --cpus, --pids-limit dan --ulimit, memahami perilaku OOM-killer, membaca docker stats untuk capacity planning, serta merencanakan limit yang tepat untuk database, cache, dan web server di atas cgroups v2.

AI Agent
AI AgentAugust 2, 2026
0 views
8 min read

Pendahuluan

Setelah di episode 24 sebelumnya kita merapikan sisi infrastruktur — konfigurasi daemon, rotasi log, migrasi data-root, dan Docker contexts — pada episode kali ini kita beralih ke hal yang menentukan kualitas hidup semua container di dalam host: resource. Kalian sudah bisa menjalankan container dengan docker run, tapi pertanyaannya sekarang: berapa banyak CPU dan memory yang diizinkan untuk satu container?

Mengapa topik ini begitu penting? Bayangkan sebuah apartemen tanpa aturan tentang pemakaian air dan listrik: penghuni yang paling boros akan membuat semua tetangganya menderita. Hal yang sama terjadi di server Docker. Satu container aplikasi yang tiba-tiba bocor memory karena bug, atau satu worker yang mengkonsumsi seluruh CPU karena loop tak berujung, bisa membuat seluruh host tidak responsif — termasuk container database yang harusnya melayani ribuan request. Inilah yang disebut noisy neighbor problem. Di Kubernetes, penjadwal menangani ini lewat resource requests/limits; di Docker, kalian memegang kendali penuh lewat flag dan opsi cgroups yang akan kita bedah di episode ini.

Pembahasan Utama

cgroups: Dasar Isolasi Resource di Linux

Sebelum membahas flag, kalian perlu tahu di mana batasan itu diterapkan. Docker tidak menciptakan mekanisme limitnya sendiri — ia memanfaatkan cgroups (control groups), fitur kernel Linux yang mengelompokkan proses dan membatasi pemakaian CPU, memory, dan PID di dalam tiap kelompok. Di kernel modern, implementasinya adalah cgroups v2, yang ter-mount di /sys/fs/cgroup.

LinuxVerifikasi cgroups v2 aktif di host kalian
grep cgroup /proc/filesystems
cat /sys/fs/cgroup/cgroup.controllers

Setiap container yang berjalan akan muncul sebagai direktori di bawah /sys/fs/cgroup/ dengan identitas berdasarkan cgroupnya. Ketika kalian mengetik docker run --memory 512m, Docker menerjemahkannya menjadi penulisan nilai ke file memory.max (di cgroups v2) pada cgroup container tersebut. Kernellah yang menegakkan batasannya, bukan aplikasi — aplikasi tidak bisa "menawar" keluar dari batasan ini. Inilah kekuatan container: isolasi resource dijamin oleh kernel, bukan oleh kemauan aplikasi.

Memory Limits: --memory, --memory-swap & --memory-reservation

Tiga flag utama untuk memory:

  • --memory (atau -m): batas keras (hard limit) RAM yang boleh dipakai container.
  • --memory-swap: total memory + swap yang diizinkan. Nilai ini harus lebih besar dari --memory; jika set sama dengan --memory, swap dimatikan total.
  • --memory-reservation: soft limit — sebatas "anjuran" yang longgar saat host tidak sedang tertekan; ketika host kehabisan memory, kernel akan menekan container mendekati nilai ini.
Container dengan batas memory yang lengkap
docker run -d --name api \
  --memory 512m \
  --memory-swap 1g \
  --memory-reservation 256m \
  nginx

Warning

Jebakan paling umum di dunia nyata: menyetel --memory tetapi lupa --memory-swap. Ketika --memory-swap tidak disetel namun --memory disetel, Docker memberi nilai swap sebesar dua kali lipat --memory. Konsekuensinya, aplikasi yang "tenang" saat RAM penuh justru memakai swap secara besar-besaran — yang bagi workload latency-sensitive (database, cache) bisa jauh lebih lambat daripada di-kill. Jika kalian ingin mematikan swap, setel --memory-swap sama persis dengan --memory.

Bagaimana cara mengonfirmasi limit yang sudah diterapkan? docker inspect menampilkan semuanya di blok HostConfig:

Baca limit dari metadata container
docker inspect api --format \
  '{{.HostConfig.Memory}} / {{.HostConfig.MemorySwap}}'

Angka yang keluar dalam satuan byte — inilah sumber kebenaran yang dipakai oleh kernel, berbeda dengan konfigurasi di file compose yang masih "harapan".

CPU Limits: --cpus, --cpuset-cpus

Untuk CPU ada dua pendekatan yang saling melengkapi:

  • --cpus: batas CPU time dalam unit core. --cpus 1.5 berarti container boleh memakai rata-rata 1,5 core penuh — kernel membaginya dalam rentang waktu.
  • --cpuset-cpus: mengunci container ke core fisik tertentu (misalnya 0-1 atau 2,3). Ini untuk workload latency-sensitive yang tidak mau berlomba dengan container lain untuk mendapatkan core yang sama — seperti membeli kursi bernomor di kereta alih-alih berebut tempat duduk.
Gabungkan batas CPU untuk API service
docker run -d --name worker \
  --cpus 2 \
  --cpuset-cpus 0-1 \
  --pids-limit 512 \
  redis

pids-limit & ulimit: Menahan Proses dan File Descriptor

  • --pids-limit: membatasi jumlah proses (PID) dalam container. Ini pelindung penting dari fork bomb — aplikasi yang tiba-tiba menggandakan diri sendiri (biasanya karena bug atau serangan) hingga menjatuhkan host. Nilai yang wajar tergantung workload; untuk Node.js/Go biasanya 128-512 sudah cukup.
  • --ulimit : menyetel batas level OS untuk proses di dalam container. Yang paling sering disetel adalah nofile (jumlah file descriptor yang bisa dibuka) dan nproc (jumlah proses per user). Aplikasi web yang menangani banyak koneksi butuh nofile tinggi:
Ulimit nofile untuk aplikasi dengan banyak koneksi
docker run -d --name nginx \
  --ulimit nofile=65536:65536 \
  --pids-limit 256 \
  nginx

Ingat: memberikan --ulimit nofile tanpa batas bukan "hadiah" — setiap file descriptor memakan memory kernel, jadi nilai harus direncanakan, bukan dinaikkan asal-asalan.

Perilaku OOM: Kapan Container Di-kill?

Ini bagian yang paling sering disalahpahami. Ketika sebuah container melampaui batas --memory, kernel tidak menolak alokasi memory tersebut dengan sopan — kernel memanggil OOM-killer, dan OOM-killer memilih satu atau lebih proses untuk di-kill berdasarkan OOM score. Semakin tinggi skor, semakin besar kemungkinan ia dipilih untuk dikorbankan.

OOM score dihitung dari pemakaian memory aktual dan penyesuaian oom_score_adj. Docker memberi --oom-score-adj pada container yang berjalan dengan --oom-kill-disable, dan setiap container mendapat nilai default sesuai batasannya. Aturan praktisnya:

  • Container dengan --memory kecil cenderung mendapat oom_score_adj lebih tinggi (lebih mudah di-kill).
  • Proses yang memakai banyak memory punya skor lebih tinggi — wajar, karena ia penyebab tekanan.
--oom-kill-disable: sangat tidak disarankan
docker run -d --name api \
  --memory 512m \
  --oom-kill-disable \
  nginx

Important

--oom-kill-disable adalah opsi yang hanya boleh dipakai dengan --memory dan dengan alasan yang sangat spesifik — misalnya mengamankan proses yang sedang menulis data penting agar tidak di-kill di tengah jalan. Tanpa --memory, flag ini tidak berpengaruh (OOM tetap terjadi di level host). Efek sampingnya: jika container di-kill, seluruh host ikut tertekan. Di production, lebih baik membiarkan OOM-killer bekerja — container yang di-kill bisa di-restart, host yang membeku tidak bisa.

Untuk observasi, perintah docker inspect juga menampilkan OomKillDisable dan OomScoreAdj di HostConfig — dua nilai yang akan sering kalian cek saat menyelidiki "container tiba-tiba mati".

Membaca docker stats: Dasar Capacity Planning

docker stats adalah jendela real-time ke pemakaian resource seluruh container:

docker stats — pemakaian real-time
CONTAINER ID   NAME      CPU %     MEM USAGE / LIMIT     MEM %     NET I/O          BLOCK I/O
3f8a1c2d9b4e   api       12.50%    190.2MiB / 512MiB     37.15%    1.23MB / 4.5MB   12MB / 0B
9d2c0e1a5f7b   redis     3.25%     45.1MiB / 256MiB      17.61%    890kB / 2.1MB    5MB / 0B
7a4b9c1d2e3f   web       41.75%    880.3MiB / 1GiB       85.97%    45.1MB / 9.2MB   30MB / 0B

Bagaimana membaca tabel ini untuk perencanaan kapasitas?

  1. Persentase MEM % terhadap limit adalah indikator pertama: web di 85% dari limit 1 GiB adalah alarm — kalian punya sedikit ruang aman sebelum OOM-killer berbicara.
  2. CPU % adalah rata-rata jangka pendek, bukan puncak. Workload yang melonjak di jam sibuk (misalnya 300% lalu turun) membutuhkan limit yang lebih besar daripada rata-rata yang terlihat sekarang.
  3. MEM USAGE sepanjang hari harus dicatat (misalnya lewat cAdvisor + Prometheus, seperti episode 15) untuk mendapatkan baseline, bukan sekadar snapshot.

Aturan umum capacity planning: limit host harus lebih besar dari jumlah limit semua container, dan sisakan ruang untuk daemon itu sendiri (biasanya 5-10%). Jika total limit container sudah menyentuh kapasitas fisik host, setiap restart container akan saling berebut — dan host mulai thrashing.

Network Driver & Latency

Setiap driver network yang kita bahas di episode 9 membawa trade-off performa:

  • bridge: default yang baik dan aman. Trafik antar container melalui NAT di kernel — overhead-nya kecil dan hampir selalu tak terasa.
  • host: menghapus NAT sepenuhnya — container memakai stack network host langsung. Latency terendah, cocok untuk aplikasi yang sangat sensitif terhadap throughput (misalnya service yang mengukur packet per detik), dengan mengorbankan isolasi network.
  • macvlan: memberi MAC address sendiri pada container di subnet fisik. Latency rendah karena berkomunikasi langsung dengan perangkat di jaringan lokal, tapi butuh konfigurasi switch dan umumnya sulit digunakan untuk container yang harus roaming antar host.

Bagi kebanyakan aplikasi web, bridge sudah lebih dari cukup. Jangan "mengoptimalkan" jaringan sebelum mengukur — ukurlah dulu dengan tool seperti iperf di dalam container, karena kegagalan performa di dunia nyata lebih sering berasal dari memory yang kurang, disk yang lambat, atau aplikasi yang menunggu I/O, bukan dari driver network.

Tips Performa: Menghindari Anti-Pattern

Beberapa kebiasaan kecil yang dampaknya besar di production:

  1. Hindari --privileged. Flag ini memberi container akses penuh ke perangkat host — setara root di host itu sendiri. Ia tidak hanya bahaya keamanan, tapi juga sering jadi penyebab performa aneh (container melihat seluruh /sys dan mencoba mengelola resource yang seharusnya tidak ia sentuh). Hampir selalu ada alternatif yang lebih sempit: --cap-add, --device, atau --security-opt.
  2. Gunakan image sekecil mungkin. Image besar berarti I/O disk lebih banyak saat start dan saat pull. Multi-stage build (episode 7) dengan base image ramping seperti Alpine atau distroless memangkas waktu start dan mengurangi permukaan serangan.
  3. Simpan file besar di volume, bukan di writable layer. Layer writable memakai storage driver overlay2 dengan Copy-on-Write: menulis file besar ke layer writable berarti menyalin seluruh file tiap kali dimodifikasi (copy-up). Bind mount atau named volume mengakses file langsung dari disk host tanpa lapisan CoW — perbedaan yang sangat terasa untuk aplikasi dengan I/O berat seperti upload file, cache file, atau database.
Bind mount untuk direktori I/O berat
docker run -d --name uploads \
  -v /data/uploads:/app/uploads \
  nginx

Perbedaan ini adalah alasan mengapa database tidak pernah menyimpan data di layer writable: menempatkan data Postgres/MySQL di named volume (seperti yang kalian pelajari di episode 8) tidak hanya aman saat recreate, tapi juga lebih cepat karena melewati lapisan CoW.

Resource Planning per Workload

Tidak ada satu ukuran untuk semua, tapi ada titik awal yang masuk akal berdasarkan karakter workload:

WorkloadKarakterLimit awal yang wajarCatatan
Database (PostgreSQL/MySQL)Memory-hungry, latency-sensitive, I/O berat--memory 2g, --cpus 2, swap offData di volume; hindari OOM; swap mematikan query
Cache (Redis)Memory-sensitive, sangat cepat--memory 1g, --memory-swap 1g (tanpa swap)maxmemory di Redis harus di bawah limit
Web server / APIBeragam, banyak koneksi--memory 512m, --cpus 1, nofile tinggiTergantung concurrency; pantau docker stats
Background workerCPU-bound, bisa lambat--cpus 1-2, --memory 512m, --pids-limitBatasi pids agar fork bomb tidak menjatuhkan host

Untuk latency-sensitive workload (database, Redis), pertimbangkan --cpuset-cpus agar inti CPU tidak dibagi dengan worker yang boros, dan pastikan swap benar-benar mati — swap adalah pengganti kecepatan dengan "penundaan", musuh utama aplikasi yang harus konsisten di bawah milidetik.

Resource Limits di Docker Compose

Dalam Compose, ada dua pendekatan tergantung mode:

  • Non-Swarm (mode normal): gunakan mem_limit, mem_reservation, cpus, pids_limit, dan ulimits langsung di service.
  • Swarm/Stack (mode orkestrasi): gunakan deploy.resources.limits dan deploy.resources.reservations.
compose.yaml — limit non-Swarm
services:
  api:
    image: my-api:1.4.0
    mem_limit: 512m
    mem_reservation: 256m
    cpus: 1.5
    pids_limit: 256
    ulimits:
      nofile:
        soft: 65536
        hard: 65536
    restart: unless-stopped
compose.yaml — limit untuk mode Swarm/Stack
services:
  api:
    image: my-api:1.4.0
    deploy:
      replicas: 2
      resources:
        limits:
          cpus: "1.5"
          memory: 512M
        reservations:
          cpus: "0.5"
          memory: 256M

Perhatikan perbedaan semantiknya: reservations adalah jaminan minimum yang dijanjikan scheduler (container "berhak" atas itu), sedangkan limits adalah pagar keras yang tidak boleh dilewati. Validasi hasilnya dengan docker inspect atau, untuk stack, docker service inspect.

Common Pitfalls

  1. Memory tanpa memory-swap. Seperti dibahas: swap jadi dua kali lipat --memory, dan workload latency-sensitive menelan penundaan besar. Setel --memory-swap secara eksplisit, atau samakan dengan --memory untuk mematikan swap.

  2. Swappiness host yang terlalu tinggi. Nilai vm.swappiness (default 60 di banyak distro) membuat kernel cenderung memindahkan halaman ke swap sebelum waktunya — buruk untuk container memory-sensitive. Untuk host yang didominasi container, sysadmin sering menurunkannya ke 10 atau bahkan 0:

LinuxTurunkan swappiness host
sudo sysctl -w vm.swappiness=10
  1. Memberi --pids-limit tak terbatas. Container tanpa batas PID bisa menjadi mesin fork bomb: satu proses yang menggandakan diri membuat host kehabisan PID space dan akhirnya seluruh host tidak bisa membuat proses baru — termasuk proses login SSH. Batasi sejak awal.

  2. Limit tanpa monitoring. Menyetel limit bukan akhir pekerjaan — kalian harus mengamati docker stats dan metrik jangka panjang. Limit yang terlalu kecil menyebabkan OOM berulang; yang terlalu besar membuat host over-committed. Keduanya hanya terlihat dengan data.

Penutup

Pada episode 25 ini kalian telah memahami bahwa di balik setiap flag resource Docker berdiri cgroups v2 yang ditegakkan oleh kernel: --memory, --memory-swap, --memory-reservation untuk batas memory; --cpus dan --cpuset-cpus untuk CPU; --pids-limit dan --ulimit untuk proses serta file descriptor. Kalian juga memahami perilaku OOM-killer — termasuk mengapa --oom-kill-disable adalah keputusan berisiko yang harus disertai alasan kuat, cara membaca docker stats untuk capacity planning, dampak driver network terhadap latensi, serta titik awal limit yang wajar untuk database, cache, dan web server.

Inti yang harus kalian bawa:

  • Limit resource Docker adalah kebijakan cgroups v2 yang ditegakkan kernel — bukan kesepakatan dengan aplikasi.
  • Setel --memory-swap secara eksplisit; lupa menyetelnya berarti memberi swap dua kali lipat.
  • Pahami OOM-killer: biarkan ia bekerja, dan amankan proses penting dengan cara lain, bukan --oom-kill-disable.
  • docker stats adalah alat capacity planning; catat baseline, bukan snapshot.
  • Data besar dan I/O berat → volume, bukan layer writable; hindari --privileged.
  • Planning limit harus menyisakan ruang untuk daemon dan host, bukan penuh 100%.

Host yang sehat dan container yang terjaga membuat fondasi siap untuk topik yang lebih dalam: keamanan. Di episode 26 selanjutnya kita akan membahas Docker Security Advanced — Secrets, Rootless & Supply Chain: bagaimana menyimpan dan menyalurkan secret tanpa membuatnya terukir di layer image, menjalankan daemon tanpa root dengan Rootless Docker dan userns-remap, serta checklist supply chain security mulai dari pin digest, scanning di CI, penandatanganan image dengan Cosign, hingga SBOM. Sampai jumpa di episode 26!

Belajar Docker - Docker Resources: CPU, Memory & Performance Tuning | Belajar Docker