Optimalisasi Kinerja

Optimalisasi Kinerja

Bitnesia Sep 12, 2026 11 EN

Image yang gemuk dan build yang lambat menghambat produktivitas Developer sehari-hari, sementara container yang boros resource bisa mengganggu service lain yang berjalan di server yang sama saat production. Optimalisasi kinerja Docker bukan cuma soal kecepatan, tapi juga soal efisiensi biaya infrastruktur dan stabilitas sistem secara keseluruhan. Bab ini membahas empat area optimalisasi yang paling sering jadi perhatian Sysadmin/DevOps Engineer maupun Developer: memperkecil ukuran image, mempercepat proses build, menjaga performa runtime container, dan mengatur alokasi resource supaya tidak ada satu container pun yang "memakan" seluruh kapasitas host.

27.1 Image Size Optimization

Image yang besar memperlambat proses docker pull saat deployment, memperbesar kebutuhan storage registry, dan memperluas attack surface karena lebih banyak package yang berpotensi punya vulnerability. Mengecilkan ukuran image jadi salah satu optimalisasi dengan dampak paling langsung terasa, baik dari sisi kecepatan maupun keamanan.

27.1.1 Multi-Stage Build dan Base Image Minimal

Multi-stage build memisahkan tahap kompilasi atau instalasi dependency dari tahap image akhir yang benar-benar dijalankan, sehingga tool build seperti compiler atau dependency development tidak ikut terbawa ke image production. Contoh berikut membangun aplikasi Go yang hasil akhirnya cuma berisi satu binary.

FROM golang:1.23 AS builder
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o app .

FROM gcr.io/distroless/static-debian12
COPY --from=builder /src/app /app
ENTRYPOINT ["/app"]

Stage builder memakai image golang:1.23 yang berukuran ratusan megabyte karena berisi seluruh toolchain Go, tapi stage akhir cuma menyalin satu file binary ke base image distroless yang tidak punya shell, package manager, atau tool tambahan apa pun. Pemilihan base image sama pentingnya dengan pola multi-stage itu sendiri. Image alpine yang memakai musl libc dan busybox jauh lebih kecil dibanding image berbasis Debian atau Ubuntu penuh, sementara base image distroless dari Google malah tidak menyertakan shell sama sekali, cocok untuk binary yang sudah dikompilasi statis dan tidak butuh proses debugging interaktif di dalam container.

Bandingkan ukuran base image sebelum memutuskan mana yang dipakai, karena selisihnya bisa signifikan untuk aplikasi yang sering di-deploy ulang.

docker pull python:3.12
docker pull python:3.12-slim
docker pull python:3.12-alpine
docker images python

Di lapangan, image alpine memang paling kecil, tapi variant ini memakai musl libc alih-alih glibc, yang kadang menimbulkan masalah kompatibilitas untuk package Python dengan native extension seperti numpy atau psycopg2 yang mengharapkan glibc. Variant slim jadi kompromi yang lebih aman untuk kasus seperti ini karena tetap memakai glibc tapi sudah membuang dokumentasi dan tool yang tidak esensial.

27.1.2 Mengurangi Jumlah Layer dan .dockerignore

Setiap instruksi RUN, COPY, dan ADD di Dockerfile menghasilkan satu layer baru. Menggabungkan beberapa perintah shell yang saling berkaitan ke satu instruksi RUN mengurangi jumlah layer sekaligus mencegah file sementara ikut tersimpan permanen di layer terpisah.

