Ritual kebersihan pada chapter penutup Section Docker Image meninggalkan penyimpanan lokal kita dalam kondisi terkendali: docker system df mengukur ruang yang dipakai image, container, dan build cache, lalu perintah prune memangkas semuanya yang menganggur. Namun ada ketimpangan yang mulai terasa: sepanjang dua section terakhir kita berperan sebagai konsumen image — menarik, mengevaluasi, menandai, bahkan membersihkan image buatan orang lain — tanpa pernah sekali pun membuat image sendiri. Padahal kekuatan terbesar Docker justru ada pada kemampuan mengemas aplikasi kita sendiri menjadi image yang bisa dijalankan di mesin mana pun. Chapter ini memindahkan kursi kita dari konsumen ke produsen image, dan kendaraannya adalah satu berkas teks kecil bernama Dockerfile.
Dari Konsumen Image Menjadi Produsen #
Budi memiliki aplikasi Node.js kecil bernama myapp: sebuah server HTTP sederhana yang menyapa setiap pengunjung dengan respons JSON. Selama ini Budi menjalankan myapp di laptopnya dengan perintah node server.js, dan aplikasi itu berjalan baik karena laptop Budi sudah terpasang Node.js. Masalahnya muncul begitu myapp harus dijalankan di tempat lain — laptop Siti, server uji kantor, atau mesin CI — karena setiap mesin harus dipasangi Node.js versi yang sama lebih dulu. Inilah titik di mana image publik yang selama ini kita pakai tidak lagi cukup: node:22-alpine hanya memuat runtime Node.js tanpa satu baris kode Budi pun, sementara nginx memuat web server lengkap yang tidak tahu menahu tentang myapp. Tidak ada image publik di Docker Hub yang berisi kode milik Budi, dan tidak akan pernah ada.
Satu-satunya jalan adalah membangun image sendiri yang memuat kebutuhan lengkap myapp:
- Runtime — Node.js 22 yang konsisten di mesin mana pun.
- Berkas aplikasi —
server.jsdanpackage.jsonmilik Budi. - Perintah start — cara container menyalakan aplikasi.
- Deklarasi port — dokumentasi port yang didengarkan aplikasi.
Jawaban Docker untuk keempat kebutuhan itu adalah Dockerfile: berkas teks bernama Dockerfile yang berisi daftar instruksi build, ditulis dari atas ke bawah, lalu dieksekusi oleh builder untuk merakit sebuah image. Setiap instruksi menyumbang satu layer berisi berkas atau satu catatan metadata — persis struktur tumpukan yang sudah kita bedah pada Chapter Konsep Image Layer — dan perintah yang membaca resep tersebut adalah docker build, digerakkan BuildKit sebagai engine build bawaan sejak Docker Engine 23.0.
Menyiapkan Folder Aplikasi myapp #
Budi memulai dengan membuat folder khusus untuk proyeknya, lalu mengisinya dengan dua berkas aplikasi dan satu Dockerfile. Struktur foldernya terlihat seperti ini:
$ tree myappOutput yang Dihasilkan:
myapp
├── Dockerfile
├── package.json
└── server.jsStruktur ini sengaja dibuat minimal supaya kita bisa fokus pada Dockerfile-nya; proyek nyata tentu punya lebih banyak berkas, dan justru di situ nanti muncul pertanyaan menarik tentang berkas mana yang layak masuk image. Bila perintah tree belum tersedia di mesin latihan, padanan ls -R myapp menampilkan struktur yang sama. Mari kita isi kedua berkas aplikasinya terlebih dahulu.
Menulis Berkas server.js #
Berkas server.js adalah seluruh logika aplikasi myapp, dan isinya cukup ringkas:
const http = require("http");
const server = http.createServer((req, res) => {
if (req.url === "/") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ app: "myapp", pesan: "Halo dari container" }));
return;
}
res.writeHead(404, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Rute tidak ditemukan" }));
});
server.listen(8000, () => {
console.log("myapp mendengarkan permintaan di port 8000");
});Mari kita telaah isi server.js di atas:
require("http")— memuat modulhttpbawaan Node.js; myapp tidak memakai satu pun paket eksternal, sehingga tidak ada daftardependenciesyang harus dipasang.http.createServer— membuat server HTTP dengan fungsi callback yang dipanggil untuk setiap permintaan masuk.- Rute
/— permintaan ke path/dijawab dengan status 200 dan JSON berisi nama aplikasi serta sapaan; path lain dijawab 404 dengan JSON pesan kesalahan. server.listen(8000)— server mulai mendengarkan port 8000 dan mencetak satu baris log sebagai penanda bahwa aplikasi siap menerima permintaan.
Menulis Berkas package.json #
Berkas package.json mendeskripsikan aplikasi bagi ekosistem Node.js:
{
"name": "myapp",
"version": "1.0.0",
"private": true,
"main": "server.js",
"scripts": {
"start": "node server.js"
}
}Mari kita telaah berkas package.json di atas:
namedanversion— identitas aplikasi; versi 1.0.0 akan kita pakai juga sebagai tag image sebentar lagi.private: true— menandai myapp sebagai proyek internal yang tidak boleh dipublikasikan ke registry npm.scripts.start— menyediakan pintasannpm startyang menjalankannode server.js; Dockerfile kita akan memakai bentuk perintah langsung agar jalur prosesnya lebih bersih.
Karena myapp tidak punya dependensi eksternal, Dockerfile kita tidak perlu memasang apa pun — cukup menyalin dua berkas ini. Aplikasi nyata yang memakai framework seperti Express tentu membawa node_modules, dan urutan penyalinan berkasnya menjadi urusan yang cukup penting untuk dibedah tersendiri pada Chapter COPY dan ADD.
Dockerfile Pertama Budi #
Sekarang saatnya berkas yang menjadi inti section ini. Dockerfile pertama Budi hanya berisi lima instruksi — sengaja paling sederhana yang bisa dipakai, tanpa satu pun teknik optimasi; urusan mengoptimalkan build adalah wilayah Section Image Build dan Optimisasi:
FROM node:22-alpine
WORKDIR /app
COPY package.json server.js ./
EXPOSE 8000
CMD ["node", "server.js"]Mari kita telaah kelima instruksi di atas:
FROM node:22-alpine— menentukan image dasar tempat myapp dibangun; seluruh layernode:22-alpine(sekitar 130MB) menjadi fondasi tumpukan, dan Node.js 22 ikut tersedia di dalam image final. Varianalpinedipilih karena jauh lebih ramping daripada basis Debian.WORKDIR /app— menetapkan/appsebagai direktori kerja untuk semua instruksi setelahnya; direktori ini dibuat otomatis bila belum ada, dan seluruh path relatif di Dockerfile mengacu ke sana.COPY package.json server.js ./— menyalin dua berkas dari build context ke direktori kerja/app;./berarti “direktori kerja saat ini”, jadi keduanya mendarat di/app/package.jsondan/app/server.js.EXPOSE 8000— mendokumentasikan bahwa aplikasi mendengarkan port 8000; instruksi ini murni catatan metadata, ia tidak menerbitkan port apa pun ke host — penerbitan tetap tugas flag-psaatdocker run, dan kita bedah tuntas hubungan keduanya pada Chapter EXPOSE dan Port.CMD ["node", "server.js"]— perintah bawaan yang dijalankan container saat start; bentuk kurung siku ini disebut exec form, dan alasan kuat mengapa ia lebih baik dari bentuk shell akan kita bedah pada Chapter CMD dan ENTRYPOINT.
Membangun Image dengan docker build #
Dengan ketiga berkas siap, Budi masuk ke dalam folder myapp lalu menjalankan build pertamanya:
$ cd myapp
$ docker build -t myapp:1.0 .Mari kita telaah argumen perintah di atas:
-t myapp:1.0— nama dan tag untuk image hasil build; polanya sama seperti image yang kita tarik dari Docker Hub, yaiturepositori:tag..(satu titik) — path build context, yaitu folder yang isinya dikirim ke builder sebagai bahan mentah build; titik berarti “folder saat ini”, sehingga seluruh isi foldermyappikut tersedia untuk instruksiCOPY. Mekanisme context ini punya konsekuensi menarik yang kita bedah mendalam pada Chapter docker build dan Build Context di Section Image Build dan Optimisasi.
Build Pertama #
Output yang Dihasilkan:
[+] Building 9.2s (11/11) FINISHED
=> [internal] load build definition from Dockerfile 0.1s
=> => transferring dockerfile: 102B 0.0s
=> [internal] load metadata for docker.io/library/node:22-alpine 2.1s
=> [1/4] FROM docker.io/library/node:22-alpine 4.3s
=> => resolve docker.io/library/node:22-alpine 2.1s
=> [2/4] WORKDIR /app 0.0s
=> [3/4] COPY package.json server.js ./ 0.1s
=> => transferring context: 722B 0.0s
=> [4/4] EXPOSE 8000 0.0s
=> exporting to image 0.3s
=> => exporting layers 0.2s
=> => writing image sha256:05303e530e6f71e59072f3b3a7693c74dcc0d22fea8b9403c056c86b3f83bdec 0.1s
=> => naming to docker.io/library/myapp:1.0 0.0sMari kita telaah baris-baris output BuildKit di atas:
load build definition+transferring dockerfile: 102B— BuildKit membaca berkasDockerfilesepanjang 102 byte sebagai definisi build.load metadata— BuildKit bertanya ke Docker Hub: digest mana yang saat ini ditunjuk tagnode:22-alpine; langkah ini penting karena tag bisa berpindah, jebakan yang sudah kita bahas pada Chapter Memahami Image Tag.[1/4] FROM— langkah termahal: 4,3 detik mencakup pengunduhan seluruh layernode:22-alpinesekitar 130MB yang belum tersimpan di lokal; progres unduhannya diringkas oleh BuildKit di balik satu angka durasi.[2/4] WORKDIR— eksekusi nyaris instan karena hanya membuat direktori dan mencatat metadata.[3/4] COPY+transferring context: 722B— build context sebesar 722 byte (gabunganDockerfile,package.json, danserver.js) dikirim ke builder, lalu dua berkas aplikasi disalin menjadi satu layer baru seukuran 620 byte di atas layer basis.[4/4] EXPOSE— mencatat metadata port, tanpa isi layer.exporting to image+writing image sha256:0530...— seluruh layer dirakit menjadi image final dengan IDsha256:05303e53...; perhatikan bahwaCMDtidak tampil sebagai langkah bernomor karena perintah itu dicatat pada konfigurasi image saat ekspor.naming to docker.io/library/myapp:1.0— tagmyapp:1.0ditempelkan pada image yang barusan lahir.
Build Kedua dan Build Cache #
Budi penasaran apa yang terjadi bila build dijalankan ulang tanpa perubahan apa pun. Kali ini kita menambahkan flag --progress=plain supaya setiap langkah terbaca baris per baris:
$ docker build --progress=plain -t myapp:1.0 .Output yang Dihasilkan:
#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 102B done
#1 DONE 0.1s
#2 [internal] load metadata for docker.io/library/node:22-alpine
#2 DONE 0.8s
#3 [1/4] FROM docker.io/library/node:22-alpine
#3 resolve docker.io/library/node:22-alpine 0.8s done
#3 CACHED
#4 [2/4] WORKDIR /app
#4 CACHED
#5 [3/4] COPY package.json server.js ./
#5 CACHED
#6 [4/4] EXPOSE 8000
#6 CACHED
#7 exporting to image
#7 exporting layers done
#7 writing image sha256:05303e530e6f71e59072f3b3a7693c74dcc0d22fea8b9403c056c86b3f83bdec done
#7 naming to docker.io/library/myapp:1.0 doneMari kita telaah perbedaan penting pada output kedua ini:
- Baris
DONEdanCACHED— setiap langkah yang selesai ditandaiDONE, dan langkah yang isinya sama persis dengan build sebelumnya dilayani dari build cache dengan tandaCACHED, tanpa dieksekusi ulang. - Durasi total yang menyusut drastis — build pertama memakan 9,2 detik karena didominasi unduhan basis, sedangkan build kedua selesai di bawah satu detik.
- sha256 yang identik — image yang ditulis bernilai
sha256:05303e53...persis seperti build pertama; input yang identik menghasilkan image yang identik, wujud nyata sifat content-addressable dari Chapter Konsep Image Layer. - Konsekuensi praktis — build ulang menjadi murah, sehingga siklus sunting kode → build → uji terasa cepat sepanjang pengembangan.
Image Buatan Sendiri di Penyimpanan Lokal #
Image baru kini tinggal di penyimpanan lokal bersama image lain yang kita kenal:
$ docker images myappOutput yang Dihasilkan:
REPOSITORY TAG IMAGE ID CREATED SIZE
myapp 1.0 05303e530e6f About a minute ago 130MBMari kita telaah baris di atas:
IMAGE ID 05303e530e6f— 12 karakter pertama darisha256:05303e53...yang tampil pada output build; inilah bukti bahwa output BuildKit dan tabeldocker imagesmenunjuk image yang sama.SIZE 130MB— ukuran yang tampak sama dengannode:22-alpinekarena layer aplikasi hanya menambah 620 byte, terlalu kecil untuk menggeser pembulatan ke atas; kolom SIZE memang menjumlahkan seluruh layer tanpa membedakan milik siapa, kebiasaan yang sudah kita bedah pada Chapter Melihat Image Lokal.CREATED About a minute ago— waktu image dibangun, bukan waktu basisnya dibuat di Docker Hub.
Menjalankan dan Menguji Container #
Saatnya momen yang dinanti Budi: menjalankan image buatan sendiri sebagai container:
$ docker run -d --name myapp -p 8000:8000 myapp:1.0Output yang Dihasilkan:
2776f838d0568a72540f38a764133003dba9d712c98f0f5150b1b80643bff83cMari kita telaah perintah di atas:
-ddan--name myapp— container berjalan di belakang layar dengan namamyapp, mengikuti kebiasaan penamaan eksplisit yang kita pakai sejak Section Dasar Docker CLI.-p 8000:8000— menerbitkan port 8000 milik container ke port 8000 di host; inilah yang benar-benar membuka akses, sementaraEXPOSEdi Dockerfile hanya dokumentasi.- 64 karakter hex pada output — ID container yang baru dibuat; nilainya berbeda dari ID image karena container dan image adalah objek yang berbeda, meski saling terkait.
Budi lalu menguji aplikasinya dari host dengan curl:
$ curl localhost:8000Output yang Dihasilkan:
{"app":"myapp","pesan":"Halo dari container"}Mari kita telaah hasil di atas:
- JSON sapaan myapp — aplikasi di dalam container merespons persis seperti saat dijalankan langsung di laptop Budi; runtime, berkas, dan perintah start semuanya bekerja dari dalam image.
- Kode yang berjalan adalah salinan di dalam image —
server.jsyang dikerjakan container adalah salinan yang dibekukan saat build; Budi bisa mengubahserver.jsdi foldernya sebanyak apa pun, container yang berjalan tidak akan berubah sedikit pun sampai image dibangun ulang dan container dijalankan dari image baru. Inilah siklus kerja inti pemilik image: sunting kode, build ulang, jalankan ulang.
Dengan lima instruksi dan dua perintah, Budi kini resmi menjadi produsen image: myapp bisa dikirim ke laptop Siti atau server uji kantor tanpa memasang Node.js di sana. Namun Dockerfile yang kita tulis masih terasa seperti teka-teki singkat — baru lima dari tujuh belas instruksi yang dikenal Dockerfile, belum ada komentar, belum ada aturan penulisan yang rapi. Sebelum melanjutkan ke instruksi-instruksi berikutnya, kita perlu peta lengkap anatomi Dockerfile: apa saja instruksinya, mana yang menghasilkan layer, dan bagaimana menyusunnya agar tetap terbaca — semuanya materi Chapter Struktur Dockerfile.
Referensi #
- Dockerfile Reference — sintaks resmi setiap instruksi Dockerfile beserta aturan penulisannya.
- docker image build — CLI Reference
— flag lengkap perintah build, termasuk
-tdan--progress. - Docker Build — ikhtisar proses build dan peran BuildKit sebagai engine modern.