Sebelum kita bisa membangun Dockerfile sendiri secara efektif, ada baiknya memahami dulu apa yang sebenarnya terjadi di balik sebuah image Docker. Bab ini mengupas image lebih dalam dari sudut struktur dan penamaannya: bagaimana image tersusun dari banyak layer yang bisa saling dipakai ulang, kenapa base image yang dipilih sangat menentukan ukuran dan keamanan aplikasi kita nantinya, serta bagaimana sistem tag dan digest memastikan kita menjalankan versi image yang benar-benar kita maksud. Pemahaman ini jadi bekal penting, baik bagi Developer yang ingin image aplikasinya ringan dan cepat di-build, maupun Sysadmin/DevOps Engineer yang harus memastikan image yang di-deploy ke production konsisten dan bisa dilacak asal-usulnya.
4.1 Apa itu Docker Image?
Docker image adalah paket mandiri berisi semua yang dibutuhkan untuk menjalankan sebuah aplikasi: kode program, runtime, library, environment variable, dan konfigurasi default seperti perintah yang dijalankan saat container menyala. Image bersifat read-only dan immutable, artinya begitu sebuah image selesai dibangun, isinya tidak pernah berubah lagi. Kalau ada perubahan yang dibutuhkan, misalnya update dependency, hasilnya adalah image baru dengan identitas berbeda, bukan mengubah image lama secara langsung.
Secara format, image Docker mengikuti spesifikasi OCI (Open Container Initiative) Image Format, standar terbuka yang memastikan image bisa dibaca oleh berbagai container runtime, tidak terkunci hanya ke Docker. Setiap image terdiri dari dua bagian utama: manifest, file JSON yang mendaftar seluruh layer penyusun image beserta konfigurasinya (seperti CMD, ENTRYPOINT, dan environment variable default), dan kumpulan layer itu sendiri yang masing-masing berisi potongan filesystem dalam bentuk arsip terkompresi.
Kita bisa melihat gambaran isi sebuah image langsung dari command line tanpa perlu menjalankan container-nya. Tarik image nginx versi stable, lalu periksa detail konfigurasinya lewat docker image inspect.
docker pull nginx:1.27
docker image inspect nginx:1.27Output docker image inspect menampilkan struktur JSON lengkap: Id berupa digest unik image, Config berisi environment variable dan perintah default, RootFS.Layers mendaftar seluruh layer penyusun image dari yang paling bawah sampai paling atas, dan Architecture beserta Os yang menunjukkan platform target image tersebut. Bagian Architecture ini penting diperhatikan terutama oleh pengguna mesin Apple Silicon (arsitektur arm64), karena tidak semua image tersedia untuk semua arsitektur, dan menjalankan image dengan arsitektur yang tidak cocok bisa membuat container error atau berjalan lambat lewat emulasi.
Kalau perintah docker pull di atas gagal dengan pesan "manifest unknown" atau "not found", penyebab paling umum adalah salah ketik nama image atau tag yang memang tidak tersedia di registry; cek ulang ejaan dan daftar tag yang tersedia lewat halaman Docker Hub image tersebut sebelum mencoba lagi.
4.2 Image Layers dan Layering Concept
Setiap instruksi di Dockerfile yang mengubah filesystem, seperti RUN, COPY, atau ADD, menghasilkan satu layer baru. Docker menyusun layer-layer ini secara bertumpuk lewat union filesystem, mekanisme yang menggabungkan beberapa filesystem terpisah menjadi satu tampilan filesystem tunggal yang terlihat utuh dari sudut pandang container. Storage driver yang paling umum dipakai untuk mengimplementasikan union filesystem ini adalah overlay2, default di Docker Engine modern untuk sistem Linux.
Keuntungan utama pendekatan berlapis ini adalah layer caching. Kalau ada dua image berbeda yang sama-sama memakai base image dan instruksi awal yang identik, Docker cukup menyimpan layer tersebut satu kali di disk dan memakainya ulang untuk kedua image, menghemat ruang penyimpanan sekaligus mempercepat proses pull karena layer yang sudah ada di mesin kita tidak perlu diunduh ulang. Mekanisme cache yang sama juga berlaku saat proses build: Docker membandingkan setiap instruksi Dockerfile dengan cache layer sebelumnya, dan hanya menjalankan ulang instruksi tersebut kalau ada perubahan pada instruksi itu sendiri atau file yang jadi inputnya.
Mari buktikan konsep ini lewat praktik langsung. Buat direktori kerja baru dan sebuah Dockerfile sederhana yang punya beberapa langkah RUN terpisah.
mkdir layer-demo && cd layer-demoFROM alpine:3.20
RUN echo "langkah pertama" > /tmp/step1.txt
RUN echo "langkah kedua" > /tmp/step2.txt
COPY . /appSimpan isi di atas sebagai Dockerfile, lalu build image untuk pertama kali sambil mencatat waktu prosesnya.
docker build -t layer-demo:1.0 .Perhatikan output build, setiap instruksi tercatat sebagai langkah terpisah. Sekarang lihat daftar layer yang terbentuk memakai docker image history.
docker image history layer-demo:1.0Output-nya menampilkan susunan layer dari yang paling baru (paling atas) ke yang paling lama (paling bawah), lengkap dengan ukuran tiap layer dan instruksi Dockerfile yang menghasilkannya. Sekarang, ubah baris terakhir Dockerfile saja, misalnya menambah satu instruksi RUN baru di bagian bawah, lalu build ulang.
FROM alpine:3.20
RUN echo "langkah pertama" > /tmp/step1.txt
RUN echo "langkah kedua" > /tmp/step2.txt
RUN echo "langkah ketiga" > /tmp/step3.txt
COPY . /appdocker build -t layer-demo:2.0 .Perhatikan baris "CACHED" di output build untuk tiga instruksi pertama; Docker mengenali bahwa instruksi tersebut tidak berubah sama sekali dari build sebelumnya, sehingga langsung memakai ulang layer yang sudah ada tanpa mengeksekusi ulang perintahnya. Sebaliknya, kalau instruksi yang diubah berada di tengah Dockerfile, bukan di akhir, seluruh layer sesudahnya ikut batal dari cache dan harus dibangun ulang, karena setiap layer bergantung pada hasil layer sebelumnya. Ini alasan kenapa urutan instruksi di Dockerfile sangat berpengaruh pada kecepatan build: menaruh instruksi yang jarang berubah (seperti instalasi dependency sistem) di bagian atas, dan instruksi yang sering berubah (seperti menyalin source code aplikasi) di bagian bawah, adalah pola umum yang dipakai Developer untuk memaksimalkan cache hit.
Di lapangan, Sysadmin/DevOps Engineer yang mengelola banyak image serupa (misalnya beberapa microservice yang sama-sama memakai base image Node.js versi tertentu) akan melihat penghematan disk yang cukup signifikan, karena layer base image tersebut hanya tersimpan satu kali di host meski dipakai puluhan image berbeda. Bersihkan image percobaan setelah selesai supaya tidak menumpuk di disk.
docker rmi layer-demo:1.0 layer-demo:2.04.3 Base Images dan Parent Images
Base image adalah image awal yang jadi fondasi sebuah image baru, ditentukan lewat instruksi FROM di Dockerfile. Istilah parent image sering dipakai bergantian dengan base image untuk merujuk hal yang sama, yaitu image yang menjadi titik mula sebelum layer tambahan ditumpuk di atasnya. Satu-satunya image yang benar-benar tidak punya parent adalah image scratch, image kosong yang tidak berisi filesystem apa pun, biasanya dipakai untuk membangun image minimal berisi satu binary statis saja.
Pemilihan base image berdampak langsung ke tiga hal: ukuran image akhir, permukaan serangan (attack surface) dari sisi keamanan, dan kompatibilitas dengan dependency aplikasi. Base image yang penuh dengan tools dan package bawaan distro memang praktis untuk debugging karena banyak utility tersedia langsung, tapi ikut membawa lebih banyak potensi vulnerability yang harus dipantau. Sebaliknya, base image minimal mengurangi risiko tersebut, tapi kadang butuh usaha ekstra untuk memasang dependency yang sebenarnya dibutuhkan aplikasi.
Mari bandingkan langsung ukuran beberapa base image populer yang sering dipakai untuk kebutuhan serupa.
docker pull ubuntu:24.04
docker pull debian:12-slim
docker pull alpine:3.20
docker pull scratch 2>/dev/null || echo "scratch bukan image yang bisa di-pull terpisah"Perintah docker pull scratch memang akan gagal karena scratch bukan image sungguhan yang tersimpan di registry, melainkan konsep image kosong yang langsung dikenali BuildKit saat dipakai di instruksi FROM. Sekarang bandingkan ukuran tiga image lain yang berhasil ditarik. Perintah docker image ls hanya menerima satu argumen REPOSITORY[:TAG], jadi cek satu per satu, atau langsung tampilkan seluruh image lokal tanpa filter karena baru saja hanya tiga image ini yang ditarik.
docker image lsAlpine Linux biasanya jauh lebih kecil dibanding Ubuntu atau Debian karena memakai musl libc dan busybox sebagai pengganti GNU libc dan coreutils standar, sehingga total ukurannya bisa di kisaran beberapa megabita saja dibanding puluhan megabita milik distro yang lebih lengkap. Perbedaan libc ini bukan sekadar soal ukuran; ada kasus tertentu, terutama untuk aplikasi yang di-compile dan di-link secara dinamis terhadap glibc, yang bisa berperilaku beda atau bahkan gagal berjalan di atas Alpine karena ketidakcocokan library C tersebut. Di lapangan, kasus seperti ini sering membuat Developer terjebak: image jadi jauh lebih kecil setelah pindah ke Alpine, tapi aplikasi tiba-tiba error yang sebelumnya tidak pernah muncul di base image berbasis Debian atau Ubuntu, sehingga pengujian menyeluruh tetap wajib dilakukan setiap kali mengganti base image, bukan hanya mengandalkan ukuran yang lebih kecil sebagai satu-satunya pertimbangan.
Selain distro umum, banyak bahasa pemrograman juga menyediakan base image resmi dengan varian ukuran berbeda, misalnya python:3.12 (lengkap), python:3.12-slim (lebih ringkas, sebagian besar tools build dihilangkan), dan python:3.12-alpine (paling minimal, berbasis Alpine). Memilih varian yang tepat sebaiknya disesuaikan kebutuhan nyata aplikasi, bukan asal memilih yang paling kecil; Sysadmin/DevOps Engineer yang harus menjaga stabilitas production sering lebih memilih varian slim berbasis Debian dibanding alpine justru demi menghindari isu kompatibilitas glibc tersebut, terutama untuk aplikasi dengan banyak dependency native.
Bersihkan image percobaan setelah selesai membandingkan.
docker rmi ubuntu:24.04 debian:12-slim alpine:3.204.4 Image Naming dan Tagging
Setiap image Docker diidentifikasi lewat kombinasi repository dan tag, ditulis dengan format [HOST[:PORT]/]PATH[:TAG]. PATH bisa terdiri dari satu atau lebih komponen yang dipisah garis miring, misalnya namespace/repository. Kalau registry host tidak disebutkan secara eksplisit, Docker CLI menganggap image tersebut berasal dari Docker Hub (docker.io). Kalau namespace juga tidak disebutkan, seperti pada nginx atau alpine, Docker CLI mengasumsikannya sebagai official image di bawah namespace library, sehingga nginx:1.27 sebenarnya setara dengan docker.io/library/nginx:1.27 secara penuh.
Tag adalah label yang menandai versi spesifik dari sebuah repository image. Nama tag harus berupa karakter ASCII, boleh berisi huruf besar maupun kecil, angka, underscore, titik, dan tanda hubung, dengan panjang maksimal 128 karakter, serta tidak boleh diawali dengan titik atau tanda hubung. Kalau tag tidak disebutkan sama sekali saat menjalankan docker pull atau docker run, Docker secara default memakai tag latest.
Perlu jadi catatan penting: latest hanyalah nama tag biasa, bukan penanda otomatis "versi paling baru" seperti yang sering disalahpahami. Kalau maintainer sebuah image tidak sengaja meng-update tag latest mengikuti rilis terbaru, tag tersebut bisa saja tertinggal jauh dari versi terkini, atau bahkan tidak pernah di-update lagi. Karena itu, memakai tag latest untuk image di production sangat tidak disarankan; Sysadmin/DevOps Engineer yang mengelola deployment production sebaiknya selalu mengunci ke tag versi spesifik (misalnya nginx:1.27.1) supaya proses deployment tetap dapat diprediksi dan tidak tiba-tiba berubah perilaku hanya karena maintainer image meng-update tag latest di registry.
Mari praktikkan pemberian tag pada image lewat docker tag. Tarik dulu image alpine dengan tag default.
docker pull alpine:3.20Buat tag baru yang mengarah ke image yang sama, seolah-olah image tersebut milik registry privat dengan namespace kita sendiri.
docker tag alpine:3.20 registry.contoh-internal.local/tim-backend/alpine:3.20docker tag tidak menduplikasi data image sama sekali; perintah ini hanya menambah satu referensi baru yang menunjuk ke image ID yang sama, mirip membuat symbolic link. Buktikan lewat docker image ls yang memfilter berdasarkan image ID yang sama.
docker image ls --filter reference="*alpine*"Kedua baris akan menampilkan IMAGE ID yang identik meski nama repository dan registry-nya berbeda, membuktikan keduanya memang merujuk data fisik yang sama di disk. Pola penamaan seperti ini yang biasa dipakai sebelum docker push ke private registry, karena Docker mewajibkan nama image mengandung alamat registry tujuan sebelum bisa di-push ke sana.
Selain tag, setiap image juga punya digest, hash SHA-256 dari manifest image yang bersifat unik dan tidak pernah berubah selama isi image tersebut sama persis. Berbeda dari tag yang bisa dipindahkan menunjuk ke image lain kapan saja oleh maintainer-nya, digest selalu mengacu ke satu isi image yang persis sama, sehingga referensi lewat digest jauh lebih aman dipakai kalau kita butuh kepastian mutlak bahwa image yang ditarik benar-benar identik dengan yang pernah diuji sebelumnya, misalnya dalam pipeline CI/CD yang menuntut reproducibility tinggi.
docker image inspect --format='{{index .RepoDigests 0}}' alpine:3.20Perintah di atas menampilkan digest lengkap image alpine:3.20 dalam format alpine@sha256:..., yang bisa langsung dipakai sebagai referensi pull yang imun terhadap perubahan tag di kemudian hari.
docker pull alpine@sha256:<digest_hasil_perintah_sebelumnya>Kalau perintah docker tag menampilkan error "invalid reference format", penyebab paling umum adalah nama repository memakai huruf besar (repository harus huruf kecil semua) atau tag diawali karakter yang tidak diizinkan seperti titik atau tanda hubung; perbaiki penulisan nama sesuai aturan format di atas sebelum mencoba lagi. Bersihkan tag percobaan setelah selesai.
docker rmi registry.contoh-internal.local/tim-backend/alpine:3.20 alpine:3.20| Istilah | Penjelasan |
|---|---|
| Repository | Kumpulan image yang berkaitan, dibedakan lewat tag (mis. nginx) |
| Tag | Label versi dalam satu repository (mis. 1.27, latest), bisa dipindah menunjuk image lain |
| Digest | Hash SHA-256 dari manifest image, unik dan permanen untuk satu isi image |
| Base/parent image | Image awal yang jadi fondasi lewat instruksi FROM |

