Docker-Halo teman-teman developers! Hari ini kita akan lanjut lagi latihan kita menggunakan Docker. Bagi teman-teman yang belum mengetahui apa itu Docker, silakan cek artikel sebelumnya mengenai Docker ya!
Pada artikel kali ini kita akan membahas mengenai bagaimana cara membuat Docker image sendiri, bagaimana cara menjalankan aplikasi kita sebagai container, dan bagaimana cara menggunakan Docker Compose untuk menjalankan banyak aplikasi sekaligus.
Chapter 3 - Building Docker Images
Seperti yang sudah kita bahas sebelumnya, untuk menjalankan suatu container, kita membutuhkan image yang merupakan blueprint dari container yang akan kita jalankan. Image ini bisa berasal dari registry yang sudah ada seperti Docker Hub atau bisa kita buat sendiri sesuai dengan kebutuhan.
Untuk membangun suatu Docker image, kita membutuhkan suatu definisi atau menifest yang disebut sebagai Dockerfile
.
Dockerfile
merupakan sebuah file teks yang berisi urutan perintah yang akan dieksekusi oleh Docker machine untuk
menjelaskan suatu image atau sebuah blueprint untuk membangun image. Dockerfile
disebut juga sebagai build
context.
Contoh Dockerfile
:
|
|
Selanjutnya, buka terminal dan jalankan perintah berikut.
|
|
Tanda .
berarti lokasi Dockerfile
terdapat pada direktori yang sama dengan working directory terminal saat ini.
Voila! Sekarang teman-teman sudah mempunyai image aplikasi Redis server yang siap berjalan di atas Docker!.
Perintah-Perintah pada Dockerfile
Nah teman-teman mungkin akan bingung dengan maksud dari Dockerfile di atas, yuk kita pelajari apa arti dari tiap-tiap perintah di atas!
FROM alpine
, baris ini menyatakan base image atau image yang akan menjadi dasar untuk membuat image baru kita. Kenapa kita perlu base image? Karena sering kali kita tidak mau membuat container dari awal (from scratch), melainkan kita ingin menggunakan basis yang sudah ada seperti Alpine Linux untuk menjalankan aplikasi kita.RUN apk add --update gcc
, baris ini memerintahkan Docker untuk menjalankan perintahapk add --update gcc
pada container. Bayangkan seperti kita menjalankan perintah ini pada terminal di komputer kita. Pada perintah ini kita akan menginstallgcc
.RUN apk add --update redis
, menginstallredis
.CMD ["redis-server"]
, perintah ini menyatakan perintah apa yang akan dieksekusi oleh Docker saat pertama kali menjalankan container. Mirip seperti aplikasi apa yang akan dieksekusi saat startup. Catatan: terdapat dua jenis perintah padaDockerfile
untuk mengeksekusi perintah saat container melakukan startup, yaituCMD
danENTRYPOINT
.
Proses Membangun Image
Nah kita sekarang sudah tau apa saja arti dari perintah-perintah di atas, tapi kita masih belum mengetahui sebenarnya seperti apa proses pembuatan image oleh Docker. Sederhananya bisa kita buat seperti ini:
- Docker akan mengeksekusi perintah-perintah pada
Dockerfile
- Setiap perintah pada
Dockerfile
akan menjadi sebuah snapshot yang akan menyimpan kondisi file system setelah mengeksekusi perintah tersebut. - Docker akan meyimpan semua snapshot tersebut sebagai layer yang nantinya akan digabung menjadi suatu container utuh.
- Docker akan menjakankan perintah sesuai pada
ENTRYPOINT
atauCMD
. - Aplikasi akan mulai dijalankan.
Secara umum, ketika kita ingin membuat sebuah image, kita akan mengikuti urutan berikut:
- Tentukan base image
- Install dependencies dan library yang dibutuhkan aplikasi
- Tentukan program atau perintah apa yang akan dijalankan saat container di mulai menggunakan perintah
ENTRYPOINT
atauCMD
Image Tagging
Oke, kita sudah tau dasar-dasar mengenai Dockerfile, sekarang saatnya kita mulai belajar mengenai tagging atau versioning image yang sudah kita buat. Versioning ini sangat penting untuk menjaga agar aplikasi yang kita buat dapat kita lacak perkembangannya dan tidak salah mendistribusikan versi aplikasi.
Setiap image sebaiknya memiliki tag sesuai dengan versinya, misalnya pada contoh di atas teman-teman sudah menggunakan tag latest. latest merupakan salah satu tag yang umum digunakan untuk menandakan versi terbaru dari aplikasi. Selain itu, teman-teman juga bisa menggunakan skema SemVer atau tag lain untuk menandakan versi image yang dibuat.
Contoh:
|
|
Membuat Image yang Efisien
Ketika membuat suatu image, ukuran akhir image akan sesuai dengan ukuran base image ditambah dengan berbagai dependencies, libraries, dan file-file yang teman-teman masukan ke dalam image tersebut. Sering kali teman-temen membuat image menggunakan base image yang besar atau menginstall packages yang tidak diperlukan oleh aplikasi saat runtime yang menyebabkan ukuran image menjadi besar.
Ada beberapa tips untuk membuat image yang efisien dari segi ukuran image dan juga lama waktu build image tersebut, beberapa diantaranya yaitu:
- Gunakan base image yang ramping, misalnya Alpine Linux.
- Hanya install packages yang diperlukan untuk runtime, misalnya yang packages yang digunakan untuk melakukan unit testing tidak diperlukan untuk production sehingga bisa di hapus dari dependensi aplikasi.
- Menggunakan teknik multi-stage build, teknik ini bertujuan untuk memcah proses pembuatan image menjadi proses yang lebih panjang tapi memiliki unit perubahan yang lebih kecil, sehingga proses pembuatan image bisa lebih cepat dengan menggunakan caching dan mengurangi jumlah layer yang tidak digunakan pada image.
Chapter 4 - Using Docker in Real Applications
Nah kita sudah mengetahui bagaimana cara membuat sebuah image, tapi masih berupa image yang berasal dari package manager Linux. Bagaimana caranya kalau kita ingin membuat image dari aplikasi kita sendiri?
Pada contoh ini kita akan menggunakan NodeJS untuk mempraktikan bagaimana cara mambuat image yang berupa aplikasi
backend sederhana yang akan memberikan output Hello World saat kita memanggil route default-nya. Kita akan
menggunakan library express
untuk membuat server.
Buka terminal, kemudian buat proyek NPM baru.
|
|
Selanjutnya buat file index.js
kemudian isi dengan kode berikut.
|
|
Setelah itu, buat file Dockerfile
seperti di bawah ini.
|
|
Setelah itu buat file .dockerignore
seperti di bawah ini.
|
|
Setelah itu, jalankan perintah berikut.
|
|
Sekarang teman-teman bisa membuka browser kemudian membuka alamat http://localhost:9000 untuk melihat server yang kita buat menggunakan NodeJS dapat dibuka seperti aplikasi pada umumnya. Sebelum kita lanjut dengan pembahasan lebih dalam, yuk kita pahami dulu bagaimana aplikasi kita bisa dijalankan menggunakan Docker.
Proses ini diawali dengan membuat proyek NPM baru menggunakan perintah npm init
, kemudian kita membuat sebuah server
sederhana menggunakan express
. Pada titik ini kita sudah bida menjalankan server kita menggunakan perintah npm start
atau node index.js
, tetapi kita akan lakukan tahap selanjutnya yaitu membuat Dockerfile
untuk membuat image dari
aplikasi yang akan kita jalankan.
Dockerfile
yang kita buat berisi urutan perintah sebagai berikut:
- Gunakan base image NodeJS pada Linux Alpine.
- Ubah working directory menjadi
/usr/app
. - Salin file
package.json
ke working directory. - Jalankan perintah
npm install
untuk menginstall library yang dibutuhkan oleh aplikasi kita sesuai denganpackage.json
. - Salin sisa file yang ada di dalam folder relatif terhadap working directory teman-teman menjalankan perintah
docker build
ke folder relatif terhadap working directory pada image (kecuali file/folder yang terdapat pada file.dockerignore
). - Jalankan perintah
node index.js
saat container dijalankan.
Teman-teman mungkin bertanya:
Q: Kenapa kita memisahkan proses menyalin file
package.json
dan source code aplikasi kita? Akan lebih cepat kalau jadi satu proses kan?Betul perintahnya akan menjadi lebih sedikit, tapi ingat Docker akan menggunakan cache saat membuat image. Source code aplikasi yang kita buat akan lebih sering berubah daripada isi file
package.json
yang berisi definisi library dan versi aplikasi. Dengan memisahkan dua proses ini, kita bisa memanfaatkan cache pada Docker untuk mempercepat proses build image.Q: Kenapa menggunakan perintah
node index.js
, kenapa tidak pakainpm start
?Hal ini disebabkan karena
npm
akan mencegah program yang kita buat dari menerima OS signal, misalnya SIGTERM dan SIGKILL. Artinya kita tidak akan bisa mengimplementasikan tenkik graceful shutdown atau bisa menyebabkan aplikasi kita menggantung hingga dipaksa berhenti oleh Docker. Kita akan pelajari mengenai graceful shutdown di artikel yang lain!
Wah tidak terasa teman-teman sekarang sudah bisa membuat image dari aplikasi sendiri menggunakan NodeJS. Proses ini sebagian besar sama apabila teman-teman ingin membuat image dari aplikasi teman-teman, pastikan base image, packages, dan aplikasi teman-teman support untuk menggunakan Docker ya!
Advanced Multi Stage Build
Nah selain cara-cara di atas, ada cara lain lho untuk membuat image yang lebih efisien lagi! Berikut adalah contoh menggunakan multi stage build yang lebih kompleks.
Docker file ini penulis gunakan di salah satu proyek Kodesiana yang sedang dalam tahap pengembangan๐ yaitu KF-EdgeML.
|
|
Wah ko panjang sekali ya Dockerfile
ini? Yuk kita bahas satu persatu!
- Kita menggunakan dua base image, yaitu
node:14-alpine
dannode-14:slim
. Image Alpine akan kita gunakan untuk menginstall dan compile TypeScript menjadi JavaScript. Dua image ini dipakai karena versi Alpine memiliki lebih banyak package bawaan sepertinpm
yang dibutuhkan untuk menjalankan script untuk compile source code aplikasi TypeScript. - Mengupdate NPM ke versi 7.22.0, kita tentukan versi spesifik agar Docker bisa membuat layer ini menjadi cache sehingga tidak pelru mengupdate NPM terus menerus setiap kali build image.
- Menyalin
package.json
danpackage-lock.json
kemudian menginstall semua library yang diperlukan. Di sini kita juga menyalin filepackage-lock.json
agar versi library yang kita install sama persis dengan yang kita gunakan pada local machine. - Salin source code aplikasi dan melakukan build
npm run build
. - Menghapus development dependency seperti
jest
,babel
, dan library lain yang tidak akan digunakan saat production. Hal ini penting untuk mengurangi file sampah yang tidak akan digunakan nantinya (npm ci --production
). - Membuat image baru menggunakan base image
node:14-slim
yang akan digunakan sebagai image akhir untuk menjalankan aplikasi. Versislim
ini memiliki ukuran yang lebih kecil dibandingkanalpine
tetapi tidak memilikipackage
tambahan seperti Alpine. - Menyalin file-file dan
node_modules
yang sudah dibersihkan dari tahap build sebelumnya masuk ke tahap runtime. Sampai di sini kita sudah memiliki semua file dan library yang dibutuhkan oleh aplikasi kita. - Terakhir adalah menjalankan perintah
node bin/serve.js
untuk menjalankan server saat container dijalankan.
Trik di atas penulis gunakan untuk menghasilkan image yang benar-benar hanya berisi aplikasi dan tidak membawa library dan file-file yang tidak krusial untuk aplikasi. Dengan menggunakan tenkik di atas image yang penulis buat bisa berkurang dari 632,13 MB (Alpine) menjadi 174,1 MB! (Alpine + slim)
Tentu teman-teman harus mempertimbangkan terlebih dahulu apakah base image yang akan digunakan oleh teman-teman memang bisa digunakan untuk build dan runtime, karena tidak semua proyek bisa menggunakan teknik ini! Be smart!
Chapter 5 - Docker Compose
Well done temen-temen! Sekarang kita sudah ada di akhir pembahasan mengenai Docker di Chapter 5 - Docker Compose. Apa sih Docker Compose itu dan kenapa kita perlu Docker Compose?
Saat kita membuat sebuah aplikasi, biasanya kita butuh aplikasi pendukung lain agar aplikasi kita bisa berjalan, yaitu basis data. Pada contoh ini kita akan membuat aplikasi yang akan menghitung berapa kali kita membuka halaman aplikasi kita. Basis data yang akan kita gunakan adalah Redis, teman-teman bisa menggunakan basis data lain jika mau.
Buka kembali file index.js
yang sebelumnya teman-teman buat, kemudian ganti dengan kode berikut. Jangan lupa untuk
install package redis
ya!
|
|
Nah pada kode di atas kita mencoba untuk mengkoneksikan ke Redis pada localhost
port 6379. Kita akan lihat skenario
saat kita menjalankan dua container, satu untuk aplikasi kita dan satu lagi untuk Redis.
Jalankan kode berikut:
- Terminal 1:
docker run redis
- Terminal 2:
docker build -t helloredis:latest .
dandocker run -t helloredis:latest
Setelah kedua container berjalan, buka http://localhost:4001. Pada tahap ini teman-teman akan mendapatkan pesan error pada konsol terminal 2 yang akan menampilkan pesan koneksi yang ditolak ke server Redis. Kenapa?
Saat menjalankan dua container berbeda, maka jaringan virtual Docker akan mengisolasi kedua container tersebut sehingga mereka tidak bisa saling berkomunikasi. Salah satu cara untuk membuka koneksi antara dua container melalui jaringan adalah dengan menggunakan Docker network yang bisa kita gunakan melalui Docker Compose.
Pada folder yang sama, buat file baru docker-compose.yml
dan isikan dengan kode berikut.
|
|
Kemudian ubah kode pada file index.js
sebagai berikut.
|
|
Setelah itu, jalankan perintah docker-compose up
. Buka http://localhost:4001 dan sekarang
teman-teman harusnya bisa melihat counter yang dimulai dari angka nol. Setiap kali teman-teman me-refresh halaman
tersebut, counter akan terus naik.
Hmm, ko bisa?
File docker-compose.yml
mendefinisikan services atau layanan atau container apa saja yang akan kita jalankan dalam
satu grup. Pada contoh ini kita akan menjalankan service Redis dan aplikasi kita. Pada service Redis, kita cukup
melampirkan nama image, sedangkan untuk aplikasi yang akan kita buat, kita tidak perlu menggunakan nama image, kita
bisa menggunakan Dockerfile
yang kita punya sebagai pengganti image.
Pada bagian host
Redis, kita set menjadi redis-server
. Nilai ini sama dengan nilai yang kita buat pada
docker-compose.yml
. Pada Docker Compose, untuk mengakses services lain pada satu network, kita bisa menggunakan
nama service tersebut untuk menggantikan alamat IP dari service tersebut.
Nah, sampai di sini teman-teman sudah berhasil menggunakan Docker Compose untuk menjalankan aplikasi server dan basis
data Redis. Tapi kode di atas belum dapat kita gunakan untuk production karena kita masih menggunakan host
yang di
hardcode, bagaimana jika kita ingin mengganti host yang berbeda pada kode kita?
Environment Variable
Salah satu cara untuk menggunakan data dinamis pada aplikasi menggunakan container adalah melalui environment variable. Teman-teman pasti sudah familiar dengan konsep ini jadi kita akan langsung lihat contoh penggunaan environment variable pada Docker Compose dan NodeJS.
docker-compose.yml
|
|
index.js
|
|
Sekarang saat kita menjalankan Docker Compose, maka properti host
dan port
yang digunakan pada aplikasi kita akan
sesuai dengan environment variable yang kita set pada docker-compose.yml
, sehingga kita tidak perlu meng-hardcode
host
dan port
ke dalam source code.
Restart Policy dan Graceful Shutdown
Restart policy mengatur apa yang harus dilakukan ketika container pada Docker Compose berhenti, sedangkan graceful shutdown merupakan teknik untuk “mematikan” aplikasi secara sewajarnya, artinya sebelum aplikasi berhenti, aplikasi perlu melakukan proses pembersihan seperti menutup koneksi ke basis data, menyimpan semua perubahan dari user, menghentikan server, dan lain-lain.
Terdapat empat pilihan restart policy pada Docker Compose, yaitu:
no
, artinya container yang mengalami crash atau dimatikan manual (melaluidocker stop
) tidak akan secara otomatis dijalankan kembali.always
, artinya container yang mengalami crash atau dimatikan manual akan selalu dijalankan kembali.on-failure
, artinya container akan dijalankan kembali apabila exit code dari proses terakhir dalam container bukan nol yang menandakan suatu error. Jika container dimatikan secara manual, maka container tersebut tidak akan dijalankan kembali secara otomatis.unless-stopped
, artinya container akan dijalankan kembali apabila dimatikan secara manual, tetapi tidak akan dijalankan kembali apabila aplikasi memberikan exit kode bukan nol (tidak akan restart aplikasi meskipun crash karena error).
Referensi
- nodepractices. 2021. Bootstrap container using node command instead of npm. Diakses 19 September 2021.