Menulis Dockerfile yang bisa di-build memang tidak sulit, tapi menulis Dockerfile yang efisien, aman, cepat di-build ulang, dan mudah dipahami rekan tim lain adalah keterampilan yang terus berkembang lewat pengalaman. Bab ini merangkum kebiasaan lanjutan yang biasa dijaga tim Sysadmin/DevOps Engineer dan Developer berpengalaman setelah Dockerfile dasar sudah berjalan baik: instruksi yang membuat container lebih mudah dipantau, kebiasaan keamanan saat menulis instruksi, teknik BuildKit untuk mempercepat build berulang, sampai cara mendokumentasikan Dockerfile supaya orang lain (atau diri kita sendiri beberapa bulan kemudian) tidak perlu menebak-nebak alasan di balik sebuah baris instruksi.
42.1 Efisiensi Image dengan HEALTHCHECK dan COPY --link
Efisiensi Dockerfile tidak melulu soal ukuran image; instruksi yang tepat juga membuat Docker lebih efisien memantau kondisi container dan lebih presisi memakai ulang cache saat build berikutnya. Dua instruksi berikut sering terlewat meski dampaknya cukup besar pada operasional sehari-hari.
42.1.1 HEALTHCHECK untuk Deteksi Container Bermasalah
HEALTHCHECK memberi tahu Docker cara memeriksa apakah proses di dalam container masih benar-benar berfungsi, bukan cuma sekadar masih berjalan. Sebuah web server bisa saja statusnya tetap running menurut docker ps padahal proses di dalamnya sudah macet dalam infinite loop dan tidak lagi merespons koneksi baru; HEALTHCHECK menangkap kondisi semacam ini lewat status kesehatan terpisah yang dimulai dari starting, berubah jadi healthy setelah pemeriksaan berhasil, dan jadi unhealthy setelah sejumlah kegagalan berturut-turut.
FROM nginx:1.27-alpine
COPY index.html /usr/share/nginx/html/index.html
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost/ || exit 1Opsi --interval menentukan jeda antar pemeriksaan (default 30 detik), --timeout membatasi waktu maksimal satu pemeriksaan sebelum dianggap gagal (default 30 detik), --start-period memberi waktu inisialisasi di awal supaya kegagalan saat container baru saja start tidak langsung dihitung sebagai percobaan gagal (default 0 detik), dan --retries menentukan berapa kali kegagalan berturut-turut sebelum status berubah jadi unhealthy (default 3 kali). Build dan jalankan container ini, lalu periksa status kesehatannya.
docker build -t nginx-healthcheck:1.0 .
docker run -d -p 8080:80 --name demo-health nginx-healthcheck:1.0
docker inspect --format='{{.State.Health.Status}}' demo-healthKode keluar (exit code) dari perintah pemeriksaan menentukan status: 0 berarti sehat, 1 berarti tidak sehat, sedangkan kode 2 dicadangkan dan sebaiknya tidak dipakai. Di lapangan, orkestrator seperti Docker Swarm maupun load balancer di depan container memanfaatkan status ini untuk menentukan apakah traffic boleh diarahkan ke container tersebut, sehingga container yang unhealthy otomatis dikeluarkan dari rotasi tanpa perlu campur tangan manual. Bersihkan container percobaan setelah selesai.
docker rm -f demo-healthInstruksi STOPSIGNAL juga layak diperhatikan berdampingan dengan HEALTHCHECK, karena keduanya sama-sama berurusan dengan siklus hidup container. Secara default, Docker mengirim sinyal SIGTERM saat docker stop dipanggil, memberi proses utama kesempatan mematikan diri secara graceful sebelum akhirnya dipaksa berhenti lewat SIGKILL setelah batas waktu tertentu terlampaui. Kalau aplikasi di dalam image butuh sinyal berbeda untuk shutdown yang bersih, misalnya sebagian aplikasi berbasis Nginx yang merespons SIGQUIT untuk graceful shutdown, tetapkan lewat STOPSIGNAL.
FROM nginx:1.27-alpine
STOPSIGNAL SIGQUITSTOPSIGNAL hanya memengaruhi sinyal yang dikirim docker stop atau saat Docker daemon menghentikan container, dan tidak berlaku untuk Ctrl+C di terminal interaktif yang selalu mengirim SIGINT langsung ke proses.
42.1.2 COPY --link untuk Cache Layer Lebih Presisi
Secara default, setiap layer hasil COPY terhubung erat dengan layer-layer sebelumnya, sehingga perubahan pada instruksi sebelum COPY, misalnya mengganti versi base image, ikut membatalkan cache layer COPY tersebut meski isi file yang disalin sama sekali tidak berubah. Flag --link pada BuildKit mengatasi ini dengan menyalin file ke direktori kosong yang independen, lalu menautkan hasilnya sebagai layer terpisah di atas state sebelumnya, sehingga cache-nya tidak ikut batal hanya karena layer lain di bawahnya berubah.
# syntax=docker/dockerfile:1
FROM alpine:3.20
COPY --link app/ /opt/app/Manfaat --link makin terasa saat kita perlu mengganti tag base image tanpa mengubah source code, misalnya update patch versi Alpine karena ada CVE yang ditambal. Tanpa --link, seluruh layer COPY setelah FROM harus dieksekusi ulang meski isi file yang disalin identik; dengan --link, layer COPY tersebut bisa dipakai ulang dari cache karena statusnya independen dari base image di bawahnya. Fitur ini butuh syntax directive syntax=docker/dockerfile:1 di baris pertama Dockerfile supaya BuildKit mengenali sintaks tersebut.
42.2 Kebiasaan Keamanan saat Menulis Dockerfile
Pembahasan mendalam soal supply chain security, scanning, dan pemilihan base image minimal sudah dibahas di bab tentang keamanan image. Sub-bab ini melengkapi dengan dua kebiasaan spesifik saat menulis instruksi Dockerfile yang langsung memengaruhi keamanan build: mengunci base image sampai ke digest, dan memvalidasi integritas resource yang diunduh lewat ADD.
42.2.1 Kunci Base Image ke Digest
Tag image seperti node:20-alpine bisa menunjuk ke konten berbeda dari waktu ke waktu, karena maintainer image boleh saja mem-publish ulang tag yang sama dengan isi baru, misalnya setelah base OS di baliknya menerima patch keamanan. Untuk kebutuhan build yang benar-benar reproducible, kunci FROM sampai ke digest SHA-256, yang bersifat unik dan permanen untuk satu konten image tertentu.
docker pull node:20-alpine
docker inspect --format='{{index .RepoDigests 0}}' node:20-alpinePerintah di atas menampilkan digest lengkap dari image yang baru ditarik, dalam format node@sha256:.... Tempelkan digest tersebut pada instruksi FROM.
FROM node@sha256:1a2b3c4d5e6f7890abcdef1234567890abcdef1234567890abcdef1234567890Konsekuensinya, build tidak lagi otomatis mengikuti update base image; digest harus diperbarui manual (atau lewat automasi seperti Dependabot/Renovate) setiap kali ingin menarik konten terbaru dari tag yang sama. Di lapangan, Sysadmin/DevOps Engineer yang mengejar reproducibility tinggi di pipeline CI/CD biasanya menerima trade-off ini demi kepastian bahwa image yang sama persis akan selalu dihasilkan dari Dockerfile yang sama, tanpa kejutan dari perubahan base image yang terjadi di luar kendali tim.
42.2.2 Validasi Integritas Resource dengan ADD --checksum
Saat sebuah Dockerfile perlu mengunduh artifact dari luar internet, misalnya installer atau binary rilis dari repository pihak ketiga, instruksi ADD dengan opsi --checksum lebih dianjurkan dibanding menjalankan wget atau curl manual lewat RUN. Selain memberi cache build yang lebih presisi karena BuildKit tahu persis kapan resource tersebut berubah, --checksum memverifikasi hash SHA-256 dari file yang diunduh dan langsung menggagalkan build kalau hasilnya tidak cocok, mencegah artifact yang sudah dirusak atau ditukar diam-diam ikut masuk ke dalam image.
# syntax=docker/dockerfile:1
FROM alpine:3.20
ADD --checksum=sha256:5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be0 \
https://example.com/rilis/app-1.0.0.tar.gz /tmp/app.tar.gz
RUN tar -xzf /tmp/app.tar.gz -C /opt/ && rm /tmp/app.tar.gzKalau digest yang tertulis di --checksum tidak cocok dengan hasil unduhan, Docker menghentikan build dengan pesan error checksum mismatch alih-alih diam-diam melanjutkan dengan file yang salah. Dapatkan checksum resmi dari halaman rilis penerbit resource tersebut, bukan menghitungnya sendiri dari file yang sudah diunduh tanpa verifikasi sumbernya, karena tujuan utama --checksum adalah memastikan file yang kita terima memang sama dengan yang dipublikasikan penerbitnya. Skenario yang coba dicegah di sini nyata: Attacker yang berhasil membajak jalur distribusi artifact, misalnya lewat mirror registry yang dikompromikan atau serangan man-in-the-middle di jaringan CI/CD, bisa menyisipkan binary berbahaya yang tampak identik dari sisi nama file dan ukurannya. Tanpa --checksum, artifact palsu semacam ini akan lolos begitu saja masuk ke dalam image tanpa terdeteksi.
42.2.3 Checklist Review Dockerfile Sebelum Merge
Sebelum perubahan pada Dockerfile di-merge ke branch utama, beberapa poin berikut layak jadi bahan review, baik lewat proses manual di pull request maupun otomatis lewat linter seperti Hadolint di pipeline CI/CD.
| Poin Pemeriksaan | Alasan |
|---|---|
Base image memakai tag versi spesifik atau digest, bukan latest | Build tetap dapat diprediksi dan direproduksi |
Tidak ada credential atau API key tertulis lewat ENV, ARG, atau COPY | Data sensitif tidak tertinggal permanen di layer image |
Instruksi USER non-root ditulis sebelum CMD/ENTRYPOINT | Membatasi dampak kalau terjadi container escape |
HEALTHCHECK tersedia untuk aplikasi yang melayani traffic | Memungkinkan deteksi otomatis container yang macet |
Resource eksternal via ADD memakai --checksum | Mencegah artifact yang dirusak ikut masuk ke image |
42.3 Optimasi Build dengan BuildKit Cache
Cache layer bawaan Docker sudah membantu mempercepat build ulang, tapi cache ini batal total begitu ada perubahan pada instruksi manapun sebelum sebuah RUN, termasuk kalau hanya satu dependency baru ditambahkan ke package.json. BuildKit menyediakan mekanisme cache tambahan yang bertahan lintas build meski instruksi sebelumnya berubah, dan bisa dibagikan lintas mesin lewat registry.
42.3.1 RUN --mount=type=cache untuk Package Manager
Cache mount memasang direktori sementara yang isinya bertahan antar-invokasi builder, dipakai khusus untuk direktori cache compiler dan package manager seperti npm, pip, atau apt. Berbeda dari layer biasa, isi direktori cache mount ini tidak pernah ikut tersimpan sebagai bagian dari layer image final; direktori tersebut hanya tersedia selama instruksi RUN yang bersangkutan berjalan.
# syntax=docker/dockerfile:1
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ciBuild pertama tetap mengunduh seluruh dependency seperti biasa, tapi build berikutnya, bahkan ketika layer cache normal sudah batal karena package-lock.json berubah, tetap bisa memakai ulang paket yang sudah pernah diunduh sebelumnya dari cache mount tadi, sehingga hanya paket baru atau yang berubah versi saja yang perlu diunduh ulang. Pola serupa berlaku untuk apt-get di base image Debian/Ubuntu, dengan opsi sharing=locked supaya build paralel tidak saling berebut akses ke direktori cache yang sama.
# syntax=docker/dockerfile:1
FROM ubuntu:24.04
RUN rm -f /etc/apt/apt.conf.d/docker-clean; \
echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && apt-get install -y --no-install-recommends curlBaris rm -f /etc/apt/apt.conf.d/docker-clean perlu ditambahkan karena image dasar Ubuntu/Debian secara default mengonfigurasi apt untuk otomatis menghapus file .deb yang sudah diunduh setelah instalasi selesai, sebuah kebiasaan yang justru bertentangan dengan tujuan cache mount yang ingin mempertahankan file tersebut lintas build. Opsi id pada --mount=type=cache berguna kalau ingin beberapa cache terpisah untuk kebutuhan berbeda dalam satu Dockerfile yang sama, misalnya cache pip yang berbeda antar stage.
42.3.2 Cache Build Lewat Registry dengan Buildx
Cache mount di atas hanya tersimpan lokal di mesin builder yang menjalankannya, tidak banyak membantu kalau setiap build berjalan di runner CI/CD yang berbeda-beda dan selalu memulai dari kondisi bersih. Buildx, plugin CLI resmi Docker untuk build lanjutan, menyediakan opsi --cache-to dan --cache-from untuk mengekspor dan mengimpor cache build lewat registry, sehingga cache bisa dipakai bersama lintas mesin maupun lintas job CI/CD.
docker buildx build --push -t registry.example.com/app:1.0 \
--cache-to type=registry,ref=registry.example.com/app:buildcache \
--cache-from type=registry,ref=registry.example.com/app:buildcache .Setiap backend cache storage, termasuk registry, harus secara eksplisit diekspor lewat --cache-to dan diimpor lewat --cache-from; berbeda dari cache BuildKit lokal yang selalu aktif otomatis tanpa perlu dikonfigurasi. Kalau target --cache-from belum pernah ada sebelumnya, misalnya di build pertama kali, langkah impor cache akan gagal secara diam-diam tanpa menghentikan build, dan build tetap lanjut tanpa cache tersebut. Untuk mengimpor cache dari lebih dari satu sumber, misalnya cache dari branch saat ini digabung dengan cache dari branch utama, tulis --cache-from berulang kali.
docker buildx build --push -t registry.example.com/app:1.0 \
--cache-to type=registry,ref=registry.example.com/app:buildcache-feature \
--cache-from type=registry,ref=registry.example.com/app:buildcache-feature \
--cache-from type=registry,ref=registry.example.com/app:buildcache-main .Perhatikan bahwa satu lokasi cache tidak boleh ditulisi dua kali secara bersamaan tanpa saling menimpa data sebelumnya; kalau ingin menjaga cache terpisah per branch Git, pastikan setiap branch memakai referensi cache yang berbeda seperti contoh di atas. Buildx juga mendukung build multi-platform sekaligus lewat opsi --platform, berguna kalau image yang sama perlu berjalan baik di runner linux/amd64 maupun perangkat linux/arm64 seperti Apple Silicon atau server ARM.
docker buildx build --platform linux/amd64,linux/arm64 \
-t registry.example.com/app:1.0 --push .Build multi-platform seperti ini butuh builder Buildx yang mendukung eksekusi lintas arsitektur, biasanya lewat emulasi QEMU, dan bisa memakan waktu jauh lebih lama dibanding build satu platform karena setiap target arsitektur pada dasarnya di-build secara terpisah sebelum digabung jadi satu manifest list.
42.4 Dokumentasi Dockerfile
Dockerfile yang efisien dan aman tetap sulit dipelihara kalau tidak ada yang tahu alasan di balik pilihan tertentu, misalnya kenapa base image tertentu dipilih atau kenapa satu instruksi RUN ditulis dengan cara yang tidak lazim. Dokumentasi yang baik menyelamatkan waktu tim di kemudian hari, jauh lebih murah dibanding harus menebak ulang konteks yang sudah hilang.
42.4.1 LABEL dan OCI Image Annotations
Instruksi LABEL menambahkan metadata permanen ke image dalam bentuk pasangan key-value, dan bisa diperiksa kapan saja lewat docker image inspect tanpa perlu membuka Dockerfile aslinya. Open Container Initiative (OCI) mendefinisikan sekumpulan key LABEL standar berawalan org.opencontainers.image.* yang dikenali luas oleh tooling ekosistem container, menggantikan instruksi MAINTAINER yang sudah dianggap deprecated di Dockerfile reference resmi Docker.
FROM node:20-alpine
LABEL org.opencontainers.image.title="Payment Service"
LABEL org.opencontainers.image.version="1.4.2"
LABEL org.opencontainers.image.authors="[email protected]"
LABEL org.opencontainers.image.source="https://github.com/contoh/payment-service"
LABEL org.opencontainers.image.description="Service pemrosesan pembayaran internal"
LABEL org.opencontainers.image.licenses="MIT"Key org.opencontainers.image.source khususnya berguna untuk melacak image kembali ke repository sumbernya, sementara org.opencontainers.image.revision bisa diisi otomatis dengan commit hash Git saat build lewat ARG supaya setiap image bisa ditelusuri persis dibuat dari commit yang mana.
FROM node:20-alpine
ARG GIT_REVISION=unknown
LABEL org.opencontainers.image.revision=$GIT_REVISIONdocker build --build-arg GIT_REVISION=$(git rev-parse HEAD) -t app:1.0 .Verifikasi label yang sudah tertanam di image lewat docker image inspect, memfilter khusus bagian Labels supaya output lebih ringkas.
docker image inspect app:1.0 --format='{{json .Config.Labels}}'42.4.2 Komentar dan README Pendamping
Komentar di dalam Dockerfile ditulis dengan tanda pagar (#) di awal baris, dan paling berguna untuk menjelaskan alasan (why) di balik sebuah keputusan yang tidak jelas hanya dari membaca instruksinya, bukan mengulang apa yang sudah jelas terbaca dari instruksi itu sendiri.
# syntax=docker/dockerfile:1
# Base image dikunci ke versi 20 LTS, jangan naikkan ke versi major baru
# tanpa pengujian kompatibilitas dependency native di package.json.
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
# --omit=dev karena devDependencies (test runner, linter) tidak dibutuhkan
# di image production dan cuma menambah ukuran image tanpa manfaat runtime.
RUN npm ci --omit=dev
COPY . .
USER node
CMD ["node", "server.js"]Untuk proyek dengan Dockerfile yang cukup kompleks, misalnya memakai banyak build argument atau beberapa varian Dockerfile untuk environment berbeda, dokumentasi tambahan di README.md yang berdampingan dengan Dockerfile membantu Developer baru memahami cara memakainya tanpa harus membongkar seluruh isi file satu per satu. README pendamping ini sebaiknya mencakup daftar build argument yang tersedia beserta nilai default dan fungsinya, contoh perintah docker build yang lengkap dengan opsi yang lazim dipakai, serta catatan singkat soal port yang di-EXPOSE dan volume yang diharapkan di-mount saat container dijalankan. Dokumentasi semacam ini juga mempermudah proses onboarding Developer baru yang baru pertama kali menyentuh proyek tersebut, karena mereka tidak perlu bertanya langsung ke rekan tim lain hanya untuk mengetahui cara build image yang benar.