RUN apt-get update \
    && apt-get install -y --no-install-recommends curl ca-certificates \
    && rm -rf /var/lib/apt/lists/*

Perhatikan bahwa proses apt-get update, instalasi package, dan pembersihan cache apt digabung dalam satu instruksi RUN yang sama. Jika perintah pembersihan dipisah ke instruksi RUN tersendiri, cache apt tetap tersimpan permanen di layer instalasi sebelumnya meski secara logis sudah "dihapus" di layer berikutnya, karena setiap layer di image Docker bersifat immutable dan cuma menyimpan selisih perubahan filesystem. Opsi --no-install-recommends mencegah apt-get menambahkan package rekomendasi yang sebetulnya tidak wajib untuk menjalankan aplikasi.

File .dockerignore punya peran yang sering diabaikan dalam optimalisasi ukuran image, terutama untuk instruksi COPY . . yang menyalin seluruh isi build context. Tanpa .dockerignore, folder seperti node_modules, .git, atau file .env lokal ikut terkirim ke build context dan berpotensi ikut tersalin ke dalam image.

.git
node_modules
*.log
.env
dist
__pycache__/

Selain memperkecil image, .dockerignore juga mempercepat proses build karena Docker daemon tidak perlu mengirim file yang tidak relevan sebagai bagian dari build context ke builder.

27.1.3 Menganalisis Ukuran Layer dengan Dive

Perintah docker history menampilkan ukuran tiap layer dari sebuah image beserta instruksi Dockerfile yang menghasilkannya, cara paling cepat untuk mencari layer mana yang paling boros ukuran.

docker history payment-service:latest

Untuk analisis yang lebih detail sampai ke tingkat file individual di dalam tiap layer, tool Dive memberi tampilan interaktif yang memisahkan kontribusi ukuran per layer dan menandai potensi wasted space akibat file yang ditimpa atau dihapus di layer berikutnya tanpa benar-benar mengecilkan ukuran image.

dive payment-service:latest

Dive menampilkan skor efisiensi image beserta daftar file yang paling banyak memakan ruang, membantu menemukan kasus seperti file cache atau log build yang tidak sengaja ikut tersalin ke image akhir. Untuk verifikasi cepat tanpa install tool tambahan, bandingkan ukuran image sebelum dan sesudah optimalisasi lewat docker images dan pastikan selisihnya sesuai ekspektasi setelah menerapkan multi-stage build atau mengganti base image.

27.2 Build Speed Improvement

Build yang lambat menghambat iterasi Developer saat development dan memperpanjang waktu tunggu pipeline CI/CD sebelum image siap di-deploy. BuildKit, builder default sejak Docker Engine 23.0, menyediakan beberapa fitur cache yang jauh lebih fleksibel dibanding builder generasi sebelumnya (legacy builder).

27.2.1 Cache Mount BuildKit untuk Dependency

Cache mount memungkinkan sebuah instruksi RUN memakai direktori cache persisten yang tidak ikut tersimpan sebagai bagian dari layer image, cocok untuk cache package manager seperti pip, npm, atau apt yang seharusnya dipakai ulang antar-build tapi tidak perlu ikut membesarkan ukuran image akhir. Fitur ini butuh baris syntax directive di baris pertama Dockerfile.

# syntax=docker/dockerfile:1
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt
COPY . .
CMD ["python", "app.py"]

Tanpa --mount=type=cache, setiap perubahan pada requirements.txt memaksa pip mengunduh ulang seluruh dependency dari awal karena layer instalasi dianggap invalid begitu file sumbernya berubah. Cache mount membuat direktori /root/.cache/pip tetap tersimpan di cache BuildKit lintas-build, sehingga package yang sudah pernah diunduh tidak perlu diunduh ulang meski layer RUN-nya sendiri harus dieksekusi ulang. Verifikasi cache bekerja dengan menjalankan build dua kali berturut-turut dan bandingkan waktu yang dibutuhkan pada tahap instalasi dependency.

time docker build -t myapp .
time docker build -t myapp .

27.2.2 Urutan Instruksi dan Build Cache

BuildKit maupun legacy builder sama-sama memakai mekanisme layer cache yang menganggap sebuah layer masih valid selama instruksi dan konteksnya tidak berubah dari build sebelumnya. Begitu satu layer dianggap berubah, seluruh layer setelahnya ikut dibangun ulang meski isinya sebetulnya tidak terkait. Susun instruksi Dockerfile dari yang paling jarang berubah ke yang paling sering berubah supaya cache tetap valid selama mungkin.

FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
CMD ["node", "server.js"]

Menyalin package.json dan package-lock.json lebih dulu sebelum menjalankan npm ci, baru kemudian menyalin seluruh source code, membuat layer instalasi dependency tetap memakai cache selama file lock tidak berubah, meski kode aplikasi berubah setiap commit. Kalau urutannya dibalik, yaitu COPY . . dijalankan sebelum instalasi dependency, setiap perubahan sekecil apa pun pada source code memaksa seluruh proses instalasi dependency diulang dari nol karena layer COPY di atasnya sudah invalid.

27.2.3 Build Paralel dengan Buildx Bake

Project yang punya banyak target image, misalnya beberapa microservice dalam satu monorepo, sering butuh build lebih dari satu image sekaligus. docker buildx bake menjalankan beberapa build sesuai definisi dari satu file konfigurasi, dengan target yang tidak saling bergantung dijalankan paralel oleh BuildKit.

group "default" {
  targets = ["api", "worker"]
}

target "api" {
  context    = "./api"
  dockerfile = "Dockerfile"
  tags       = ["myorg/api:latest"]
}

target "worker" {
  context    = "./worker"
  dockerfile = "Dockerfile"
  tags       = ["myorg/worker:latest"]
}

Simpan konfigurasi di atas sebagai docker-bake.hcl, lalu jalankan seluruh group lewat satu perintah.

docker buildx bake

Dibanding menjalankan docker build satu per satu untuk tiap service secara berurutan, buildx bake memanfaatkan paralelisme BuildKit sehingga total waktu build mendekati waktu build target yang paling lambat, bukan akumulasi seluruh target. Di lapangan, manfaat ini makin terasa di pipeline CI dengan banyak core CPU menganggur karena build sekuensial tidak memanfaatkan resource yang tersedia secara maksimal.

27.3 Runtime Performance

Image yang kecil dan build yang cepat tidak otomatis berarti container berjalan optimal saat production. Konfigurasi runtime seperti storage driver, logging driver, dan health check ikut menentukan seberapa responsif dan stabil sebuah container di bawah beban kerja nyata.

27.3.1 Memilih Storage Driver dan Logging Driver

overlay2 adalah storage driver default dan yang direkomendasikan Docker untuk kernel Linux modern, menggantikan driver lama seperti aufs atau devicemapper yang sudah dianggap legacy. Periksa storage driver yang aktif lewat docker info.

docker info --format '{{.Driver}}'

Logging driver juga berdampak langsung ke performa, terutama untuk aplikasi yang menghasilkan log dalam volume besar. Driver default json-file menyimpan seluruh log sebagai file JSON di host tanpa batasan ukuran kecuali dikonfigurasi eksplisit, yang lama-kelamaan bisa memenuhi disk host dan memperlambat proses baca log lewat docker logs.

docker run -d \
  --log-driver json-file \
  --log-opt max-size=10m \
  --log-opt max-file=3 \
  nginx:alpine

Opsi max-size=10m membatasi ukuran satu file log sebelum dilakukan rotasi, sementara max-file=3 membatasi jumlah file log yang disimpan sebelum file paling lama dihapus otomatis. Tanpa batasan ini, Sysadmin/DevOps Engineer di lapangan cukup sering menemukan disk host penuh bukan karena data aplikasi, melainkan karena log container yang tidak pernah dirotasi bertahun-tahun.

27.3.2 Mengoptimalkan Health Check

Instruksi HEALTHCHECK membantu orchestrator maupun Sysadmin/DevOps Engineer mendeteksi container yang secara teknis masih berjalan tapi aplikasinya sudah tidak merespons dengan benar. Interval health check yang terlalu agresif bisa membebani aplikasi dengan request pengecekan yang berulang, sementara interval yang terlalu longgar memperlambat deteksi masalah.

HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
  CMD curl -f http://localhost:8080/health || exit 1

--start-period memberi masa tenggang di awal container berjalan sebelum kegagalan health check dihitung sebagai unhealthy, penting untuk aplikasi yang butuh waktu warm-up seperti aplikasi Java atau aplikasi yang memuat cache besar saat startup. Tanpa masa tenggang ini, container bisa langsung ditandai unhealthy dan berpotensi di-restart paksa oleh orchestrator padahal aplikasinya sebetulnya masih dalam proses inisialisasi normal. Pastikan endpoint health check sendiri ringan dan tidak melakukan query berat ke database, karena endpoint yang lambat justru menambah beban tiap kali dipanggil sesuai --interval yang dikonfigurasi.

Verifikasi status health check lewat docker inspect atau kolom STATUS pada docker ps.

docker inspect --format='{{.State.Health.Status}}' payment-api

27.3.3 Memantau Performa dengan Docker Stats

docker stats menampilkan penggunaan CPU, memory, network I/O, dan block I/O secara real-time untuk seluruh container yang sedang berjalan, cara paling cepat mendeteksi container mana yang jadi sumber beban berlebih di host.

docker stats --no-stream

Opsi --no-stream menampilkan satu snapshot lalu keluar, cocok dipakai dalam script monitoring dibanding mode default yang terus memperbarui tampilan tiap detik. Untuk investigasi lebih dalam saat CPU sebuah container tiba-tiba melonjak, kombinasikan dengan docker top untuk melihat proses spesifik apa yang berjalan di dalam container tersebut.

docker top payment-api

Di lapangan, lonjakan CPU yang tidak wajar pada satu container sering kali berasal dari proses child yang tidak ter-manage dengan benar, misalnya proses zombie yang menumpuk karena container tidak menjalankan init process yang benar. Opsi --init pada docker run menyisipkan init process minimal (tini) yang menangani proses zombie ini secara otomatis.

27.4 Resource Allocation

Tanpa batasan eksplisit, sebuah container secara default bisa memakai seluruh CPU dan memory yang tersedia di host, berisiko membuat satu aplikasi yang bermasalah mengganggu stabilitas service lain yang berjalan di host yang sama. Docker memakai fitur cgroups (control groups) di kernel Linux untuk membatasi resource tiap container secara terisolasi.

27.4.1 Membatasi Memory dan Swap

Opsi --memory membatasi jumlah memory maksimum yang boleh dipakai sebuah container. Begitu batas ini terlampaui, kernel akan mematikan proses di dalam container lewat OOM killer (Out Of Memory killer).

docker run -d --memory=512m --memory-swap=512m nginx:alpine

Menyamakan nilai --memory-swap dengan --memory secara efektif mematikan penggunaan swap untuk container tersebut, karena menurut dokumentasi resmi Docker, --memory-swap merepresentasikan total memory plus swap yang boleh dipakai, sehingga nilai yang sama dengan --memory berarti alokasi swap tambahan adalah nol. Mematikan swap membuat container yang kehabisan memory langsung dihentikan oleh OOM killer alih-alih diam-diam melambat drastis karena terus-menerus swapping ke disk, perilaku yang di lapangan justru lebih mudah didiagnosis dibanding container yang "menggantung" tanpa pesan error jelas.

Periksa apakah sebuah container pernah kena OOM kill lewat docker inspect.

docker inspect --format='{{.State.OOMKilled}}' payment-api

27.4.2 Membatasi CPU

Opsi --cpus membatasi jumlah core CPU yang boleh dipakai sebuah container, dinyatakan dalam angka desimal yang merepresentasikan jumlah core, bukan persentase.

docker run -d --cpus=1.5 payment-worker:latest

Nilai 1.5 berarti container tersebut maksimal memakai setara satu setengah core CPU secara keseluruhan, meski host punya lebih banyak core yang tersedia. Untuk kasus banyak container yang perlu berbagi CPU secara proporsional (bukan dibatasi mutlak), opsi --cpu-shares mengatur prioritas relatif antar-container hanya ketika CPU host dalam kondisi jenuh (contention).

docker run -d --cpu-shares=1024 payment-api:latest
docker run -d --cpu-shares=512 payment-worker:latest

Memakai nilai bawaan --cpu-shares=1024 sebagai referensi, container payment-worker di atas mendapat proporsi setengah dari payment-api saat kedua container sama-sama berebut CPU, tapi kalau CPU host sedang tidak jenuh, kedua container tetap bebas memakai CPU semaksimal yang mereka butuhkan tanpa dibatasi. Berbeda dengan --cpus yang jadi batas mutlak, --cpu-shares murni soal prioritas relatif saat rebutan resource, jadi cocok dikombinasikan sesuai kebutuhan: pakai --cpus untuk batas keras demi mencegah satu container memonopoli host, pakai --cpu-shares untuk memastikan service yang lebih kritis tetap diprioritaskan saat beban tinggi.

27.4.3 Mengubah Limit Tanpa Restart Container

Mengubah limit resource biasanya berarti menghentikan dan menjalankan ulang container dengan opsi baru, proses yang menyebabkan downtime singkat meski cuma sekadar menyesuaikan batas memory atau CPU. Perintah docker update memungkinkan perubahan limit resource pada container yang sedang berjalan tanpa perlu restart.

docker update --memory=1g --memory-swap=1g --cpus=2 payment-api

Perubahan lewat docker update langsung berlaku ke cgroup container yang bersangkutan tanpa mengganggu proses yang sedang berjalan di dalamnya, berguna saat Sysadmin/DevOps Engineer perlu menaikkan kapasitas sebuah service secara darurat ketika trafik melonjak tiba-tiba tanpa harus menunggu jendela maintenance untuk restart. Verifikasi limit baru sudah diterapkan lewat docker inspect.

docker inspect --format='{{.HostConfig.Memory}} {{.HostConfig.NanoCpus}}' payment-api

Perlu dicatat bahwa docker update hanya mengubah batas resource, bukan konfigurasi lain seperti port atau volume mount, dan perubahan ini tidak persisten kalau container nanti dihapus dan dibuat ulang dari image, sehingga untuk perubahan permanen tetap perlu memperbarui definisi docker run atau file Compose yang dipakai supaya limit yang sama diterapkan lagi di deployment berikutnya.