Docker adalah teknologi yang sangat populer dalam dunia pengembangan perangkat lunak modern. Hampir semua perusahaan teknologi saat ini, baik startup, software house, hingga enterprise, menggunakan Docker untuk membangun dan menjalankan aplikasi secara konsisten.
Di Indonesia, kebutuhan akan skill Docker terus meningkat seiring berkembangnya cloud computing, microservices, dan praktik DevOps. Banyak lowongan kerja untuk Backend Engineer, DevOps Engineer, dan Cloud Engineer yang menjadikan Docker sebagai skill wajib.
Artikel ini merupakan panduan lengkap Docker dari nol hingga penerapannya dalam CI/CD. Cocok untuk pemula yang ingin belajar Docker maupun developer yang ingin memperdalam DevOps workflow.
Apa Itu Docker?
Docker adalah platform open-source yang dirancang untuk membantu developer membangun, mengemas, dan menjalankan aplikasi di dalam lingkungan yang terisolasi bernama container. Dengan Docker, sebuah aplikasi beserta seluruh dependensinya — mulai dari library, runtime, hingga konfigurasi sistem — dapat dikemas menjadi satu unit yang portabel dan konsisten, sehingga bisa berjalan di mana saja: laptop lokal, server staging, hingga cloud production, tanpa khawatir soal perbedaan environment.
Pengertian Docker Secara Sederhana
Bayangkan Anda memesan sebuah burger di restoran. Terlepas dari kota mana Anda berada, burger yang sama akan disajikan dengan rasa, bahan, dan tampilan yang identik — karena semua bahan dikemas dalam standar yang sama. Docker bekerja dengan prinsip serupa: aplikasi Anda "dikemas" bersama semua yang dibutuhkannya, sehingga hasilnya selalu konsisten di environment mana pun.
Secara teknis, Docker memanfaatkan fitur kernel Linux seperti namespaces dan cgroups untuk menciptakan isolasi proses yang ringan. Berbeda dengan Virtual Machine yang menjalankan sistem operasi penuh, container Docker berbagi kernel dengan host-nya — menjadikannya jauh lebih cepat untuk dijalankan dan lebih hemat sumber daya.
Sejarah dan Latar Belakang Docker
Docker pertama kali diperkenalkan ke publik oleh Solomon Hykes dalam konferensi PyCon pada Maret 2013. Awalnya Docker merupakan proyek internal di perusahaan PaaS bernama dotCloud, sebelum akhirnya dirilis sebagai proyek open-source dan langsung mendapat sambutan luar biasa dari komunitas developer global.
Sejak saat itu, Docker berkembang pesat dan menjadi fondasi dari ekosistem container modern. Pada tahun 2015, Docker bersama perusahaan-perusahaan teknologi besar seperti Google, Microsoft, dan Red Hat mendirikan Open Container Initiative (OCI) untuk menstandardisasi format container, memastikan interoperabilitas lintas platform.
Perbedaan Docker dengan Virtual Machine (VM)
Ini adalah pertanyaan paling umum bagi pemula. Meskipun keduanya menyediakan isolasi lingkungan, Docker dan Virtual Machine bekerja dengan cara yang sangat berbeda dan memiliki trade-off masing-masing.
- Virtual Machine menjalankan Guest OS secara penuh di atas Hypervisor, sehingga membutuhkan memori dan storage yang besar (biasanya beberapa GB per VM).
- Docker Container berbagi kernel OS dengan host, sehingga jauh lebih ringan (hanya beberapa MB), dan dapat dijalankan dalam hitungan detik.
- VM menawarkan isolasi yang lebih kuat karena setiap mesin memiliki OS sendiri, sedangkan container mengandalkan isolasi di level proses.
- Docker unggul dalam kecepatan deployment dan efisiensi resource, menjadikannya pilihan utama untuk microservices dan CI/CD modern.
Mengapa Docker Sangat Populer di Dunia DevOps?
Popularitas Docker bukan sekadar tren — melainkan jawaban nyata atas frustrasi bertahun-tahun yang dialami para developer. Kalimat klasik "it works on my machine" kini menjadi lelucon lama, karena Docker memastikan bahwa apa yang berjalan di laptop developer akan berjalan persis sama di server production.
Berikut adalah alasan utama mengapa Docker menjadi alat wajib dalam ekosistem DevOps modern:
- Konsistensi Environment: Eliminasi perbedaan konfigurasi antara development, staging, dan production secara menyeluruh.
- Deployment Lebih Cepat: Container dapat di-build dan di-deploy dalam hitungan detik, mempercepat siklus release secara signifikan.
- Skalabilitas Mudah: Menjalankan puluhan hingga ratusan container secara paralel menjadi hal yang mudah dilakukan.
- Ekosistem yang Kaya: Docker Hub menyediakan ribuan image siap pakai — dari database, web server, hingga tools developer.
- Integrasi CI/CD Seamless: Docker menjadi komponen kunci dalam pipeline otomatis GitHub Actions, GitLab CI, maupun Jenkins.
Singkatnya, Docker bukan hanya alat — ia adalah cara berpikir baru dalam membangun dan mendistribusikan software. Memahami Docker berarti membuka pintu menuju dunia DevOps, cloud-native development, dan arsitektur microservices yang saat ini mendominasi industri teknologi global.
Konsep Dasar dan Arsitektur Docker
Sebelum mulai menggunakan Docker secara praktis, penting untuk memahami bagaimana Docker bekerja di balik layar. Memahami arsitektur dan komponen-komponen utamanya akan membuat Anda jauh lebih percaya diri saat men-debug masalah, merancang sistem, maupun mengoptimalkan performa container di lingkungan production.
Docker Engine: Inti dari Ekosistem Docker
Docker Engine adalah jantung dari seluruh ekosistem Docker. Ia merupakan aplikasi client-server yang bertanggung jawab untuk membangun, menjalankan, dan mendistribusikan container. Docker Engine terdiri dari tiga komponen utama yang bekerja secara sinergis:
- Docker Daemon (dockerd): Proses background yang berjalan di host, bertugas mengelola semua objek Docker seperti image, container, network, dan volume.
- Docker CLI (docker): Antarmuka command-line yang digunakan developer untuk berinteraksi dengan Docker Daemon melalui perintah seperti docker run, docker build, dan docker pull.
- REST API: Antarmuka pemrograman yang memungkinkan aplikasi pihak ketiga berkomunikasi langsung dengan Docker Daemon secara terprogram.
Saat Anda mengetik perintah docker run nginx di terminal,
Docker CLI mengirimkan instruksi tersebut ke Docker Daemon melalui REST API,
lalu Daemon-lah yang benar-benar mengeksekusi perintah tersebut di sistem.
Docker Image vs Docker Container: Apa Bedanya?
Ini adalah konsep fundamental yang wajib dipahami sebelum melangkah lebih jauh. Banyak pemula yang menggunakan kedua istilah ini secara bergantian, padahal keduanya merujuk pada hal yang berbeda.
Docker Image adalah blueprint atau template read-only yang berisi instruksi untuk membuat container. Image tersusun dari lapisan-lapisan (layers) yang di-stack secara berurutan — setiap instruksi dalam Dockerfile menghasilkan satu layer baru. Image bersifat statis: ia tidak berubah dan tidak bisa dimodifikasi secara langsung saat runtime.
Docker Container adalah instansi yang sedang berjalan dari sebuah image. Analogi paling tepat: jika Image adalah cetakan kue, maka Container adalah kue yang sudah jadi. Dari satu image yang sama, Anda bisa menjalankan puluhan container secara bersamaan, dan setiap container bersifat terisolasi satu sama lain.
- Image bersifat immutable (tidak bisa diubah), sedangkan container memiliki writable layer di atasnya yang menyimpan perubahan selama runtime.
- Image disimpan di registry (seperti Docker Hub), sedangkan container hanya ada di host tempat ia dijalankan.
- Menghapus container tidak akan menghapus image-nya — image tetap tersimpan dan bisa digunakan untuk membuat container baru kapan saja.
- Satu image bisa menghasilkan banyak container yang berjalan secara paralel, menjadikannya dasar dari horizontal scaling.
Docker Registry dan Docker Hub
Docker Registry adalah sistem penyimpanan dan distribusi untuk Docker Image — anggap saja seperti "GitHub, tapi khusus untuk image Docker". Registry memungkinkan tim untuk menyimpan, berbagi, dan mendistribusikan image secara terpusat, baik secara publik maupun privat.
Docker Hub adalah registry publik resmi yang dikelola
oleh Docker Inc dan merupakan registry terbesar di dunia. Di sini Anda
akan menemukan ribuan image resmi (official images) dari
vendor-vendor besar seperti nginx, postgres, redis, node, python, dan
lainnya — semua siap digunakan hanya dengan satu perintah
docker pull.
- Docker Hub (hub.docker.com): Registry publik default, menyediakan image resmi dan komunitas yang bisa langsung digunakan.
- GitHub Container Registry (ghcr.io): Registry yang terintegrasi dengan ekosistem GitHub, cocok untuk project open-source.
- AWS ECR, Google Artifact Registry, Azure ACR: Registry privat dari provider cloud besar, ideal untuk kebutuhan production enterprise.
- Self-hosted Registry: Anda bisa menjalankan registry sendiri menggunakan image resmi 'registry' dari Docker Hub untuk kebutuhan privat on-premise.
Docker Daemon, Client, dan REST API
Arsitektur Docker menggunakan pola client-server yang memisahkan antarmuka pengguna dengan mesin eksekusi. Pemisahan ini memberikan fleksibilitas besar — misalnya, Docker CLI di laptop Anda bisa terhubung dan mengontrol Docker Daemon yang berjalan di server remote sekalipun.
Docker Daemon (dockerd)
adalah proses server yang mendengarkan permintaan dari Docker API.
Ia mengelola seluruh lifecycle objek Docker: membuat dan menghapus
container, mengunduh image dari registry, mengatur network virtual,
serta mengelola volume penyimpanan. Daemon berjalan terus-menerus
sebagai background service di sistem operasi host.
Docker Client (docker)
adalah cara utama pengguna berinteraksi dengan Docker. Setiap perintah
yang Anda ketik — seperti docker build atau
docker ps —
diterjemahkan menjadi panggilan REST API yang dikirimkan ke Daemon.
Satu Docker Client bahkan bisa dikonfigurasi untuk berkomunikasi dengan
beberapa Daemon sekaligus.
Cara Kerja Docker Secara Keseluruhan
Untuk memahami alur kerja Docker secara menyeluruh, mari ikuti perjalanan sebuah aplikasi dari kode sumber hingga menjadi container yang berjalan di server production:
- Developer menulis Dockerfile — file instruksi yang mendefinisikan bagaimana image harus dibangun, mulai dari base image, instalasi dependensi, hingga perintah startup.
- Perintah docker build dieksekusi: Docker CLI mengirim instruksi ke Daemon, yang kemudian membangun image layer demi layer sesuai isi Dockerfile.
- Image yang sudah jadi di-push ke registry (misalnya Docker Hub atau AWS ECR) menggunakan perintah docker push, sehingga bisa diakses dari server mana pun.
- Di server production, Docker Daemon menarik (pull) image dari registry, lalu menjalankannya sebagai container dengan perintah docker run beserta konfigurasi yang diperlukan.
- Container berjalan dalam isolasi penuh: memiliki filesystem, network interface, dan proses sendiri — namun tetap berbagi kernel dengan host untuk efisiensi maksimal.
Alur inilah yang menjadi dasar dari pipeline CI/CD modern: setiap kali developer melakukan push kode, sistem otomatis akan mem-build image baru, menjalankan pengujian di dalam container, lalu mendeploy image tersebut ke production — semuanya dalam hitungan menit, tanpa intervensi manual. Inilah kekuatan sesungguhnya dari Docker.
Instalasi Docker di Berbagai Platform
Salah satu keunggulan Docker adalah dukungannya yang luas terhadap berbagai sistem operasi. Baik Anda menggunakan Windows, macOS, maupun Linux, Docker menyediakan cara instalasi yang resmi dan terdokumentasi dengan baik. Pada section ini, kita akan membahas langkah-langkah instalasi di masing-masing platform secara lengkap dan akurat.
Cara Install Docker di Windows
Di Windows, Docker dijalankan melalui aplikasi bernama Docker Desktop — sebuah paket lengkap yang sudah mencakup Docker Engine, Docker CLI, Docker Compose, dan antarmuka grafis untuk memantau container. Docker Desktop memanfaatkan WSL 2 (Windows Subsystem for Linux 2) sebagai backend-nya, yang memberikan performa jauh lebih baik dibanding metode lama berbasis Hyper-V.
Sebelum memulai instalasi, pastikan sistem Anda memenuhi persyaratan berikut:
- Windows 10 64-bit versi 21H2 atau lebih baru, atau Windows 11 — baik edisi Home, Pro, maupun Enterprise.
- Virtualization harus diaktifkan di BIOS/UEFI. Anda bisa memverifikasinya melalui Task Manager → tab Performance → CPU, pastikan 'Virtualization: Enabled'.
- WSL 2 sudah terinstal. Jalankan perintah wsl --install di PowerShell sebagai Administrator jika belum.
- Minimal RAM 4 GB, namun sangat disarankan 8 GB atau lebih untuk penggunaan yang nyaman.
Langkah instalasinya: kunjungi
docs.docker.com, unduh installer
Docker Desktop Installer.exe,
jalankan installer tersebut, pastikan opsi
"Use WSL 2 instead of Hyper-V" dicentang, lalu ikuti wizard
hingga selesai dan restart komputer Anda. Setelah restart, Docker
Desktop akan berjalan otomatis di system tray.
Cara Install Docker di macOS
Di macOS, Docker juga tersedia dalam bentuk Docker Desktop. Docker Desktop for Mac tersedia dalam dua varian yang berbeda sesuai arsitektur chip yang digunakan, jadi pastikan Anda mengunduh versi yang tepat untuk menghindari masalah performa.
- Mac dengan chip Apple Silicon (M1, M2, M3, M4): Unduh Docker Desktop for Mac with Apple Silicon. Versi ini dioptimalkan untuk arsitektur ARM64.
- Mac dengan chip Intel: Unduh Docker Desktop for Mac with Intel Chip. Versi ini menggunakan arsitektur x86_64.
- Persyaratan minimum: macOS 13 (Ventura) atau lebih baru, dan minimal 4 GB RAM.
Alternatif yang populer di kalangan developer macOS adalah menggunakan Homebrew. Cukup jalankan perintah berikut di terminal:
brew install --cask docker Setelah instalasi selesai, buka aplikasi Docker dari folder Applications, izinkan akses yang diminta sistem, dan tunggu hingga ikon Docker di menu bar menunjukkan status "Docker Desktop is running".
Cara Install Docker di Linux (Ubuntu / Debian / CentOS)
Di Linux, Docker Engine dapat diinstal langsung tanpa memerlukan Docker Desktop. Ini adalah cara yang paling umum digunakan di server production maupun lingkungan development berbasis Linux. Berikut panduan untuk distro yang paling banyak digunakan.
Ubuntu & Debian — Cara yang direkomendasikan adalah menggunakan repository resmi Docker, bukan package dari repository default Ubuntu yang seringkali sudah usang. Jalankan perintah berikut secara berurutan di terminal:
# 1. Hapus versi lama jika ada
sudo apt remove docker docker-engine docker.io containerd runc
# 2. Update package index dan install dependensi
sudo apt update
sudo apt install -y ca-certificates curl gnupg
# 3. Tambahkan GPG key resmi Docker
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
# 4. Tambahkan repository Docker ke apt sources
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# 5. Install Docker Engine
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin CentOS & RHEL — Gunakan
dnf
sebagai package manager dan repository resmi Docker:
# 1. Install dnf-plugins-core
sudo dnf install -y dnf-plugins-core
# 2. Tambahkan repository Docker
sudo dnf config-manager --add-repo \
https://download.docker.com/linux/centos/docker-ce.repo
# 3. Install Docker Engine
sudo dnf install -y docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-plugin
# 4. Jalankan dan aktifkan Docker service
sudo systemctl start docker
sudo systemctl enable docker
Setelah instalasi di Linux, ada satu langkah penting yang sering
terlewat: menambahkan user Anda ke grup
docker
agar bisa menjalankan perintah Docker tanpa
sudo:
sudo usermod -aG docker $USER
newgrp docker Verifikasi Instalasi dan Menjalankan Hello World
Setelah Docker berhasil diinstal di platform mana pun, langkah pertama yang harus dilakukan adalah memverifikasi bahwa semua komponen berjalan dengan benar. Buka terminal atau command prompt, lalu jalankan perintah berikut:
# Cek versi Docker yang terinstal
docker --version
# Cek detail lengkap Docker Engine
docker info
# Verifikasi Docker Compose
docker compose version
Jika instalasi berhasil, perintah
docker --version
akan menampilkan output seperti
Docker version 27.x.x, build xxxxxxx.
Selanjutnya, jalankan container pertama Anda dengan image
hello-world yang memang dirancang khusus untuk
memverifikasi instalasi Docker:
docker run hello-world
Docker akan otomatis mengunduh image
hello-world
dari Docker Hub (karena belum ada di lokal), membuat container baru,
menjalankannya, lalu mencetak pesan konfirmasi di terminal. Jika Anda
melihat pesan "Hello from Docker!" diikuti penjelasan
singkat tentang langkah-langkah yang baru saja terjadi, selamat —
instalasi Docker Anda sudah sempurna dan siap digunakan!
- docker pull hello-world: Docker mengunduh image dari Docker Hub ke local cache di mesin Anda.
- Container baru dibuat dari image tersebut dan langsung dijalankan oleh Docker Daemon.
- Proses di dalam container mencetak pesan konfirmasi ke stdout, yang diteruskan ke terminal Anda.
- Container berhenti secara otomatis setelah tugasnya selesai — inilah contoh paling sederhana dari lifecycle sebuah container.
Dengan instalasi yang sudah terverifikasi, Anda kini siap melangkah ke tahap berikutnya: mempelajari perintah-perintah dasar Docker yang akan menjadi senjata utama Anda sehari-hari dalam mengelola image dan container.
Perintah Dasar Docker yang Wajib Diketahui
Menguasai perintah dasar Docker adalah fondasi dari semua aktivitas yang akan Anda lakukan — mulai dari development lokal hingga deployment production. Meskipun Docker memiliki puluhan perintah, dalam praktiknya Anda akan menggunakan sekitar 15–20 perintah inti secara berulang. Section ini membahas perintah-perintah tersebut secara terstruktur, dikelompokkan berdasarkan fungsinya agar lebih mudah dipahami dan diingat.
Perintah untuk Mengelola Docker Image
Image adalah titik awal dari segalanya di Docker. Berikut adalah perintah-perintah esensial untuk mengelola image di mesin lokal Anda, mulai dari mengunduh, memeriksa, hingga membersihkan image yang tidak lagi dibutuhkan.
# Mengunduh image dari registry (default: Docker Hub)
docker pull nginx
docker pull nginx:1.25-alpine # Dengan tag versi spesifik
docker pull ubuntu:22.04
# Melihat semua image yang tersimpan di lokal
docker images
docker image ls # Perintah alternatif yang sama
# Melihat detail lengkap sebuah image (layers, env, entrypoint, dll)
docker image inspect nginx
# Membangun image dari Dockerfile di direktori saat ini
docker build -t myapp:1.0 .
docker build -t myapp:latest -f Dockerfile.prod . # Dockerfile custom
# Memberi tag tambahan pada image yang sudah ada
docker tag myapp:1.0 username/myapp:1.0
# Push image ke Docker Hub (harus login terlebih dahulu)
docker login
docker push username/myapp:1.0
# Menghapus image dari lokal
docker rmi nginx
docker rmi nginx:1.25-alpine # Hapus tag spesifik
docker image prune # Hapus semua image yang tidak digunakan (dangling)
docker image prune -a # Hapus SEMUA image yang tidak ada container-nya
Perhatikan penggunaan tag pada image. Tag seperti
:latest,
:1.25-alpine,
atau :22.04
adalah penanda versi yang sangat penting. Sebaiknya hindari menggunakan
tag :latest
di environment production karena nilainya bisa berubah sewaktu-waktu
dan berpotensi menyebabkan inkonsistensi deployment.
Perintah untuk Mengelola Docker Container
Jika image adalah blueprint, maka container adalah aplikasi yang benar-benar berjalan. Perintah-perintah berikut mencakup seluruh lifecycle container — dari pembuatan, penghentian, inspeksi, hingga penghapusan.
# Menjalankan container baru dari sebuah image
docker run nginx # Foreground (blocking)
docker run -d nginx # Detached mode (background)
docker run -d -p 8080:80 nginx # Map port host:container
docker run -d -p 8080:80 --name web nginx # Beri nama container
docker run -it ubuntu:22.04 bash # Mode interaktif dengan terminal
# Melihat daftar container
docker ps # Hanya container yang sedang berjalan
docker ps -a # Semua container termasuk yang sudah berhenti
docker ps -a --format "table {{.Names}} {{.Status}} {{.Ports}}"
# Menghentikan dan memulai ulang container
docker stop web # Graceful stop (SIGTERM → SIGKILL)
docker kill web # Force stop (SIGKILL langsung)
docker start web # Menjalankan container yang sudah berhenti
docker restart web # Stop lalu start ulang
# Masuk ke dalam container yang sedang berjalan
docker exec -it web bash # Buka shell bash
docker exec -it web sh # Buka shell sh (untuk Alpine Linux)
docker exec web cat /etc/nginx/nginx.conf # Jalankan perintah tanpa masuk shell
# Menyalin file antara host dan container
docker cp ./config.conf web:/etc/nginx/ # Host → Container
docker cp web:/var/log/nginx/error.log ./ # Container → Host
# Melihat detail konfigurasi container
docker inspect web
docker inspect web --format '{{.NetworkSettings.IPAddress}}'
# Menghapus container
docker rm web # Hapus container yang sudah berhenti
docker rm -f web # Force hapus container yang masih berjalan
docker container prune # Hapus semua container yang sudah berhenti
Salah satu flag yang paling sering digunakan adalah kombinasi
-d
(detached) dan
--name.
Dengan memberikan nama yang deskriptif pada container, Anda tidak perlu
mengingat container ID yang panjang dan acak setiap kali ingin
berinteraksi dengan container tersebut.
Perintah untuk Melihat Log dan Monitoring Container
Ketika sebuah container berperilaku tidak sesuai ekspektasi, log dan data monitoring adalah sumber kebenaran pertama yang harus diperiksa. Docker menyediakan perintah bawaan yang cukup powerful untuk kebutuhan debugging sehari-hari.
# Melihat log output dari container
docker logs web # Tampilkan semua log
docker logs -f web # Follow log secara real-time (seperti tail -f)
docker logs --tail 100 web # Tampilkan 100 baris terakhir
docker logs --since 1h web # Log dari 1 jam terakhir
docker logs --since "2024-01-15T10:00:00" web # Log sejak waktu tertentu
docker logs -f --tail 50 web # Kombinasi: 50 baris terakhir + follow
# Monitoring penggunaan resource secara real-time
docker stats # Semua container yang berjalan
docker stats web # Container spesifik
docker stats --no-stream # Snapshot sekali (tidak live)
docker stats --format "table {{.Name}} {{.CPUPerc}} {{.MemUsage}}"
# Melihat proses yang berjalan di dalam container
docker top web
# Melihat perubahan filesystem pada container sejak dibuat
docker diff web
Perintah docker stats
menampilkan data real-time seperti persentase CPU, penggunaan memori,
jumlah data yang dikirim/diterima melalui network, dan aktivitas
read/write disk — semua dalam satu tampilan. Ini sangat berguna untuk
mendeteksi container yang mengalami memory leak atau
konsumsi CPU yang tidak wajar.
Perintah Jaringan dan Volume Docker
Network dan volume adalah dua komponen infrastruktur yang menghubungkan container dengan dunia luar serta memastikan data tetap tersimpan meski container dihapus. Berikut perintah-perintah yang perlu Anda kuasai untuk mengelola keduanya.
Docker Network:
# Melihat semua network yang tersedia
docker network ls
# Membuat custom network
docker network create mynetwork
docker network create --driver bridge mynetwork # Eksplisit gunakan driver bridge
# Menjalankan container di network tertentu
docker run -d --network mynetwork --name db postgres
# Menghubungkan container yang sudah berjalan ke network
docker network connect mynetwork web
# Memutuskan koneksi container dari network
docker network disconnect mynetwork web
# Melihat detail network (container yang terhubung, subnet, gateway)
docker network inspect mynetwork
# Menghapus network
docker network rm mynetwork
docker network prune # Hapus semua network yang tidak digunakan Docker Volume:
# Melihat semua volume
docker volume ls
# Membuat volume baru
docker volume create mydata
# Menggunakan volume saat menjalankan container
docker run -d -v mydata:/var/lib/postgresql/data postgres # Named volume
docker run -d -v $(pwd)/data:/var/lib/postgresql/data postgres # Bind mount
# Melihat detail volume (lokasi di host, driver, dll)
docker volume inspect mydata
# Menghapus volume
docker volume rm mydata
docker volume prune # Hapus semua volume yang tidak digunakan - Gunakan named volume (docker volume create) untuk data production seperti database — Docker mengelola lokasi penyimpanannya secara otomatis dan aman.
- Gunakan bind mount (-v $(pwd)/src:/app/src) saat development agar perubahan kode di lokal langsung terefleksi di dalam container tanpa perlu rebuild.
- Selalu hapus volume yang tidak terpakai secara berkala dengan docker volume prune untuk mencegah penumpukan storage di mesin Anda.
- Jaringan custom (docker network create) memungkinkan container saling berkomunikasi menggunakan nama container sebagai hostname, tanpa perlu mengetahui IP-nya.
Dengan memahami keempat kelompok perintah ini — image, container, log & monitoring, serta network & volume — Anda sudah memiliki bekal yang cukup untuk mengelola infrastruktur container di lingkungan development maupun production. Langkah selanjutnya adalah belajar membuat image sendiri menggunakan Dockerfile, yang akan kita bahas tuntas di section berikutnya.
Dockerfile: Membuat Image Sendiri
Sejauh ini kita hanya menggunakan image yang sudah tersedia di Docker Hub. Namun dalam praktik nyata, Anda perlu membuat image sendiri yang berisi aplikasi Anda beserta seluruh dependensinya. Di sinilah peran Dockerfile — sebuah file teks berisi serangkaian instruksi yang memberitahu Docker bagaimana cara membangun image secara otomatis, konsisten, dan dapat direproduksi kapan pun dibutuhkan.
Apa Itu Dockerfile dan Fungsinya?
Dockerfile adalah resep dari sebuah Docker Image. Sama seperti resep masakan yang mendefinisikan bahan-bahan dan langkah-langkah memasaknya, Dockerfile mendefinisikan dari mana image dimulai, software apa yang perlu diinstal, file apa yang perlu disalin, dan perintah apa yang harus dijalankan saat container pertama kali dihidupkan.
Setiap baris instruksi di dalam Dockerfile menghasilkan sebuah layer baru yang di-stack di atas layer sebelumnya. Docker menggunakan sistem caching yang cerdas: jika sebuah layer tidak berubah sejak build terakhir, Docker akan menggunakan cache-nya alih-alih membangun ulang dari awal. Inilah mengapa urutan instruksi di dalam Dockerfile sangat berpengaruh terhadap kecepatan proses build.
- Reproducibility: Image yang dibangun dari Dockerfile yang sama akan selalu menghasilkan environment yang identik, di mesin mana pun dan kapan pun.
- Version Control: Dockerfile adalah file teks biasa yang bisa di-commit ke Git, sehingga perubahan infrastruktur bisa di-review dan di-rollback seperti kode biasa.
- Automation: Dockerfile menjadi fondasi dari pipeline CI/CD — setiap push kode bisa secara otomatis memicu proses build image tanpa intervensi manual.
- Dokumentasi Hidup: Dockerfile secara implisit mendokumentasikan semua dependensi dan konfigurasi yang dibutuhkan aplikasi untuk berjalan.
Struktur dan Sintaks Dockerfile
Dockerfile memiliki sintaks yang sederhana: setiap baris terdiri dari
sebuah instruksi (ditulis dengan huruf kapital) diikuti
oleh argumennya. Baris yang diawali dengan tanda
#
adalah komentar dan akan diabaikan saat proses build. Berikut adalah
contoh Dockerfile lengkap untuk aplikasi Node.js:
# ─── Base Image ───────────────────────────────────────────────
FROM node:20-alpine
# ─── Metadata Image ───────────────────────────────────────────
LABEL maintainer="yourname@email.com"
LABEL version="1.0"
LABEL description="Aplikasi Node.js production-ready"
# ─── Set Working Directory ────────────────────────────────────
WORKDIR /app
# ─── Install Dependensi ───────────────────────────────────────
# Salin package.json DULU (manfaatkan layer caching)
COPY package*.json ./
RUN npm ci --only=production
# ─── Salin Source Code ────────────────────────────────────────
COPY . .
# ─── Build Aplikasi (jika diperlukan) ─────────────────────────
RUN npm run build
# ─── Environment Variables ────────────────────────────────────
ENV NODE_ENV=production
ENV PORT=3000
# ─── Expose Port ──────────────────────────────────────────────
EXPOSE 3000
# ─── Health Check ─────────────────────────────────────────────
HEALTHCHECK --interval=30s --timeout=10s --retries=3 CMD wget -qO- http://localhost:3000/health || exit 1
# ─── Jalankan Aplikasi ────────────────────────────────────────
CMD ["node", "dist/server.js"] Instruksi Penting: FROM, RUN, COPY, CMD, ENTRYPOINT, ENV
Meskipun Dockerfile memiliki lebih dari 15 instruksi, ada sejumlah instruksi inti yang akan Anda gunakan di hampir setiap Dockerfile. Memahami fungsi dan perbedaan masing-masing instruksi ini adalah kunci untuk menulis Dockerfile yang benar dan efisien.
FROM — Instruksi pertama dan wajib ada di setiap
Dockerfile. Mendefinisikan base image yang menjadi fondasi layer pertama.
Pilih base image sesuai kebutuhan: gunakan varian
-alpine
untuk ukuran image yang minimal, atau
-slim
sebagai kompromi antara ukuran dan kompatibilitas.
FROM node:20-alpine # Image ringan berbasis Alpine Linux (~7MB)
FROM node:20-slim # Image lebih kecil dari default, berbasis Debian
FROM node:20 # Image lengkap dengan semua tools (~1GB)
FROM scratch # Image kosong, untuk binary yang self-contained RUN — Mengeksekusi perintah shell selama proses build dan hasilnya disimpan sebagai layer baru. Sering digunakan untuk menginstal package, mengompilasi kode, atau melakukan konfigurasi sistem.
# ❌ Buruk: Setiap RUN menghasilkan layer baru yang membengkakkan ukuran image
RUN apt update
RUN apt install -y curl
RUN apt install -y git
# ✅ Baik: Gabungkan dengan && dan bersihkan cache di akhir untuk satu layer
RUN apt update && apt install -y curl git && rm -rf /var/lib/apt/lists/* COPY vs ADD — Keduanya menyalin file dari host ke
dalam image. Namun COPY
lebih direkomendasikan karena perilakunya transparan dan dapat diprediksi.
ADD
memiliki fitur tambahan seperti auto-extract arsip tar dan mendukung
URL remote, namun justru hal ini yang membuatnya sulit diprediksi.
COPY package*.json ./ # Salin file spesifik ke working directory
COPY src/ ./src/ # Salin seluruh direktori
COPY --chown=node:node . . # Salin dengan kepemilikan user tertentu
ADD https://example.com/file.tar . # ADD: bisa dari URL (tidak disarankan) CMD vs ENTRYPOINT — Ini adalah salah satu perbedaan yang paling sering membingungkan pemula. Keduanya mendefinisikan perintah yang dijalankan saat container pertama kali dihidupkan, namun dengan perilaku yang berbeda secara fundamental.
# CMD: Perintah default yang BISA di-override saat docker run
CMD ["node", "server.js"]
# docker run myapp npm test ← CMD diganti dengan "npm test"
# ENTRYPOINT: Perintah utama yang TIDAK bisa di-override (hanya bisa ditambah argumen)
ENTRYPOINT ["node"]
CMD ["server.js"] # Argumen default untuk ENTRYPOINT
# docker run myapp other.js ← Hasilnya: node other.js
# Kombinasi ENTRYPOINT + CMD adalah pola terbaik untuk executable container
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["postgres"] ENV dan ARG — Keduanya mendefinisikan variabel, namun
dengan scope yang berbeda.
ENV
tersedia baik saat build maupun runtime (di dalam container),
sedangkan ARG
hanya tersedia selama proses build dan tidak akan ada di container yang sudah jalan.
# ARG: Variabel build-time, bisa di-pass dari perintah docker build
ARG NODE_VERSION=20
FROM node:${NODE_VERSION}-alpine
# ENV: Variabel yang tersedia di dalam container saat runtime
ENV NODE_ENV=production
ENV APP_PORT=3000
ENV DB_HOST=localhost
# Override ENV saat docker run
# docker run -e NODE_ENV=development -e DB_HOST=db myapp Best Practice Menulis Dockerfile yang Efisien
Menulis Dockerfile yang sekadar berfungsi tidaklah sulit. Namun menulis Dockerfile yang efisien — menghasilkan image sekecil mungkin, build secepat mungkin, dan seaman mungkin — membutuhkan pemahaman tentang beberapa prinsip penting berikut.
- Urutkan instruksi dari yang paling jarang berubah ke yang paling sering berubah. Letakkan COPY package.json dan RUN npm install sebelum COPY source code, sehingga layer dependensi bisa di-cache dan tidak perlu diinstal ulang setiap kali ada perubahan kode.
- Gunakan file .dockerignore untuk mengecualikan file yang tidak perlu masuk ke dalam image, seperti direktori node_modules, .git, file .env, dan folder coverage. Ini mempercepat proses build dan memperkecil ukuran image.
- Jangan jalankan proses sebagai root. Buat user non-root khusus dengan RUN adduser dan gunakan instruksi USER untuk berpindah ke user tersebut sebelum CMD — ini adalah langkah keamanan fundamental.
- Gunakan tag versi yang spesifik untuk base image (FROM node:20.11-alpine) bukan :latest, agar build Anda tetap deterministik dan tidak terdampak update mendadak dari upstream.
- Satu container, satu tanggung jawab. Jangan menjalankan multiple services utama (seperti web server + database) dalam satu container — pisahkan ke container berbeda dan hubungkan via Docker network.
Build dan Push Image ke Docker Hub
Setelah Dockerfile siap, langkah selanjutnya adalah membangun image dan mendistribusikannya ke registry agar bisa digunakan oleh tim lain atau di-deploy ke server production. Berikut alur lengkapnya:
# 1. Build image dengan tag yang bermakna
docker build -t username/myapp:1.0.0 .
docker build -t username/myapp:1.0.0 -t username/myapp:latest . # Multi-tag sekaligus
# 2. Verifikasi hasil build
docker images username/myapp
docker image inspect username/myapp:1.0.0
# 3. Test image secara lokal sebelum push
docker run -d -p 3000:3000 --name test-app username/myapp:1.0.0
curl http://localhost:3000/health
docker logs test-app
docker rm -f test-app
# 4. Login ke Docker Hub
docker login
# Masukkan username dan password/access token Docker Hub Anda
# 5. Push image ke registry
docker push username/myapp:1.0.0
docker push username/myapp:latest
# 6. Verifikasi image tersedia di registry
docker pull username/myapp:1.0.0
Sebagai praktik terbaik keamanan, gunakan Access Token
alih-alih password akun Docker Hub Anda saat melakukan
docker login
— terutama di lingkungan CI/CD. Access Token bisa dibuat di halaman
Account Settings Docker Hub dan bisa di-revoke kapan saja tanpa
mengubah password utama Anda. Dengan image yang sudah tersimpan di
registry, tim Anda bisa langsung menggunakannya, dan pipeline CI/CD
bisa otomatis men-deploy versi terbaru ke server production hanya
dengan satu perintah
docker pull.
Docker Compose: Mengelola Multi-Container
Aplikasi modern jarang berdiri sendiri. Sebuah aplikasi web tipikal
setidaknya terdiri dari frontend, backend API, database, dan mungkin
juga cache server seperti Redis. Menjalankan setiap container secara
manual dengan panjangnya deretan flag
docker run
adalah pendekatan yang tidak praktis dan rawan kesalahan. Di sinilah
Docker Compose hadir sebagai solusinya — sebuah tool
yang memungkinkan Anda mendefinisikan dan menjalankan aplikasi
multi-container hanya dengan satu file konfigurasi dan satu perintah.
Apa Itu Docker Compose dan Kapan Digunakannya?
Docker Compose adalah tool orchestration tingkat dasar yang membaca
file konfigurasi bernama docker-compose.yml
(atau compose.yaml
di versi terbaru) untuk mendefinisikan seluruh stack aplikasi —
termasuk services, network, volume, environment variable, dan urutan
startup — dalam satu tempat yang terpusat.
Docker Compose paling tepat digunakan dalam skenario-skenario berikut:
- Development lokal: Menjalankan seluruh stack aplikasi (app + database + cache + message broker) di laptop developer dengan satu perintah docker compose up.
- Testing dan CI/CD: Menyiapkan environment pengujian yang terisolasi dan identik setiap kali pipeline dijalankan, lalu membersihkannya setelah selesai.
- Staging environment: Mereplikasi kondisi production di server staging tanpa kerumitan konfigurasi manual per-service.
- Prototyping: Dengan cepat mencoba kombinasi teknologi baru (misalnya Next.js + PostgreSQL + Redis) tanpa menginstal apapun langsung di sistem.
Perlu dicatat bahwa Docker Compose dirancang untuk deployment single-host. Untuk kebutuhan orkestrasi di multiple server secara bersamaan, Anda perlu menggunakan Docker Swarm atau Kubernetes yang akan dibahas di section berikutnya.
Instalasi Docker Compose
Kabar baiknya: jika Anda menginstal Docker Desktop di Windows atau macOS, Docker Compose sudah otomatis tersedia sebagai bagian dari paket instalasi. Anda tidak perlu melakukan langkah tambahan apapun.
Di Linux, Docker Compose versi terbaru sudah disertakan sebagai plugin resmi Docker Engine (bukan binary terpisah seperti versi lama). Jika Anda mengikuti panduan instalasi di section 3, plugin ini sudah terinstal. Verifikasi dengan perintah:
# Verifikasi Docker Compose tersedia (sebagai plugin)
docker compose version
# Output: Docker Compose version v2.x.x
# Catatan perbedaan sintaks penting:
# docker compose (v2, plugin) ← Gunakan ini, ini yang terbaru
# docker-compose (v1, binary) ← Legacy, sudah deprecated
Perhatikan perbedaan antara
docker compose
(spasi, v2) dan
docker-compose
(tanda hubung, v1). Versi v1 sudah resmi deprecated sejak Juli 2023
dan tidak lagi mendapat update keamanan. Pastikan Anda selalu
menggunakan versi v2 untuk project baru.
Struktur File docker-compose.yml
File docker-compose.yml
menggunakan format YAML yang mudah dibaca manusia.
File ini tersusun dari beberapa blok utama: services
(container yang akan dijalankan), networks
(jaringan virtual antar container), dan volumes
(penyimpanan persisten). Berikut anatomi lengkap dari sebuah file
Compose dengan anotasi penjelasan:
# docker-compose.yml
services:
# ── Service 1: Aplikasi Backend ──────────────────────────────
api:
build:
context: . # Lokasi Dockerfile
dockerfile: Dockerfile # Nama Dockerfile (opsional)
args:
NODE_VERSION: "20" # Build arguments
image: myapp-api:latest # Nama image hasil build
container_name: myapp_api # Nama container (opsional)
restart: unless-stopped # Policy restart otomatis
ports:
- "3000:3000" # host_port:container_port
environment:
NODE_ENV: production
DB_HOST: db # Gunakan nama service sebagai hostname
DB_PORT: 5432
DB_NAME: myappdb
DB_USER: postgres
DB_PASS: ${DB_PASSWORD} # Ambil dari file .env
env_file:
- .env # Load semua variabel dari file .env
volumes:
- ./logs:/app/logs # Bind mount untuk logs
depends_on:
db:
condition: service_healthy # Tunggu sampai db benar-benar siap
redis:
condition: service_started
networks:
- app-network
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
# ── Service 2: Database PostgreSQL ───────────────────────────
db:
image: postgres:16-alpine
container_name: myapp_db
restart: unless-stopped
environment:
POSTGRES_DB: myappdb
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data # Named volume untuk persistensi
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql # Init script
ports:
- "5432:5432" # Expose hanya saat development
networks:
- app-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d myappdb"]
interval: 10s
timeout: 5s
retries: 5
# ── Service 3: Redis Cache ────────────────────────────────────
redis:
image: redis:7-alpine
container_name: myapp_redis
restart: unless-stopped
command: redis-server --requirepass ${REDIS_PASSWORD} --appendonly yes
volumes:
- redis_data:/data
ports:
- "6379:6379"
networks:
- app-network
# ── Service 4: Nginx Reverse Proxy ───────────────────────────
nginx:
image: nginx:1.25-alpine
container_name: myapp_nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro # :ro = read-only
- ./nginx/ssl:/etc/nginx/ssl:ro
depends_on:
- api
networks:
- app-network
# ── Networks ──────────────────────────────────────────────────
networks:
app-network:
driver: bridge
name: myapp_network
# ── Volumes ───────────────────────────────────────────────────
volumes:
postgres_data:
driver: local
redis_data:
driver: local Studi Kasus: Menjalankan Aplikasi Web + Database dengan Docker Compose
Mari kita terapkan konsep di atas ke dalam studi kasus nyata:
menjalankan aplikasi Node.js + PostgreSQL + Redis
untuk keperluan development lokal. Pertama, buat file
.env
di direktori yang sama dengan
docker-compose.yml
untuk menyimpan nilai sensitif:
# .env (jangan pernah commit file ini ke Git!)
DB_PASSWORD=supersecretpassword123
REDIS_PASSWORD=redispassword456
NODE_ENV=development
Tambahkan juga file
.dockerignore
agar file sensitif dan tidak perlu tidak ikut masuk ke dalam image:
# .dockerignore
node_modules
.git
.env
*.log
coverage
dist
.DS_Store
Kemudian untuk kebutuhan development, buat file
docker-compose.dev.yml
yang meng-override konfigurasi production dengan fitur hot-reload:
# docker-compose.dev.yml (override untuk development)
services:
api:
build:
target: development # Gunakan stage 'development' dari Dockerfile
command: npm run dev # Override CMD dengan nodemon/tsx watch
volumes:
- .:/app # Bind mount source code untuk hot-reload
- /app/node_modules # Hindari override node_modules dari host
environment:
NODE_ENV: development
ports:
- "9229:9229" # Port untuk Node.js debugger Perintah Penting Docker Compose
Setelah file konfigurasi siap, berikut adalah perintah-perintah Docker Compose yang akan Anda gunakan sehari-hari dalam mengelola seluruh stack aplikasi:
# ── Menjalankan Stack ─────────────────────────────────────────
docker compose up # Jalankan semua service (foreground)
docker compose up -d # Jalankan semua service (background)
docker compose up -d api db # Jalankan service tertentu saja
docker compose up -d --build # Rebuild image sebelum menjalankan
docker compose up -d --force-recreate # Paksa buat ulang container
# Menjalankan dengan multiple compose file (development)
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d
# ── Menghentikan Stack ────────────────────────────────────────
docker compose stop # Hentikan container (data tetap ada)
docker compose down # Hentikan + hapus container & network
docker compose down -v # Hentikan + hapus container, network & volume
docker compose down --rmi all # Hapus juga semua image yang digunakan
# ── Monitoring dan Debugging ──────────────────────────────────
docker compose ps # Status semua service
docker compose logs # Log semua service
docker compose logs -f api # Follow log service tertentu
docker compose logs -f --tail 100 api db # Follow log beberapa service
docker compose top # Proses yang berjalan di setiap service
docker compose stats # Resource usage real-time
# ── Mengelola Service ─────────────────────────────────────────
docker compose restart api # Restart service tertentu
docker compose pull # Update semua image ke versi terbaru
docker compose build # Build ulang semua image
docker compose build --no-cache api # Build ulang tanpa cache
# ── Eksekusi Perintah di Dalam Container ─────────────────────
docker compose exec api bash # Masuk ke shell service api
docker compose exec db psql -U postgres # Akses PostgreSQL CLI
docker compose run --rm api npm test # Jalankan perintah sekali lalu hapus container
# ── Validasi Konfigurasi ──────────────────────────────────────
docker compose config # Tampilkan konfigurasi final yang sudah diparse
docker compose config --quiet # Validasi tanpa output (untuk CI) - Selalu gunakan docker compose down (bukan hanya stop) di akhir sesi development untuk membersihkan network yang dibuat secara otomatis dan mencegah konflik di sesi berikutnya.
- Gunakan docker compose run --rm untuk menjalankan perintah satu kali seperti migrasi database atau menjalankan test suite — flag --rm memastikan container langsung dihapus setelah selesai.
- Pisahkan konfigurasi base (docker-compose.yml) dan override per-environment (docker-compose.dev.yml, docker-compose.prod.yml) agar konfigurasi tetap DRY dan mudah dikelola.
- Gunakan docker compose config untuk memverifikasi konfigurasi YAML Anda sebelum menjalankannya — perintah ini akan menampilkan konfigurasi final setelah semua variabel dan override diterapkan.
Dengan Docker Compose, seluruh tim developer dapat menjalankan
environment yang identik hanya dengan tiga langkah:
git clone,
salin file .env,
lalu docker compose up -d.
Tidak ada lagi sesi setup berjam-jam atau masalah
"works on my machine". Selanjutnya, kita akan menyelami
lebih dalam bagaimana Docker mengelola komunikasi antar container
melalui sistem networking-nya.
Docker Networking
Setiap container Docker secara default berjalan dalam isolasi penuh — ia tidak bisa berbicara dengan container lain, dengan host, maupun dengan internet luar kecuali Anda secara eksplisit mengonfigurasinya. Sistem Docker Networking adalah mekanisme yang mengatur bagaimana container berkomunikasi satu sama lain dan dengan dunia luar. Memahami networking Docker adalah keterampilan kritis yang membedakan antara developer yang sekadar bisa menjalankan container dengan developer yang benar-benar memahami infrastrukturnya.
Jenis-Jenis Network di Docker (Bridge, Host, None, Overlay)
Docker menyediakan beberapa network driver bawaan, masing-masing dirancang untuk skenario penggunaan yang berbeda. Memilih driver yang tepat berdampak langsung pada performa, keamanan, dan kemudahan pengelolaan infrastruktur container Anda.
1. Bridge Network (Default)
Bridge adalah driver network default yang digunakan Docker ketika
Anda tidak menentukan network secara eksplisit. Docker membuat
sebuah virtual switch bernama
docker0
di host, dan setiap container yang terhubung ke bridge yang sama
dapat saling berkomunikasi menggunakan IP address. Ada dua jenis
bridge network yang perlu dibedakan:
- Default bridge (docker0): Dibuat otomatis oleh Docker saat instalasi. Container yang terhubung ke sini hanya bisa berkomunikasi via IP address — tidak mendukung DNS resolution berdasarkan nama container.
- User-defined bridge: Network bridge yang Anda buat sendiri dengan docker network create. Ini adalah pilihan yang SELALU direkomendasikan karena mendukung automatic DNS resolution — container bisa saling memanggil berdasarkan nama service-nya.
# Membuat user-defined bridge network
docker network create --driver bridge myapp-network
# Container di network yang sama bisa saling memanggil via nama
docker run -d --network myapp-network --name db postgres:16
docker run -d --network myapp-network --name api myapp-api
# Dari dalam container 'api', bisa ping container 'db' by name
docker exec api ping db # ✅ Berhasil — DNS resolution otomatis
docker exec api curl http://db:5432 # ✅ Berhasil 2. Host Network
Dengan driver host, container berbagi network stack langsung dengan host machine — tidak ada isolasi network sama sekali. Container akan menggunakan IP address dan port host secara langsung, sehingga tidak diperlukan port mapping.
# Container menggunakan network host secara langsung
docker run -d --network host nginx
# Nginx sekarang bisa diakses di port 80 host tanpa -p flag
# CATATAN: Host network hanya berfungsi di Linux
# Di Windows dan macOS, mode ini tidak didukung secara native Host network memberikan performa network tertinggi karena menghilangkan overhead NAT (Network Address Translation). Namun ia mengorbankan isolasi, sehingga hanya tepat digunakan untuk container yang membutuhkan performa jaringan ekstrem seperti monitoring agent atau network proxy.
3. None Network
Driver none menonaktifkan semua konektivitas jaringan
pada container. Container hanya memiliki loopback interface
(lo)
dan tidak bisa berkomunikasi dengan siapapun — tidak dengan container
lain, tidak dengan host, tidak dengan internet.
# Container tanpa akses network sama sekali
docker run -d --network none --name isolated-processor myapp
# Cocok untuk: batch processing, enkripsi/dekripsi data sensitif,
# atau workload komputasi yang tidak butuh koneksi jaringan apapun 4. Overlay Network
Driver overlay memungkinkan container yang berjalan di host yang berbeda untuk berkomunikasi seolah-olah berada dalam satu jaringan lokal yang sama. Overlay network adalah fondasi dari Docker Swarm dan dibutuhkan ketika Anda mulai mendistribusikan container ke multiple server.
# Overlay network memerlukan Docker Swarm yang sudah diinisialisasi
docker swarm init
# Membuat overlay network
docker network create --driver overlay --attachable myswarm-network
# Deploy service yang menggunakan overlay network
docker service create --name api --network myswarm-network --replicas 3 myapp-api:latest Cara Menghubungkan Antar Container
Ada beberapa cara untuk membuat container saling berkomunikasi. Masing-masing memiliki use case dan tingkat rekomendasi yang berbeda. Berikut adalah perbandingan dari pendekatan terlama hingga yang terbaik saat ini:
Cara 1 — IP Address Langsung (Tidak Disarankan)
# Cari tahu IP container database
docker inspect db --format '{{.NetworkSettings.IPAddress}}'
# Output: 172.17.0.2
# ❌ Tidak disarankan: IP bisa berubah setiap container di-restart
docker run -e DB_HOST=172.17.0.2 myapp-api Cara 2 — User-Defined Network (Direkomendasikan)
# ✅ Disarankan: Gunakan nama container sebagai hostname
docker network create appnet
docker run -d --network appnet --name db -e POSTGRES_PASSWORD=secret postgres:16
docker run -d --network appnet --name api -e DB_HOST=db # 'db' langsung resolve ke IP container database
-e DB_PORT=5432 myapp-api
# Verifikasi konektivitas dari dalam container api
docker exec api ping db
docker exec api nslookup db Cara 3 — Via Docker Compose (Paling Mudah)
# ✅ Paling disarankan untuk multi-container: Docker Compose otomatis
# membuat network dan DNS resolution berdasarkan nama service
services:
api:
build: .
environment:
DB_HOST: db # Nama service 'db' langsung bisa digunakan sebagai hostname
depends_on:
- db
db:
image: postgres:16
# Docker Compose secara otomatis membuat network 'projectname_default'
# dan semua service terhubung ke network tersebut Custom Network dan Penggunaannya
Untuk arsitektur yang lebih kompleks, Anda bisa membuat beberapa network terpisah dan menghubungkan container secara selektif. Ini adalah praktik terbaik untuk network segmentation — memastikan bahwa hanya container yang memang perlu berkomunikasi yang bisa saling mengakses, mirip dengan konsep firewall rules di infrastruktur tradisional.
# Arsitektur dengan network segmentation
# frontend-network: nginx ↔ api
# backend-network: api ↔ db ↔ redis
services:
nginx:
image: nginx:alpine
networks:
- frontend-network # Nginx hanya di frontend network
ports:
- "80:80"
api:
build: .
networks:
- frontend-network # API bisa diakses nginx
- backend-network # API bisa akses database
# API adalah satu-satunya container yang ada di kedua network
db:
image: postgres:16
networks:
- backend-network # DB hanya di backend, tidak bisa diakses nginx langsung
redis:
image: redis:7-alpine
networks:
- backend-network # Redis hanya di backend
networks:
frontend-network:
driver: bridge
backend-network:
driver: bridge
internal: true # Network internal: tidak ada akses ke internet Dengan arsitektur di atas, Nginx tidak bisa langsung mengakses database — ia hanya bisa berkomunikasi dengan API. Database pun tidak terekspos ke network publik sama sekali. Ini adalah pendekatan defense in depth yang sangat dianjurkan untuk aplikasi production.
Expose Port dan Port Mapping
Port mapping adalah mekanisme yang meneruskan traffic dari port tertentu di host machine ke port di dalam container. Ini adalah cara utama untuk membuat service di dalam container dapat diakses dari luar — baik dari browser, aplikasi lain, maupun internet.
# Sintaks dasar port mapping: -p HOST_PORT:CONTAINER_PORT
docker run -p 8080:80 nginx # Akses nginx via localhost:8080
# Bind ke IP address spesifik (lebih aman untuk production)
docker run -p 127.0.0.1:8080:80 nginx # Hanya bisa diakses dari lokal, bukan dari luar
# Map ke port random yang tersedia di host
docker run -p 80 nginx # Docker pilihkan port host secara otomatis
docker port nginx # Cek port yang di-assign
# Map multiple port sekaligus
docker run -p 80:80 -p 443:443 nginx
# UDP port mapping
docker run -p 53:53/udp dns-server
Perbedaan penting antara instruksi
EXPOSE
di Dockerfile dengan flag
-p
saat docker run
sering menjadi sumber kebingungan:
- EXPOSE di Dockerfile hanyalah dokumentasi — ia memberitahu pengguna image port mana yang dimaksudkan untuk digunakan aplikasi, namun TIDAK secara otomatis membuka atau meneruskan port apapun ke host.
- Flag -p saat docker run (atau ports: di docker-compose.yml) adalah yang benar-benar membuka port dan meneruskan traffic dari host ke container.
- Untuk komunikasi antar container dalam satu network yang sama, Anda tidak perlu -p sama sekali — container bisa langsung berkomunikasi via nama dan port internal tanpa eksposur ke host.
- Selalu bind port ke 127.0.0.1 (bukan 0.0.0.0) di production jika service hanya perlu diakses secara lokal atau melalui reverse proxy — ini mencegah eksposur langsung ke internet.
Pemahaman yang solid tentang Docker Networking — driver yang tepat, DNS resolution via user-defined network, segmentasi dengan multiple network, dan mekanisme port mapping — adalah bekal yang Anda butuhkan untuk merancang infrastruktur container yang aman dan skalabel. Berikutnya, kita akan membahas bagaimana Docker menangani sisi lain dari infrastruktur yang sama pentingnya: persistensi data melalui Docker Volume.
Docker Volume dan Manajemen Data
Container Docker bersifat ephemeral — artinya semua data yang ditulis di dalam container akan hilang selamanya begitu container dihapus. Ini adalah perilaku yang disengaja dan bahkan diinginkan untuk menjaga container tetap stateless dan mudah diganti. Namun tentu saja, aplikasi nyata membutuhkan data yang persisten: record database, file upload pengguna, log aplikasi, dan sebagainya. Docker Volume adalah solusi resmi Docker untuk menjawab kebutuhan ini — sebuah mekanisme penyimpanan yang hidupnya terpisah dari lifecycle container, sehingga data tetap aman meski container dihapus, diganti, atau di-upgrade ke versi baru.
Perbedaan Volume, Bind Mount, dan tmpfs
Docker menyediakan tiga mekanisme untuk me-mount data ke dalam container. Ketiganya memiliki karakteristik, kelebihan, dan skenario penggunaan yang berbeda. Memilih mekanisme yang tepat adalah keputusan arsitektur yang berdampak pada performa, keamanan, dan kemudahan pengelolaan data aplikasi Anda.
1. Named Volume
Named volume adalah mekanisme penyimpanan yang sepenuhnya dikelola
oleh Docker. Data disimpan di lokasi khusus di dalam filesystem
host (biasanya di
/var/lib/docker/volumes/
di Linux), namun Anda tidak perlu mengetahui atau mengingat lokasi
persisnya — Docker yang mengurusnya. Named volume adalah
pilihan utama yang direkomendasikan untuk data
production seperti database, karena lebih mudah di-backup,
dipindahkan, dan dikelola dibanding bind mount.
# Membuat named volume secara eksplisit
docker volume create postgres_data
# Menggunakan named volume saat docker run
docker run -d --name db -v postgres_data:/var/lib/postgresql/data postgres:16
# Di docker-compose.yml
services:
db:
image: postgres:16
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data: # Deklarasi volume di level atas 2. Bind Mount
Bind mount memetakan direktori atau file spesifik dari filesystem host ke dalam container. Berbeda dengan named volume, Anda harus menentukan path absolut di host secara eksplisit. Bind mount memberikan kontrol penuh atas lokasi data dan memungkinkan dua arah sinkronisasi secara real-time — perubahan di host langsung terlihat di container, dan sebaliknya. Inilah yang menjadikannya pilihan ideal untuk development, di mana Anda ingin perubahan kode terefleksi instan tanpa rebuild.
# Bind mount dengan path absolut
docker run -d -v /home/user/myapp/src:/app/src myapp-dev
# Bind mount dengan path relatif (hanya di docker-compose.yml)
services:
api:
build: .
volumes:
- ./src:/app/src # ✅ Path relatif didukung di Compose
- ./config:/app/config:ro # :ro = read-only, container tidak bisa mengubah config
# Bind mount file tunggal
docker run -d -v $(pwd)/nginx.conf:/etc/nginx/nginx.conf:ro nginx:alpine 3. tmpfs Mount
tmpfs mount menyimpan data langsung di memori RAM host, bukan di filesystem. Data ini bersifat sangat sementara — hilang saat container berhenti atau host di-restart. tmpfs digunakan untuk data yang sensitif (seperti secret atau token sementara yang tidak boleh tertulis ke disk) atau untuk data yang membutuhkan performa I/O ekstrem karena mengakses RAM jauh lebih cepat dibanding storage disk.
# tmpfs mount via flag --tmpfs
docker run -d --tmpfs /app/cache:rw,size=100m myapp
# tmpfs mount via --mount (sintaks lebih eksplisit)
docker run -d --mount type=tmpfs,destination=/app/temp,tmpfs-size=50m myapp
# Di docker-compose.yml
services:
api:
image: myapp
tmpfs:
- /app/cache # Direktori cache di RAM
- /tmp # /tmp juga bisa di-tmpfs - Gunakan Named Volume untuk semua data production yang perlu persisten: database, file uploads, certificate SSL. Docker mengelola lokasinya secara otomatis dan aman.
- Gunakan Bind Mount untuk development lokal agar hot-reload berfungsi, atau untuk meng-inject file konfigurasi dari host ke container dengan flag :ro.
- Gunakan tmpfs untuk data sensitif yang tidak boleh menyentuh disk (seperti secret sementara) atau untuk cache yang membutuhkan performa I/O sangat tinggi.
Cara Membuat dan Menggunakan Docker Volume
Berikut adalah referensi lengkap perintah-perintah Docker untuk mengelola volume dari pembuatan hingga pembersihan, beserta opsi-opsi lanjutan yang sering dibutuhkan dalam praktik nyata.
# ── Membuat Volume ────────────────────────────────────────────
docker volume create mydata
docker volume create --driver local --opt type=nfs --opt o=addr=192.168.1.100,rw --opt device=:/path/to/nfs/share nfs-volume # Volume dari NFS server
# ── Melihat Volume ────────────────────────────────────────────
docker volume ls # List semua volume
docker volume ls --filter dangling=true # Hanya volume yang tidak terpakai
docker volume inspect mydata # Detail lengkap: path, driver, labels
# ── Menggunakan Volume ────────────────────────────────────────
# Sintaks -v (ringkas)
docker run -d -v mydata:/data myapp
# Sintaks --mount (lebih eksplisit dan direkomendasikan)
docker run -d --mount type=volume,source=mydata,target=/data myapp
# Mount volume sebagai read-only
docker run -d --mount type=volume,source=mydata,target=/data,readonly myapp-reader
# ── Berbagi Volume Antar Container ────────────────────────────
# Container pertama menulis data
docker run -d --name writer -v shared_data:/output myapp-writer
# Container kedua membaca data yang sama
docker run -d --name reader --volumes-from writer # Inherit semua volume dari container lain
myapp-reader
# ── Membersihkan Volume ───────────────────────────────────────
docker volume rm mydata # Hapus volume spesifik
docker volume prune # Hapus semua volume yang tidak digunakan
docker volume prune --filter label=env=dev # Hapus berdasarkan label Persistensi Data pada Container Database
Database adalah use case paling umum dan paling kritis untuk Docker Volume. Tanpa volume, seluruh data Anda akan musnah setiap kali container database dihapus atau di-upgrade. Berikut konfigurasi production-ready untuk tiga database yang paling umum digunakan bersama Docker:
PostgreSQL
services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: mydb
POSTGRES_USER: admin
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
PGDATA: /var/lib/postgresql/data/pgdata # Subdirektori khusus di dalam volume
volumes:
- postgres_data:/var/lib/postgresql/data
- ./db/postgresql.conf:/etc/postgresql/postgresql.conf:ro
- ./db/init/:/docker-entrypoint-initdb.d/ # Script SQL dijalankan saat init
healthcheck:
test: ["CMD-SHELL", "pg_isready -U admin -d mydb"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s # Beri waktu PostgreSQL untuk startup
volumes:
postgres_data: MySQL / MariaDB
services:
mysql:
image: mysql:8.0
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: mydb
MYSQL_USER: admin
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
volumes:
- mysql_data:/var/lib/mysql
- ./db/my.cnf:/etc/mysql/conf.d/my.cnf:ro
- ./db/init/:/docker-entrypoint-initdb.d/
command: --default-authentication-plugin=mysql_native_password
volumes:
mysql_data: MongoDB
services:
mongodb:
image: mongo:7
restart: unless-stopped
environment:
MONGO_INITDB_ROOT_USERNAME: admin
MONGO_INITDB_ROOT_PASSWORD: ${MONGO_PASSWORD}
MONGO_INITDB_DATABASE: mydb
volumes:
- mongodb_data:/data/db
- mongodb_config:/data/configdb # Volume terpisah untuk konfigurasi
- ./db/mongo-init.js:/docker-entrypoint-initdb.d/init.js:ro
volumes:
mongodb_data:
mongodb_config: Backup dan Restore Data Docker Volume
Memiliki data yang persisten di volume saja tidak cukup — Anda juga membutuhkan strategi backup yang handal. Docker tidak menyediakan perintah backup bawaan untuk volume, namun ada pola yang sangat elegan menggunakan container sementara sebagai "helper" untuk mengeksekusi operasi backup dan restore.
Backup Volume ke File Tar
# Pola backup: jalankan container helper yang mount volume target
# lalu kompres isinya ke file .tar.gz di direktori host
docker run --rm -v postgres_data:/source:ro # Mount volume yang akan di-backup (read-only)
-v $(pwd)/backups:/backup # Mount direktori backup di host
alpine tar czf /backup/postgres_$(date +%Y%m%d_%H%M%S).tar.gz -C /source .
# Hasil: file backup/postgres_20240115_143022.tar.gz di direktori host Anda Backup Database PostgreSQL Langsung (pg_dump)
# Backup menggunakan pg_dump — lebih disarankan untuk database karena
# menghasilkan dump yang konsisten secara transaksional
docker exec postgres pg_dump -U admin mydb | gzip > backups/mydb_$(date +%Y%m%d_%H%M%S).sql.gz
# Backup semua database sekaligus
docker exec postgres pg_dumpall -U admin | gzip > backups/all_dbs_$(date +%Y%m%d_%H%M%S).sql.gz Restore Volume dari File Tar
# Pastikan container database dihentikan sebelum restore
docker compose stop db
# Restore data dari file backup ke volume
docker run --rm -v postgres_data:/target # Volume tujuan restore
-v $(pwd)/backups:/backup:ro # Direktori backup di host (read-only)
alpine sh -c "cd /target && tar xzf /backup/postgres_20240115_143022.tar.gz"
# Restore dari SQL dump PostgreSQL
docker exec -i postgres psql -U admin mydb < backups/mydb_20240115_143022.sql
# Jalankan kembali container setelah restore selesai
docker compose start db Memindahkan Volume ke Host Lain
# Di server LAMA: backup dan kirim ke server baru via scp
docker run --rm -v postgres_data:/source:ro alpine tar czf - -C /source . | ssh user@new-server "cat > /tmp/postgres_data.tar.gz"
# Di server BARU: buat volume dan restore
docker volume create postgres_data
docker run --rm -v postgres_data:/target -v /tmp:/backup:ro alpine sh -c "cd /target && tar xzf /backup/postgres_data.tar.gz"
# Verifikasi data berhasil dipindahkan
docker run --rm -v postgres_data:/data:ro alpine ls -la /data - Otomatiskan backup volume database dengan cron job atau scheduled task — jangan andalkan backup manual yang rawan terlupakan di lingkungan production.
- Selalu test proses restore secara berkala di environment staging, bukan hanya saat disaster terjadi. Backup yang belum pernah ditest restore-nya pada dasarnya tidak bisa diandalkan.
- Simpan file backup di lokasi yang berbeda dari server production — gunakan object storage seperti AWS S3, Google Cloud Storage, atau Backblaze B2 untuk durabilitas data yang sesungguhnya.
- Untuk database production dengan traffic tinggi, gunakan fitur backup native database (pg_dump, mysqldump, mongodump) daripada backup raw volume — hasilnya lebih konsisten secara transaksional dan lebih kecil ukurannya.
Dengan strategi volume dan backup yang solid, data aplikasi Anda terlindungi dari kehilangan yang tidak terduga. Infrastruktur container yang baik bukan hanya soal menjalankan container dengan benar, tapi juga memastikan data di dalamnya aman, dapat dipulihkan, dan mudah dipindahkan. Selanjutnya, kita akan membahas bagaimana membawa semua pengetahuan ini ke level berikutnya: menjalankan Docker di lingkungan production dengan standar keamanan, performa, dan observability yang sesungguhnya.
Docker di Lingkungan Production
Menjalankan Docker di laptop development dan menjalankannya di server production adalah dua hal yang sangat berbeda. Di production, setiap keputusan konfigurasi berdampak nyata pada keamanan sistem, stabilitas aplikasi, dan pengalaman pengguna akhir. Section ini membahas praktik-praktik terbaik yang harus Anda terapkan sebelum berani menyebut infrastruktur Docker Anda benar-benar production-ready — mulai dari hardening keamanan, optimasi ukuran image, pembatasan resource, hingga observability yang komprehensif.
Best Practice Keamanan Docker di Production
Keamanan container bukan fitur tambahan yang bisa dikonfigurasi belakangan — ia harus menjadi bagian integral dari desain sejak awal. Berikut adalah lapisan-lapisan keamanan yang wajib diterapkan pada setiap deployment Docker production.
1. Jalankan Container sebagai Non-Root User
Secara default, proses di dalam container berjalan sebagai
root
(UID 0). Jika terjadi container escape — kondisi di mana penyerang
berhasil keluar dari isolasi container — proses root di container
berpotensi mendapatkan akses root di host. Selalu buat dan gunakan
user non-root di dalam Dockerfile Anda:
# Dockerfile dengan non-root user
FROM node:20-alpine
WORKDIR /app
# Buat grup dan user khusus dengan UID/GID spesifik
RUN addgroup -g 1001 -S appgroup && adduser -u 1001 -S appuser -G appgroup
# Salin file dengan kepemilikan yang benar sekaligus
COPY --chown=appuser:appgroup package*.json ./
RUN npm ci --only=production
COPY --chown=appuser:appgroup . .
# Pindah ke non-root user SEBELUM CMD
USER appuser
EXPOSE 3000
CMD ["node", "server.js"] 2. Gunakan Read-Only Filesystem
# Jalankan container dengan filesystem read-only
docker run -d --read-only --tmpfs /tmp # Izinkan tulis hanya di /tmp (RAM)
--tmpfs /app/cache # Dan direktori cache
-v logs_volume:/app/logs # Volume untuk logs yang memang perlu ditulis
myapp
# Di docker-compose.yml
services:
api:
image: myapp:latest
read_only: true
tmpfs:
- /tmp
- /app/cache
volumes:
- logs_volume:/app/logs 3. Drop Linux Capabilities yang Tidak Diperlukan
Linux capabilities adalah unit-unit privilege yang dipecah dari hak akses root. Secara default Docker memberikan beberapa capability kepada container — namun sebagian besar aplikasi web tidak membutuhkan semuanya. Prinsipnya: drop semua capability, lalu tambahkan hanya yang benar-benar dibutuhkan.
# Drop semua capability, tambahkan hanya yang dibutuhkan
docker run -d --cap-drop ALL # Drop semua capability
--cap-add NET_BIND_SERVICE # Izinkan bind ke port < 1024 (jika diperlukan)
--security-opt no-new-privileges:true # Cegah privilege escalation
myapp
# Di docker-compose.yml
services:
api:
image: myapp:latest
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
security_opt:
- no-new-privileges:true 4. Scan Image untuk Vulnerability
# Scan image menggunakan Docker Scout (built-in di Docker Desktop)
docker scout cves myapp:latest
docker scout recommendations myapp:latest # Saran perbaikan
# Alternatif: Trivy (open-source, sangat populer di CI/CD)
# Install: https://aquasecurity.github.io/trivy
trivy image myapp:latest
trivy image --severity HIGH,CRITICAL myapp:latest # Filter severity
trivy image --exit-code 1 --severity CRITICAL myapp:latest # Fail CI jika ada CRITICAL
# Scan image dari Docker Hub sebelum digunakan
trivy image nginx:1.25-alpine
trivy image postgres:16-alpine - Selalu gunakan image resmi (official images) dari Docker Hub sebagai base image, dan pilih varian -alpine atau -slim untuk meminimalkan attack surface.
- Jangan pernah menyimpan secret, API key, atau password langsung di dalam Dockerfile atau di-hardcode sebagai ENV — gunakan Docker Secrets, HashiCorp Vault, atau environment variable yang di-inject saat runtime.
- Aktifkan Docker Content Trust (DCT) dengan export DOCKER_CONTENT_TRUST=1 untuk memastikan hanya image yang ditandatangani secara kriptografis yang bisa di-pull dan dijalankan.
- Pisahkan jaringan container dengan prinsip least privilege — database tidak boleh bisa diakses langsung dari internet, hanya dari API service yang membutuhkannya.
Optimasi Ukuran Docker Image (Multi-Stage Build)
Image yang besar berdampak pada banyak hal sekaligus: waktu build lebih lama, transfer ke registry lebih lambat, deployment ke server memakan waktu lebih banyak, dan attack surface yang lebih luas. Multi-stage build adalah teknik paling powerful untuk menghasilkan image production yang ramping — idenya sederhana: gunakan satu stage untuk build (yang boleh besar dan kotor), lalu salin hanya artefak yang diperlukan ke stage final yang bersih dan minimal.
Contoh: Node.js TypeScript App
# ── Stage 1: Dependencies ─────────────────────────────────────
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
# ── Stage 2: Builder ──────────────────────────────────────────
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build # Kompilasi TypeScript → JavaScript
# ── Stage 3: Production Runner (image final yang minimal) ─────
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
# Buat non-root user
RUN addgroup -g 1001 -S appgroup && adduser -u 1001 -S appuser -G appgroup
# Salin HANYA yang dibutuhkan dari stage builder
COPY --from=builder --chown=appuser:appgroup /app/dist ./dist
COPY --from=builder --chown=appuser:appgroup /app/node_modules ./node_modules
COPY --from=builder --chown=appuser:appgroup /app/package.json ./
USER appuser
EXPOSE 3000
CMD ["node", "dist/server.js"]
# Hasilnya: image ~150MB vs ~900MB tanpa multi-stage Contoh: Go Application (Binary Minimal)
# ── Stage 1: Build binary ─────────────────────────────────────
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o server .
# ── Stage 2: Image minimal dengan scratch ─────────────────────
FROM scratch AS runner # FROM scratch = image kosong, 0 byte!
COPY --from=builder /app/server /server
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 8080
ENTRYPOINT ["/server"]
# Hasilnya: image ~10-15MB vs ~350MB tanpa multi-stage Resource Limiting: CPU dan Memory pada Container
Tanpa batasan resource, sebuah container yang berperilaku buruk — entah karena memory leak, infinite loop, atau serangan DoS — bisa mengkonsumsi seluruh resource host dan menyebabkan semua container lain ikut terdampak. Resource limiting adalah jaring pengaman wajib di production yang memastikan satu container tidak bisa "merusak tetangganya".
# ── Memory Limits ─────────────────────────────────────────────
docker run -d --memory="512m" # Hard limit: container di-kill jika melebihi
--memory-reservation="256m" # Soft limit: Docker usahakan ini dulu
--memory-swap="512m" # Sama dengan memory = tidak ada swap
myapp
# ── CPU Limits ────────────────────────────────────────────────
docker run -d --cpus="1.5" # Maksimal 1.5 CPU core
--cpu-shares=512 # Bobot relatif (default 1024) untuk kontesi
myapp
# ── Kombinasi di docker-compose.yml ───────────────────────────
services:
api:
image: myapp:latest
deploy:
resources:
limits:
cpus: "1.0" # Maksimal 1 CPU core
memory: 512M # Maksimal 512MB RAM
reservations:
cpus: "0.25" # Minimal 0.25 CPU core dijamin
memory: 128M # Minimal 128MB RAM dijamin
db:
image: postgres:16-alpine
deploy:
resources:
limits:
cpus: "2.0"
memory: 1G
reservations:
cpus: "0.5"
memory: 256M
Untuk menentukan nilai limit yang tepat, jalankan aplikasi Anda
terlebih dahulu tanpa limit sambil dipantau dengan
docker stats
di bawah beban normal dan peak load. Gunakan data tersebut sebagai
baseline, lalu set limit sekitar 20-30% di atas
rata-rata peak sebagai ruang napas yang aman.
Health Check pada Docker Container
Docker hanya tahu apakah sebuah container berjalan atau tidak — ia tidak tahu apakah aplikasi di dalamnya benar-benar sehat dan siap menerima traffic. Sebuah container bisa berstatus running namun aplikasinya sebenarnya sudah deadlock, kehabisan koneksi database, atau stuck di infinite loop. Health check adalah mekanisme untuk memberitahu Docker cara memverifikasi kesehatan aplikasi secara aktif dan berkala.
# ── Health Check di Dockerfile ────────────────────────────────
# Untuk aplikasi HTTP
HEALTHCHECK --interval=30s # Cek setiap 30 detik
--timeout=10s # Timeout per cek
--start-period=40s # Grace period saat container baru start
--retries=3 # Gagal 3x berturut-turut = unhealthy
CMD wget -qO- http://localhost:3000/health || exit 1
# Untuk aplikasi yang tidak punya HTTP endpoint
HEALTHCHECK --interval=30s --timeout=5s --retries=3 CMD pg_isready -U postgres || exit 1 # PostgreSQL
HEALTHCHECK --interval=30s --timeout=5s --retries=3 CMD redis-cli ping || exit 1 # Redis
# ── Health Check di docker-compose.yml ────────────────────────
services:
api:
image: myapp:latest
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
start_period: 40s
retries: 3
db:
image: postgres:16-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
Endpoint
/health
di aplikasi Anda sebaiknya lebih dari sekadar mengembalikan
status 200 — ia harus memverifikasi koneksi ke database,
koneksi ke cache, dan dependensi kritis lainnya. Berikut
contoh implementasi health check endpoint yang komprehensif
di Node.js:
// health.route.ts
app.get('/health', async (req, res) => {
const checks = {
status: 'ok',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
checks: {
database: 'unknown',
redis: 'unknown',
}
};
try {
await db.query('SELECT 1'); // Cek koneksi database
checks.checks.database = 'ok';
} catch {
checks.checks.database = 'error';
checks.status = 'degraded';
}
try {
await redis.ping(); // Cek koneksi Redis
checks.checks.redis = 'ok';
} catch {
checks.checks.redis = 'error';
checks.status = 'degraded';
}
const httpStatus = checks.status === 'ok' ? 200 : 503;
res.status(httpStatus).json(checks);
}); Logging dan Monitoring Container di Production
Di production, Anda tidak bisa hanya mengandalkan
docker logs
untuk melihat apa yang terjadi pada container Anda. Anda
membutuhkan sistem logging terpusat dan monitoring yang
memberikan visibilitas penuh terhadap performa, error, dan
anomali di seluruh fleet container secara bersamaan.
Konfigurasi Logging Driver
# Docker mendukung berbagai logging driver:
# json-file (default), syslog, journald, fluentd, awslogs, gelf, splunk
# Konfigurasi logging driver di /etc/docker/daemon.json (global)
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m", # Rotasi log setiap 10MB
"max-file": "5", # Simpan maksimal 5 file log
"compress": "true", # Kompres log lama
"labels": "service,environment"
}
}
# Per-container via docker run
docker run -d --log-driver json-file --log-opt max-size=10m --log-opt max-file=5 myapp
# Per-service di docker-compose.yml
services:
api:
image: myapp:latest
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5" Stack Monitoring: Prometheus + Grafana
# docker-compose.monitoring.yml
services:
# Prometheus: scrape dan simpan metrics
prometheus:
image: prom/prometheus:latest
container_name: prometheus
restart: unless-stopped
volumes:
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.retention.time=15d'
ports:
- "127.0.0.1:9090:9090" # Hanya akses lokal
networks:
- monitoring-network
# Grafana: visualisasi metrics
grafana:
image: grafana/grafana:latest
container_name: grafana
restart: unless-stopped
environment:
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD}
GF_USERS_ALLOW_SIGN_UP: "false"
volumes:
- grafana_data:/var/lib/grafana
- ./monitoring/grafana/dashboards:/etc/grafana/provisioning/dashboards:ro
- ./monitoring/grafana/datasources:/etc/grafana/provisioning/datasources:ro
ports:
- "127.0.0.1:3000:3000"
networks:
- monitoring-network
# cAdvisor: metrics container Docker
cadvisor:
image: gcr.io/cadvisor/cadvisor:latest
container_name: cadvisor
restart: unless-stopped
privileged: true
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
networks:
- monitoring-network
# Node Exporter: metrics host machine
node-exporter:
image: prom/node-exporter:latest
container_name: node-exporter
restart: unless-stopped
pid: host
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
command:
- '--path.procfs=/host/proc'
- '--path.sysfs=/host/sys'
networks:
- monitoring-network
networks:
monitoring-network:
driver: bridge
volumes:
prometheus_data:
grafana_data: - Tulis log ke stdout dan stderr di dalam aplikasi Anda — bukan ke file di dalam container. Docker akan menangkap output ini dan mengarahkannya ke logging driver yang dikonfigurasi.
- Selalu set batas ukuran log (max-size dan max-file) untuk mencegah log menghabiskan seluruh storage disk server production Anda secara diam-diam.
- Gunakan structured logging dalam format JSON di aplikasi Anda agar log mudah di-parse, di-filter, dan di-query oleh sistem log aggregation seperti Loki, Elasticsearch, atau CloudWatch.
- Pisahkan stack monitoring (Prometheus + Grafana) ke compose file terpisah (docker-compose.monitoring.yml) agar bisa di-deploy dan di-update secara independen dari aplikasi utama.
- Set alert di Grafana untuk metrik kritis: CPU usage > 80%, memory usage > 85%, response time > 500ms, dan error rate > 1% — jangan tunggu pengguna yang memberitahu Anda ada masalah.
Production-readiness bukan checklist satu kali yang selesai setelah deployment pertama — ia adalah komitmen berkelanjutan terhadap keamanan, stabilitas, dan observability. Dengan menerapkan semua praktik di section ini, Anda memiliki fondasi yang kokoh untuk menjalankan container Docker di production dengan percaya diri. Selanjutnya, kita akan naik ke level berikutnya: mengelola container di multiple server sekaligus menggunakan Docker Swarm dan orkestrasi container.
Pengenalan Docker Swarm dan Orkestrasi
Sejauh ini seluruh pembahasan kita berfokus pada menjalankan container di satu host. Pendekatan ini bekerja dengan baik untuk aplikasi kecil hingga menengah, namun mulai menunjukkan keterbatasannya ketika traffic tumbuh, kebutuhan high availability meningkat, atau kapasitas satu server tidak lagi mencukupi. Di sinilah orkestrasi container menjadi jawaban — sebuah lapisan manajemen yang mengkoordinasikan ratusan hingga ribuan container yang tersebar di banyak server seolah-olah semuanya adalah satu sistem tunggal yang kohesif. Docker menyediakan solusi orkestrasi bawaan yang disebut Docker Swarm, yang menawarkan keseimbangan sempurna antara kemudahan penggunaan dan kemampuan production-grade.
Apa Itu Docker Swarm?
Docker Swarm adalah mode orkestrasi container yang sudah terintegrasi langsung ke dalam Docker Engine — tidak perlu instalasi software tambahan apapun. Dengan Swarm, sekelompok mesin Docker (disebut nodes) digabungkan menjadi satu cluster yang dikelola secara terpusat. Anda cukup mendefinisikan desired state aplikasi — misalnya "jalankan 5 replica container API saya" — dan Swarm yang akan bertanggung jawab mewujudkan serta mempertahankan state tersebut, termasuk memulihkan container yang gagal secara otomatis.
Arsitektur Docker Swarm terdiri dari dua jenis node dengan peran yang berbeda:
- Manager Node: Otak dari cluster. Bertanggung jawab atas seluruh manajemen cluster — menerima perintah dari Docker CLI, menjadwalkan service ke worker node, memantau kesehatan cluster, dan menyimpan state cluster menggunakan algoritma konsensus Raft. Untuk high availability, gunakan jumlah manager yang ganjil: 3 manager toleran terhadap 1 kegagalan, 5 manager toleran terhadap 2 kegagalan.
- Worker Node: Tenaga kerja dari cluster. Hanya bertugas menjalankan container (tasks) yang dijadwalkan oleh manager. Worker tidak terlibat dalam pengambilan keputusan manajemen cluster dan tidak menyimpan state Raft.
Konsep penting lainnya di Swarm adalah perbedaan antara Service dan Task. Service adalah definisi desired state — "saya ingin nginx berjalan dengan 3 replica menggunakan image nginx:1.25". Task adalah unit kerja aktual yang dijadwalkan ke node — setiap replica adalah satu task. Jika sebuah task gagal, Swarm secara otomatis menjadwalkan task pengganti di node yang tersedia.
Perbedaan Docker Swarm vs Kubernetes
Pertanyaan yang selalu muncul ketika membahas orkestrasi container adalah: kapan harus menggunakan Docker Swarm dan kapan harus beralih ke Kubernetes? Keduanya memecahkan masalah yang sama — mengelola container di banyak host — namun dengan filosofi desain dan trade-off yang sangat berbeda.
Docker Swarm — Simpel dan Cepat
- Kurva belajar yang sangat landai: jika sudah familiar dengan Docker Compose, Anda bisa produktif dengan Swarm dalam hitungan jam karena sintaks stack file-nya hampir identik.
- Zero dependency: sudah built-in di Docker Engine, tidak perlu instalasi komponen tambahan. Setup cluster bisa selesai dalam 10-15 menit.
- Operasional lebih sederhana: lebih sedikit moving parts berarti lebih sedikit hal yang bisa salah, lebih mudah di-debug, dan lebih ringan di resource.
- Cocok untuk: tim kecil, aplikasi dengan kebutuhan scaling sederhana, startup, atau organisasi yang tidak memiliki dedicated DevOps engineer.
Kubernetes — Powerful dan Fleksibel
- Ekosistem yang jauh lebih kaya: Helm charts, custom operators, service mesh (Istio), GitOps (ArgoCD/Flux), dan ratusan tools integrasi yang sudah production-proven.
- Fitur lanjutan: horizontal pod autoscaling berdasarkan custom metrics, rolling updates dengan strategi canary, network policies granular, dan RBAC yang sangat detail.
- Dukungan cloud native penuh: semua cloud provider besar (AWS EKS, Google GKE, Azure AKS) menyediakan managed Kubernetes yang siap digunakan.
- Cocok untuk: tim besar, aplikasi microservices kompleks, kebutuhan scaling otomatis yang agresif, atau organisasi yang sudah berkomitmen ke cloud-native.
Rekomendasi praktis: mulailah dengan Docker Swarm jika Anda baru memasuki dunia orkestrasi atau tim Anda belum siap dengan kompleksitas Kubernetes. Swarm bukanlah "Kubernetes yang kurang bagus" — ia adalah tool yang tepat untuk skenario tertentu. Migrasi dari Swarm ke Kubernetes di kemudian hari selalu memungkinkan ketika kebutuhan sudah benar-benar membutuhkannya.
Cara Setup Docker Swarm Cluster Sederhana
Untuk mengikuti panduan ini, Anda membutuhkan minimal dua server (atau VM) dengan Docker Engine terinstal dan koneksi jaringan antar server. Kita akan membuat cluster dengan arsitektur 1 manager + 2 worker yang merupakan setup minimum yang direkomendasikan untuk production.
Langkah 1 — Inisialisasi Swarm di Manager Node
# Jalankan di server yang akan menjadi Manager Node
# Ganti dengan IP address server manager Anda
docker swarm init --advertise-addr 192.168.1.10
# Output yang akan muncul:
# Swarm initialized: current node (xyz...) is now a manager.
#
# To add a worker to this swarm, run the following command:
# docker swarm join --token SWMTKN-1-xxxx 192.168.1.10:2377
#
# To add a manager to this swarm, run 'docker swarm join-token manager'
# and follow the instructions.
# Simpan token worker dan manager untuk digunakan nanti
docker swarm join-token worker # Token untuk menambah worker
docker swarm join-token manager # Token untuk menambah manager Langkah 2 — Tambahkan Worker Nodes ke Cluster
# Jalankan di setiap server Worker Node
# Gunakan token yang didapat dari langkah sebelumnya
docker swarm join --token SWMTKN-1-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 192.168.1.10:2377
# Output: This node joined a swarm as a worker. Langkah 3 — Verifikasi Cluster
# Jalankan di Manager Node untuk melihat semua node
docker node ls
# Output contoh:
# ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS
# abc123def456 * manager-1 Ready Active Leader
# def456ghi789 worker-1 Ready Active
# ghi789jkl012 worker-2 Ready Active
# Lihat detail node tertentu
docker node inspect worker-1 --pretty
# Update label pada node (berguna untuk placement constraints)
docker node update --label-add zone=us-east-1a worker-1
docker node update --label-add disktype=ssd worker-2
# Drain node untuk maintenance (pindahkan semua task ke node lain)
docker node update --availability drain worker-1
# Kembalikan node ke active setelah maintenance selesai
docker node update --availability active worker-1 Langkah 4 — Konfigurasi Overlay Network untuk Cluster
# Buat overlay network yang akan menghubungkan container lintas host
docker network create --driver overlay --attachable --subnet 10.10.0.0/16 myapp-overlay
# Verifikasi network berhasil dibuat
docker network ls --filter driver=overlay Deploy Service di Docker Swarm
Di Docker Swarm, unit deployment bukan lagi container individual melainkan service. Anda bisa mendeploy service satu per satu via CLI, atau mendeploy seluruh stack aplikasi sekaligus menggunakan file stack yang formatnya hampir identik dengan docker-compose.yml.
Deploy Service via CLI
# Membuat service baru dengan 3 replica
docker service create --name api --replicas 3 --network myapp-overlay --publish published=80,target=3000 # Port mapping di semua node
--update-delay 10s # Jeda antar update replica
--update-parallelism 1 # Update satu replica per batch
--restart-condition on-failure --restart-max-attempts 3 myapp-api:1.0.0
# Melihat semua service yang berjalan
docker service ls
# Melihat detail distribusi task di setiap node
docker service ps api
# Output contoh:
# ID NAME IMAGE NODE DESIRED STATE CURRENT STATE
# abc123 api.1 myapp-api:1.0 manager-1 Running Running 2 min
# def456 api.2 myapp-api:1.0 worker-1 Running Running 2 min
# ghi789 api.3 myapp-api:1.0 worker-2 Running Running 2 min Scaling dan Rolling Update
# Scale service secara instan — Swarm otomatis distribusikan ke node
docker service scale api=6 # Naik dari 3 ke 6 replica
docker service scale api=2 # Turun ke 2 replica
docker service scale api=5 worker=3 # Scale beberapa service sekaligus
# Rolling update ke versi baru — zero downtime!
docker service update --image myapp-api:2.0.0 # Image baru
--update-parallelism 2 # Update 2 replica sekaligus
--update-delay 15s # Tunggu 15 detik antar batch
--update-failure-action rollback # Rollback otomatis jika update gagal
api
# Rollback manual ke versi sebelumnya
docker service rollback api
# Memonitor proses update secara real-time
docker service ps api --filter desired-state=running Deploy Stack Lengkap dari Compose File
# docker-stack.yml — format mirip docker-compose namun dengan deploy config
services:
api:
image: myapp-api:2.0.0 # Harus menggunakan image, bukan build
networks:
- myapp-overlay
environment:
DB_HOST: db
NODE_ENV: production
deploy:
replicas: 3
update_config:
parallelism: 1 # Update satu per satu
delay: 10s
failure_action: rollback # Rollback otomatis jika gagal
monitor: 30s # Pantau 30 detik setelah update
max_failure_ratio: 0.1 # Toleransi 10% kegagalan
rollback_config:
parallelism: 2
delay: 5s
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
window: 120s
placement:
constraints:
- node.role == worker # Hanya deploy ke worker node
- node.labels.zone == us-east-1a
preferences:
- spread: node.id # Sebar rata ke semua node
resources:
limits:
cpus: "1.0"
memory: 512M
reservations:
cpus: "0.25"
memory: 128M
db:
image: postgres:16-alpine
networks:
- myapp-overlay
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password # Gunakan Docker Secrets
secrets:
- db_password
volumes:
- postgres_data:/var/lib/postgresql/data
deploy:
replicas: 1
placement:
constraints:
- node.labels.disktype == ssd # Database harus di node dengan SSD
nginx:
image: nginx:1.25-alpine
networks:
- myapp-overlay
ports:
- "80:80"
- "443:443"
deploy:
replicas: 2
placement:
constraints:
- node.role == manager # Nginx di manager agar dekat dengan ingress
networks:
myapp-overlay:
driver: overlay
external: true # Gunakan network yang sudah dibuat sebelumnya
volumes:
postgres_data:
secrets:
db_password:
external: true # Secret sudah dibuat via docker secret create # Buat Docker Secret untuk menyimpan data sensitif dengan aman
echo "supersecretpassword" | docker secret create db_password -
# Deploy seluruh stack dari file
docker stack deploy -c docker-stack.yml myapp
# Perintah manajemen stack
docker stack ls # List semua stack
docker stack services myapp # Service dalam stack
docker stack ps myapp # Semua task dalam stack
docker stack ps myapp --filter desired-state=running
# Update stack (re-deploy dengan konfigurasi baru)
docker stack deploy -c docker-stack.yml myapp # Jalankan ulang untuk apply perubahan
# Hapus stack beserta semua service-nya
docker stack rm myapp - Selalu gunakan Docker Secrets untuk menyimpan data sensitif seperti password database dan API key di Swarm — jangan gunakan environment variable biasa karena nilai tersebut terekspos via docker inspect.
- Gunakan placement constraints dan preferences untuk mengontrol di mana service berjalan: database di node dengan SSD, stateless service tersebar merata ke semua worker, dan ingress di manager node.
- Set update_failure_action: rollback pada setiap service production agar Swarm otomatis mengembalikan ke versi sebelumnya jika rolling update menyebabkan health check gagal.
- Untuk high availability manager, gunakan jumlah ganjil (3 atau 5) dan pertimbangkan placement manager di availability zone yang berbeda agar tahan terhadap kegagalan satu data center.
- Gunakan docker service logs -f myapp_api untuk melihat log gabungan dari semua replica service sekaligus — sangat berguna untuk debugging di cluster multi-node.
Docker Swarm memberikan kemampuan orkestrasi yang solid — high availability, rolling updates tanpa downtime, scaling instan, dan self-healing otomatis — dengan kompleksitas operasional yang jauh lebih rendah dibanding Kubernetes. Ini menjadikannya pilihan yang sangat pragmatis untuk tim yang ingin mendapatkan manfaat orkestrasi container tanpa harus menginvestasikan waktu berminggu-minggu untuk mempelajari ekosistem baru. Di section berikutnya, kita akan mengintegrasikan semua yang telah dipelajari ke dalam puncak dari keseluruhan pembahasan ini: membangun pipeline CI/CD end-to-end yang otomatis mem-build, menguji, dan mendeploy aplikasi Docker Anda setiap kali ada perubahan kode.
Integrasi Docker dengan CI/CD Pipeline
Semua pengetahuan tentang Docker yang telah kita bangun — Dockerfile, image optimization, Docker Compose, networking, volume, hingga Swarm — akan mencapai potensi penuhnya ketika diintegrasikan ke dalam pipeline CI/CD (Continuous Integration / Continuous Deployment). Pipeline CI/CD mengotomatiskan seluruh perjalanan kode dari commit developer hingga berjalan di server production: build image, jalankan test suite, scan vulnerability, push ke registry, dan deploy ke server — semuanya tanpa intervensi manual, konsisten setiap saat, dan dapat diaudit secara penuh. Inilah yang memungkinkan tim engineering modern untuk merilis software berkali-kali dalam sehari dengan tingkat kepercayaan yang tinggi.
Mengapa Docker Penting dalam CI/CD?
Sebelum Docker, pipeline CI/CD sering kali rapuh dan sulit dikelola. Runner server perlu dikonfigurasi manual dengan versi bahasa, library, dan tools yang tepat — dan perbedaan kecil antara environment runner dengan environment production sudah cukup untuk menyebabkan bug yang sulit direproduksi. Docker memecahkan masalah ini secara fundamental dengan menjadikan environment sebagai kode.
- Environment Konsisten: Setiap pipeline run menggunakan container yang identik — tidak ada lagi 'test passed di CI tapi gagal di production' karena perbedaan versi library atau konfigurasi sistem.
- Isolasi Antar Job: Setiap CI job berjalan di container segar yang bersih, menghilangkan kemungkinan state dari job sebelumnya mencemari hasil test.
- Paralelisasi Mudah: Menjalankan ratusan test suite secara paralel di container terpisah menjadi trivial, memotong waktu feedback dari menit menjadi detik.
- Artefak yang Immutable: Image Docker yang di-build di CI adalah artefak yang sama persis yang akan di-deploy ke staging dan production — tidak ada proses build ulang di setiap environment.
- Rollback Instan: Karena setiap versi aplikasi adalah image dengan tag yang unik, rollback ke versi sebelumnya semudah menjalankan docker service update dengan tag lama.
Docker dalam Pipeline GitHub Actions
GitHub Actions adalah platform CI/CD yang paling banyak digunakan saat ini, terintegrasi langsung ke dalam repository GitHub Anda. Berikut adalah pipeline production-grade yang lengkap: mulai dari test, security scan, multi-platform build, push ke registry, hingga deploy otomatis ke server.
# .github/workflows/docker-cicd.yml
name: Docker CI/CD Pipeline
on:
push:
branches: [main, develop]
tags: ['v*.*.*'] # Trigger juga saat push tag versi
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }} # username/repo-name
jobs:
# ── Job 1: Test ────────────────────────────────────────────
test:
name: Run Test Suite
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run tests in Docker container
run: |
docker compose -f docker-compose.test.yml up --build --abort-on-container-exit --exit-code-from test-runner
- name: Cleanup test containers
if: always()
run: docker compose -f docker-compose.test.yml down -v
# ── Job 2: Security Scan ───────────────────────────────────
security-scan:
name: Security Vulnerability Scan
runs-on: ubuntu-latest
needs: test # Hanya jalan jika test berhasil
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Build image for scanning
run: docker build -t scan-target:latest .
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: scan-target:latest
format: sarif
output: trivy-results.sarif
severity: CRITICAL,HIGH
exit-code: '1' # Gagalkan pipeline jika ada CRITICAL
- name: Upload scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: trivy-results.sarif
# ── Job 3: Build dan Push Image ────────────────────────────
build-and-push:
name: Build and Push Docker Image
runs-on: ubuntu-latest
needs: [test, security-scan]
permissions:
contents: read
packages: write # Izin untuk push ke GHCR
outputs:
image-digest: ${{ steps.build.outputs.digest }}
image-tag: ${{ steps.meta.outputs.tags }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up QEMU (untuk multi-platform build)
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver-opts: |
image=moby/buildkit:latest
network=host
- name: Login ke GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} # Token otomatis dari GitHub
- name: Generate image metadata dan tags
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,prefix=sha-,format=short
type=raw,value=latest,enable={{is_default_branch}}
- name: Build dan push multi-platform image
id: build
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64 # Support Apple Silicon + x86
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha # GitHub Actions cache
cache-to: type=gha,mode=max
build-args: |
BUILD_DATE=${{ github.event.head_commit.timestamp }}
GIT_COMMIT=${{ github.sha }}
# ── Job 4: Deploy ke Staging ───────────────────────────────
deploy-staging:
name: Deploy to Staging
runs-on: ubuntu-latest
needs: build-and-push
if: github.ref == 'refs/heads/develop' # Hanya dari branch develop
environment:
name: staging
url: https://staging.myapp.com
steps:
- name: Deploy ke staging server via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.STAGING_HOST }}
username: ${{ secrets.STAGING_USER }}
key: ${{ secrets.STAGING_SSH_KEY }}
script: |
# Pull image terbaru
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:develop
# Update service dengan zero downtime
docker service update --image ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:develop --update-parallelism 1 --update-delay 10s myapp_api
# Verifikasi deployment berhasil
docker service ps myapp_api --filter desired-state=running
# ── Job 5: Deploy ke Production ────────────────────────────
deploy-production:
name: Deploy to Production
runs-on: ubuntu-latest
needs: build-and-push
if: startsWith(github.ref, 'refs/tags/v') # Hanya dari tag versi
environment:
name: production
url: https://myapp.com
steps:
- name: Extract version tag
id: version
run: echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
- name: Deploy ke production server via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.PROD_HOST }}
username: ${{ secrets.PROD_USER }}
key: ${{ secrets.PROD_SSH_KEY }}
script: |
set -e # Hentikan script jika ada error
# Pull image versi spesifik (bukan latest!)
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.tag }}
# Rolling update dengan rollback otomatis
docker service update --image ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.tag }} --update-parallelism 1 --update-delay 15s --update-failure-action rollback --rollback-parallelism 2 myapp_api
echo "Deployment ${{ steps.version.outputs.tag }} berhasil!" Docker dalam Pipeline GitLab CI/CD
GitLab CI/CD menawarkan integrasi Docker yang sangat erat, termasuk
registry container bawaan di setiap project. File konfigurasinya
bernama .gitlab-ci.yml
dan mendukung konsep stages yang membuat alur
pipeline sangat visual dan mudah dipahami.
# .gitlab-ci.yml
stages:
- test
- scan
- build
- deploy-staging
- deploy-production
variables:
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: "/certs"
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
IMAGE_LATEST: $CI_REGISTRY_IMAGE:latest
# ── Template reusable untuk Docker login ──────────────────────
.docker-login: &docker-login
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
# ── Stage 1: Test ──────────────────────────────────────────────
unit-test:
stage: test
image: docker:24-dind
services:
- docker:24-dind # Docker-in-Docker service
script:
- docker compose -f docker-compose.test.yml up
--build --abort-on-container-exit --exit-code-from test-runner
after_script:
- docker compose -f docker-compose.test.yml down -v
coverage: '/Liness*:s*(d+.?d*)%/' # Parse coverage dari output
# ── Stage 2: Security Scan ─────────────────────────────────────
trivy-scan:
stage: scan
image:
name: aquasec/trivy:latest
entrypoint: [""]
script:
- trivy image --exit-code 1
--severity CRITICAL
--no-progress
$IMAGE_TAG
needs: [unit-test]
allow_failure: false
# ── Stage 3: Build Image ───────────────────────────────────────
build-image:
stage: build
image: docker:24
services:
- docker:24-dind
<<: *docker-login
script:
- docker build
--build-arg BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
--build-arg GIT_COMMIT=$CI_COMMIT_SHA
--cache-from $IMAGE_LATEST
-t $IMAGE_TAG
-t $IMAGE_LATEST .
- docker push $IMAGE_TAG
- docker push $IMAGE_LATEST
only:
- main
- develop
- tags
# ── Stage 4: Deploy Staging ────────────────────────────────────
deploy-staging:
stage: deploy-staging
image: alpine:latest
before_script:
- apk add --no-cache openssh-client
- eval $(ssh-agent -s)
- echo "$STAGING_SSH_KEY" | ssh-add -
- mkdir -p ~/.ssh && chmod 700 ~/.ssh
- ssh-keyscan $STAGING_HOST >> ~/.ssh/known_hosts
script:
- ssh $STAGING_USER@$STAGING_HOST "
docker pull $IMAGE_TAG &&
docker service update --image $IMAGE_TAG myapp_api &&
docker service ps myapp_api"
environment:
name: staging
url: https://staging.myapp.com
only:
- develop
needs: [build-image]
# ── Stage 5: Deploy Production (Manual Gate) ──────────────────
deploy-production:
stage: deploy-production
image: alpine:latest
before_script:
- apk add --no-cache openssh-client
- eval $(ssh-agent -s)
- echo "$PROD_SSH_KEY" | ssh-add -
- mkdir -p ~/.ssh && chmod 700 ~/.ssh
- ssh-keyscan $PROD_HOST >> ~/.ssh/known_hosts
script:
- ssh $PROD_USER@$PROD_HOST "
docker pull $IMAGE_TAG &&
docker service update
--image $IMAGE_TAG
--update-failure-action rollback
myapp_api"
environment:
name: production
url: https://myapp.com
when: manual # Memerlukan persetujuan manual!
only:
- tags
needs: [build-image] Docker dalam Pipeline Jenkins
Jenkins tetap menjadi pilihan utama di banyak perusahaan enterprise yang membutuhkan fleksibilitas dan kontrol penuh atas infrastruktur CI/CD-nya. Dengan Jenkins Pipeline as Code menggunakan Jenkinsfile, seluruh konfigurasi pipeline disimpan bersama kode di repository.
// Jenkinsfile
pipeline {
agent any
environment {
REGISTRY = 'registry.mycompany.com'
IMAGE_NAME = 'myapp-api'
IMAGE_TAG = "${REGISTRY}/${IMAGE_NAME}:${BUILD_NUMBER}"
IMAGE_LATEST = "${REGISTRY}/${IMAGE_NAME}:latest"
REGISTRY_CRED = credentials('docker-registry-credentials')
}
options {
buildDiscarder(logRotator(numToKeepStr: '10'))
timeout(time: 30, unit: 'MINUTES')
disableConcurrentBuilds() // Cegah build paralel di branch yang sama
}
stages {
stage('Checkout') {
steps {
checkout scm
script {
GIT_COMMIT_SHORT = sh(
script: 'git rev-parse --short HEAD',
returnStdout: true
).trim()
}
}
}
stage('Test') {
steps {
sh '''
docker compose -f docker-compose.test.yml up \
--build \
--abort-on-container-exit \
--exit-code-from test-runner
'''
}
post {
always {
sh 'docker compose -f docker-compose.test.yml down -v'
junit 'test-results/**/*.xml' // Publish test results
publishCoverage adapters: [
istanbulCoberturaAdapter('coverage/cobertura-coverage.xml')
]
}
}
}
stage('Security Scan') {
steps {
sh "docker build -t scan-target:${BUILD_NUMBER} ."
sh """
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy image --exit-code 1 --severity CRITICAL,HIGH --no-progress scan-target:${BUILD_NUMBER}
"""
}
post {
always {
sh "docker rmi scan-target:${BUILD_NUMBER} || true"
}
}
}
stage('Build Image') {
steps {
script {
docker.withRegistry("https://${REGISTRY}", 'docker-registry-credentials') {
def appImage = docker.build(IMAGE_TAG, [
'--build-arg', "BUILD_DATE=${new Date().format('yyyy-MM-dd')}",
'--build-arg', "GIT_COMMIT=${GIT_COMMIT_SHORT}",
'--cache-from', IMAGE_LATEST,
'.'
].join(' '))
appImage.push()
appImage.push('latest')
}
}
}
}
stage('Deploy Staging') {
when {
branch 'develop'
}
steps {
sshagent(['staging-ssh-credentials']) {
sh """
ssh -o StrictHostKeyChecking=no \
${STAGING_USER}@${STAGING_HOST} \
'docker pull ${IMAGE_TAG} && \
docker service update --image ${IMAGE_TAG} myapp_api'
"""
}
}
}
stage('Deploy Production') {
when {
branch 'main'
}
input {
message "Deploy ke production?"
ok "Ya, Deploy Sekarang"
submitter "admin,devops-team" // Hanya role tertentu yang bisa approve
parameters {
string(name: 'DEPLOY_NOTE', description: 'Catatan deployment')
}
}
steps {
sshagent(['production-ssh-credentials']) {
sh """
ssh -o StrictHostKeyChecking=no \
${PROD_USER}@${PROD_HOST} \
'docker pull ${IMAGE_TAG} && \
docker service update \
--image ${IMAGE_TAG} \
--update-failure-action rollback \
myapp_api'
"""
}
}
}
}
post {
success {
slackSend(
color: 'good',
message: "✅ Deploy berhasil: ${IMAGE_TAG} oleh ${env.BUILD_USER}"
)
}
failure {
slackSend(
color: 'danger',
message: "❌ Pipeline gagal: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
)
}
always {
cleanWs() // Bersihkan workspace setelah selesai
}
}
} Otomatisasi Build, Test, dan Push Image ke Registry
Agar pipeline CI/CD Anda berjalan dengan konsisten dan cepat,
ada beberapa file pendukung yang perlu disiapkan. Yang pertama
adalah docker-compose.test.yml
— file Compose khusus untuk menjalankan test suite di lingkungan
yang terisolasi:
# docker-compose.test.yml
services:
test-runner:
build:
context: .
target: builder # Gunakan stage builder yang ada devDependencies
command: npm test
environment:
NODE_ENV: test
DB_HOST: test-db
REDIS_URL: redis://test-redis:6379
depends_on:
test-db:
condition: service_healthy
test-redis:
condition: service_started
volumes:
- test-results:/app/test-results # Simpan hasil test untuk CI
test-db:
image: postgres:16-alpine
environment:
POSTGRES_DB: testdb
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpass
tmpfs:
- /var/lib/postgresql/data # Database test di RAM, lebih cepat!
healthcheck:
test: ["CMD-SHELL", "pg_isready -U testuser -d testdb"]
interval: 5s
timeout: 3s
retries: 5
test-redis:
image: redis:7-alpine
tmpfs:
- /data # Redis test di RAM juga
volumes:
test-results: Selanjutnya, strategi image tagging yang konsisten adalah kunci dari pipeline yang dapat diaudit dan di-rollback dengan mudah. Berikut konvensi yang direkomendasikan:
# Konvensi tagging yang direkomendasikan:
# 1. Tag berdasarkan Git SHA (immutable, selalu unik)
ghcr.io/username/myapp:sha-a1b2c3d
# 2. Tag berdasarkan branch (bergerak, selalu menunjuk ke commit terbaru)
ghcr.io/username/myapp:main
ghcr.io/username/myapp:develop
# 3. Tag berdasarkan semver (untuk release resmi)
ghcr.io/username/myapp:v2.1.0
ghcr.io/username/myapp:v2.1
ghcr.io/username/myapp:v2
# 4. Tag latest (hanya untuk branch default)
ghcr.io/username/myapp:latest
# Aturan: Deploy ke production SELALU menggunakan SHA atau semver
# JANGAN deploy dengan tag :latest atau :main ke production
# karena nilainya bisa berubah dan tidak bisa di-rollback dengan pasti Deploy Otomatis Container ke Server Production
Langkah terakhir dari pipeline adalah mengantarkan image yang sudah diverifikasi ke server production. Ada dua pendekatan utama yang digunakan di industri, masing-masing dengan trade-off yang berbeda.
Pendekatan 1 — Push via SSH (Simpel, Cocok untuk Single Server)
# Script deploy yang dijalankan di server production via SSH
# deploy.sh — disimpan di server production
#!/bin/bash
set -euo pipefail
IMAGE=$1 # Argumen: full image tag yang akan di-deploy
SERVICE=$2 # Argumen: nama service Swarm
echo "🚀 Memulai deployment: $IMAGE"
# Pull image terbaru
docker pull $IMAGE
# Update service dengan zero downtime
docker service update --image $IMAGE --update-parallelism 1 --update-delay 10s --update-monitor 30s --update-failure-action rollback $SERVICE
# Tunggu dan verifikasi semua replica running
echo "⏳ Menunggu deployment selesai..."
sleep 15
RUNNING=$(docker service ps $SERVICE --filter desired-state=running --format "{{.CurrentState}}" | grep -c "Running" || true)
REPLICAS=$(docker service inspect $SERVICE --format "{{.Spec.Mode.Replicated.Replicas}}")
if [ "$RUNNING" -eq "$REPLICAS" ]; then
echo "✅ Deployment berhasil! $RUNNING/$REPLICAS replica running."
else
echo "❌ Deployment gagal! Hanya $RUNNING/$REPLICAS replica running."
docker service rollback $SERVICE
exit 1
fi Pendekatan 2 — GitOps dengan Watchtower (Pull-Based, Lebih Aman)
# Watchtower secara otomatis memantau registry dan update container
# ketika ada image baru — server production tidak perlu menerima koneksi SSH!
services:
watchtower:
image: containrrr/watchtower:latest
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /root/.docker/config.json:/config.json:ro # Kredensial registry
environment:
WATCHTOWER_POLL_INTERVAL: 30 # Cek setiap 30 detik
WATCHTOWER_CLEANUP: "true" # Hapus image lama setelah update
WATCHTOWER_INCLUDE_STOPPED: "false"
WATCHTOWER_SCOPE: myapp # Hanya monitor container dengan label ini
WATCHTOWER_NOTIFICATIONS: slack
WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL: ${SLACK_WEBHOOK_URL}
WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER: "Production Server"
api:
image: ghcr.io/username/myapp:latest
labels:
- com.centurylinklabs.watchtower.scope=myapp # Tandai untuk Watchtower
restart: unless-stopped - Simpan semua credential sensitif (SSH key, registry token, API key) sebagai encrypted secrets di GitHub Actions, GitLab CI Variables, atau Jenkins Credentials — jangan pernah hardcode di file pipeline.
- Terapkan approval gate manual sebelum deploy ke production — gunakan GitHub Environments, GitLab manual trigger, atau Jenkins input step untuk memastikan ada manusia yang memverifikasi sebelum kode masuk production.
- Gunakan docker-compose.test.yml dengan database di tmpfs (RAM) untuk mempercepat test suite secara dramatis — database test di RAM bisa 5-10x lebih cepat dibanding disk.
- Selalu verifikasi deployment dengan health check setelah update service — jangan anggap deployment berhasil hanya karena perintah docker service update tidak error.
- Implementasikan notifikasi ke Slack atau platform komunikasi tim untuk setiap event pipeline: build mulai, test gagal, deployment berhasil, dan rollback terjadi — agar seluruh tim aware terhadap status deployment.
Pipeline CI/CD yang terintegrasi dengan Docker adalah investasi yang memberikan return berlipat ganda: developer lebih produktif karena feedback loop lebih cepat, deployment lebih sering dengan risiko lebih kecil, dan masalah production terdeteksi jauh lebih awal. Dengan bekal lengkap ini, saatnya kita menyatukan semua konsep dalam sebuah studi kasus nyata — membangun dan men-deploy aplikasi full-stack modern dari nol menggunakan Docker dan CI/CD end-to-end.
Studi Kasus Nyata: Deploy Aplikasi Full-Stack dengan Docker & CI/CD
Saatnya menyatukan semua konsep yang telah dipelajari — Dockerfile, Docker Compose, networking, volume, security, dan CI/CD pipeline — ke dalam satu studi kasus yang komprehensif dan realistis. Kita akan membangun dan men-deploy sebuah aplikasi full-stack modern dari nol: backend Node.js (Express + TypeScript), frontend React (Vite), database PostgreSQL, cache Redis, dan reverse proxy Nginx — semuanya dikemas dalam container Docker dengan pipeline CI/CD otomatis ke VPS production.
Persiapan Proyek (Node.js + React + PostgreSQL)
Pertama, mari kita definisikan struktur direktori proyek yang rapi dan scalable. Struktur yang baik adalah fondasi dari proyek yang mudah dikelola dan di-onboard oleh developer baru.
myapp/
├── .github/
│ └── workflows/
│ └── cicd.yml # GitHub Actions pipeline
├── backend/
│ ├── src/
│ │ ├── routes/
│ │ ├── services/
│ │ ├── middleware/
│ │ └── server.ts
│ ├── tests/
│ ├── Dockerfile # Dockerfile backend
│ ├── package.json
│ └── tsconfig.json
├── frontend/
│ ├── src/
│ │ ├── components/
│ │ ├── pages/
│ │ └── main.tsx
│ ├── Dockerfile # Dockerfile frontend
│ ├── nginx.conf # Nginx config untuk serve React
│ └── package.json
├── nginx/
│ ├── nginx.conf # Nginx reverse proxy config
│ └── ssl/ # SSL certificates
├── db/
│ ├── migrations/ # SQL migration files
│ └── init.sql # Script inisialisasi database
├── monitoring/
│ ├── prometheus.yml
│ └── grafana/
├── docker-compose.yml # Production compose
├── docker-compose.dev.yml # Development override
├── docker-compose.test.yml # Testing compose
├── docker-stack.yml # Docker Swarm stack
├── .env.example # Template variabel environment
├── .dockerignore
└── Makefile # Shortcut perintah umum
Buat file Makefile
di root proyek untuk menyederhanakan perintah-perintah yang sering
digunakan. Ini adalah praktik yang sangat menghemat waktu dan
mengurangi human error:
# Makefile
.PHONY: dev dev-build test lint prod-up prod-down logs clean help
## Jalankan environment development dengan hot-reload
dev:
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d
## Build ulang semua image lalu jalankan development
dev-build:
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --build
## Jalankan seluruh test suite
test:
docker compose -f docker-compose.test.yml up --build --abort-on-container-exit
docker compose -f docker-compose.test.yml down -v
## Jalankan linter di semua service
lint:
docker compose run --rm backend npm run lint
docker compose run --rm frontend npm run lint
## Jalankan database migration
migrate:
docker compose run --rm backend npm run migrate
## Jalankan stack production
prod-up:
docker compose -f docker-compose.yml up -d
## Hentikan stack production
prod-down:
docker compose -f docker-compose.yml down
## Tampilkan logs semua service
logs:
docker compose logs -f
## Bersihkan semua container, image, dan volume yang tidak terpakai
clean:
docker compose down -v
docker system prune -f
## Tampilkan semua perintah yang tersedia
help:
@grep -E '^##' Makefile | sed 's/## //' Membuat Dockerfile untuk Setiap Service
Setiap service memiliki karakteristik build yang berbeda dan memerlukan Dockerfile yang dioptimalkan sesuai kebutuhannya. Kita akan menggunakan multi-stage build di semua service untuk memastikan image production sekecil dan seaman mungkin.
backend/Dockerfile — Node.js TypeScript API
# backend/Dockerfile
# ── Stage 1: Base dengan dependencies ─────────────────────────
FROM node:20-alpine AS base
WORKDIR /app
RUN addgroup -g 1001 -S appgroup && adduser -u 1001 -S appuser -G appgroup
# ── Stage 2: Install semua dependencies (termasuk devDeps) ────
FROM base AS deps
COPY package*.json ./
RUN npm ci
# ── Stage 3: Development (hot-reload dengan tsx watch) ────────
FROM base AS development
COPY --from=deps /app/node_modules ./node_modules
COPY . .
USER appuser
EXPOSE 3000 9229
CMD ["npm", "run", "dev"]
# ── Stage 4: Build TypeScript → JavaScript ────────────────────
FROM base AS builder
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build && npm prune --production # Hapus devDependencies setelah build
# ── Stage 5: Production (image final yang minimal) ────────────
FROM base AS production
ENV NODE_ENV=production
COPY --from=builder --chown=appuser:appgroup /app/dist ./dist
COPY --from=builder --chown=appuser:appgroup /app/node_modules ./node_modules
COPY --from=builder --chown=appuser:appgroup /app/package.json ./
USER appuser
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "dist/server.js"] frontend/Dockerfile — React + Vite
# frontend/Dockerfile
# ── Stage 1: Install dependencies ─────────────────────────────
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
# ── Stage 2: Build React app dengan Vite ──────────────────────
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Build argument untuk environment-specific config
ARG VITE_API_URL=http://localhost:3000
ARG VITE_APP_NAME=MyApp
ENV VITE_API_URL=${VITE_API_URL}
ENV VITE_APP_NAME=${VITE_APP_NAME}
RUN npm run build # Output ke /app/dist
# ── Stage 3: Serve dengan Nginx (image final ~25MB) ───────────
FROM nginx:1.25-alpine AS production
# Hapus default config Nginx
RUN rm /etc/nginx/conf.d/default.conf
# Salin custom Nginx config untuk React SPA
COPY nginx.conf /etc/nginx/conf.d/app.conf
# Salin hasil build dari stage builder
COPY --from=builder /app/dist /usr/share/nginx/html
# Script entrypoint untuk inject env vars ke runtime
COPY docker-entrypoint.sh /docker-entrypoint.d/40-inject-env.sh
RUN chmod +x /docker-entrypoint.d/40-inject-env.sh
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=5s --retries=3 CMD wget -qO- http://localhost/health || exit 1 frontend/nginx.conf — Konfigurasi Nginx untuk React SPA
# frontend/nginx.conf
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Gzip compression
gzip on;
gzip_types text/plain text/css application/json
application/javascript text/xml
application/xml image/svg+xml;
# Cache static assets aggressively
location ~* .(js|css|png|jpg|jpeg|gif|ico|svg|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Health check endpoint
location /health {
return 200 "ok";
add_header Content-Type text/plain;
}
# React SPA: semua route diarahkan ke index.html
location / {
try_files $uri $uri/ /index.html;
}
} Konfigurasi Docker Compose untuk Development
Kita membutuhkan dua file Compose yang bekerja bersama: file base
docker-compose.yml
yang mendefinisikan semua service, dan file override
docker-compose.dev.yml
yang menambahkan fitur khusus development seperti hot-reload,
bind mount source code, dan debugger port.
# docker-compose.yml — Base configuration
services:
# ── Nginx Reverse Proxy ──────────────────────────────────────
nginx:
image: nginx:1.25-alpine
container_name: myapp_nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
depends_on:
backend:
condition: service_healthy
frontend:
condition: service_healthy
networks:
- frontend-net
# ── Frontend React ───────────────────────────────────────────
frontend:
build:
context: ./frontend
target: production
args:
VITE_API_URL: ${VITE_API_URL:-/api}
container_name: myapp_frontend
restart: unless-stopped
networks:
- frontend-net
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost/health"]
interval: 30s
timeout: 5s
retries: 3
# ── Backend API ──────────────────────────────────────────────
backend:
build:
context: ./backend
target: production
container_name: myapp_backend
restart: unless-stopped
environment:
NODE_ENV: ${NODE_ENV:-production}
PORT: 3000
DB_HOST: db
DB_PORT: 5432
DB_NAME: ${DB_NAME:-myappdb}
DB_USER: ${DB_USER:-appuser}
DB_PASS: ${DB_PASS}
REDIS_URL: redis://:${REDIS_PASS}@redis:6379
JWT_SECRET: ${JWT_SECRET}
CORS_ORIGIN: ${CORS_ORIGIN:-http://localhost}
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
networks:
- frontend-net
- backend-net
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
start_period: 40s
retries: 3
# ── PostgreSQL Database ──────────────────────────────────────
db:
image: postgres:16-alpine
container_name: myapp_db
restart: unless-stopped
environment:
POSTGRES_DB: ${DB_NAME:-myappdb}
POSTGRES_USER: ${DB_USER:-appuser}
POSTGRES_PASSWORD: ${DB_PASS}
volumes:
- postgres_data:/var/lib/postgresql/data
- ./db/init.sql:/docker-entrypoint-initdb.d/01-init.sql:ro
networks:
- backend-net
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-appuser} -d ${DB_NAME:-myappdb}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
# ── Redis Cache ──────────────────────────────────────────────
redis:
image: redis:7-alpine
container_name: myapp_redis
restart: unless-stopped
command: redis-server --requirepass ${REDIS_PASS} --appendonly yes
volumes:
- redis_data:/data
networks:
- backend-net
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASS}", "ping"]
interval: 10s
timeout: 3s
retries: 3
networks:
frontend-net:
driver: bridge
backend-net:
driver: bridge
internal: true # Database tidak bisa diakses dari luar
volumes:
postgres_data:
redis_data: # docker-compose.dev.yml — Development override
services:
nginx:
ports:
- "80:80" # Tidak perlu 443 di development
volumes:
- ./nginx/nginx.dev.conf:/etc/nginx/nginx.conf:ro # Config development
frontend:
build:
target: development # Tidak ada stage ini, gunakan node langsung
image: node:20-alpine # Override image untuk dev server Vite
command: npm run dev -- --host 0.0.0.0
volumes:
- ./frontend:/app
- /app/node_modules # Jangan override node_modules dari host
ports:
- "5173:5173" # Vite dev server langsung (opsional)
environment:
VITE_API_URL: http://localhost/api
backend:
build:
target: development # Gunakan stage development dari Dockerfile
command: npm run dev
volumes:
- ./backend/src:/app/src # Hot-reload source code
- ./backend/tests:/app/tests
ports:
- "9229:9229" # Node.js debugger (untuk VS Code attach)
environment:
NODE_ENV: development
LOG_LEVEL: debug
db:
ports:
- "5432:5432" # Expose ke host untuk koneksi DB GUI (TablePlus, DBeaver)
redis:
ports:
- "6379:6379" # Expose ke host untuk debugging Menyiapkan Pipeline CI/CD End-to-End
Pipeline CI/CD untuk studi kasus ini akan menggabungkan semua yang telah dipelajari di section 11 ke dalam satu workflow yang dioptimalkan untuk monorepo multi-service. Setiap service hanya akan di-build ulang jika file di direktorinya berubah — menghemat waktu build secara signifikan.
# .github/workflows/cicd.yml
name: Full-Stack CI/CD
on:
push:
branches: [main, develop]
tags: ['v*.*.*']
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
BACKEND_IMAGE: ghcr.io/${{ github.repository }}/backend
FRONTEND_IMAGE: ghcr.io/${{ github.repository }}/frontend
jobs:
# ── Deteksi perubahan file untuk selective build ────────────
changes:
name: Detect Changed Services
runs-on: ubuntu-latest
outputs:
backend: ${{ steps.filter.outputs.backend }}
frontend: ${{ steps.filter.outputs.frontend }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
backend:
- 'backend/**'
- 'docker-compose.yml'
frontend:
- 'frontend/**'
- 'docker-compose.yml'
# ── Test Backend ────────────────────────────────────────────
test-backend:
name: Test Backend
runs-on: ubuntu-latest
needs: changes
if: needs.changes.outputs.backend == 'true'
steps:
- uses: actions/checkout@v4
- name: Run backend tests
run: |
docker compose -f docker-compose.test.yml run --rm test-runner npm run test:coverage
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
file: ./coverage/lcov.info
flags: backend
# ── Test Frontend ───────────────────────────────────────────
test-frontend:
name: Test Frontend
runs-on: ubuntu-latest
needs: changes
if: needs.changes.outputs.frontend == 'true'
steps:
- uses: actions/checkout@v4
- name: Run frontend tests
run: |
docker run --rm -v ${{ github.workspace }}/frontend:/app -w /app node:20-alpine sh -c "npm ci && npm run test:ci"
# ── Build dan Push Backend Image ────────────────────────────
build-backend:
name: Build Backend Image
runs-on: ubuntu-latest
needs: [test-backend]
if: |
always() &&
(needs.test-backend.result == 'success' ||
needs.test-backend.result == 'skipped')
permissions:
contents: read
packages: write
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/metadata-action@v5
id: meta
with:
images: ${{ env.BACKEND_IMAGE }}
tags: |
type=sha,prefix=sha-,format=short
type=ref,event=branch
type=semver,pattern={{version}}
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push backend
id: build
uses: docker/build-push-action@v5
with:
context: ./backend
target: production
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=gha,scope=backend
cache-to: type=gha,mode=max,scope=backend
# ── Build dan Push Frontend Image ───────────────────────────
build-frontend:
name: Build Frontend Image
runs-on: ubuntu-latest
needs: [test-frontend]
if: |
always() &&
(needs.test-frontend.result == 'success' ||
needs.test-frontend.result == 'skipped')
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/metadata-action@v5
id: meta
with:
images: ${{ env.FRONTEND_IMAGE }}
tags: |
type=sha,prefix=sha-,format=short
type=ref,event=branch
type=semver,pattern={{version}}
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push frontend
uses: docker/build-push-action@v5
with:
context: ./frontend
target: production
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
build-args: |
VITE_API_URL=/api
VITE_APP_NAME=MyApp
cache-from: type=gha,scope=frontend
cache-to: type=gha,mode=max,scope=frontend
# ── Deploy ke VPS Production ────────────────────────────────
deploy:
name: Deploy to Production VPS
runs-on: ubuntu-latest
needs: [build-backend, build-frontend]
if: startsWith(github.ref, 'refs/tags/v')
environment:
name: production
url: https://myapp.com
steps:
- uses: actions/checkout@v4
- name: Deploy via SSH
uses: appleboy/ssh-action@v1
env:
BACKEND_TAG: ${{ env.BACKEND_IMAGE }}:sha-${{ github.sha }}
FRONTEND_TAG: ${{ env.FRONTEND_IMAGE }}:sha-${{ github.sha }}
with:
host: ${{ secrets.PROD_HOST }}
username: ${{ secrets.PROD_USER }}
key: ${{ secrets.PROD_SSH_KEY }}
envs: BACKEND_TAG,FRONTEND_TAG
script: |
set -euo pipefail
cd /opt/myapp
# Pull image terbaru
docker pull $BACKEND_TAG
docker pull $FRONTEND_TAG
# Update environment variables
echo "BACKEND_IMAGE=$BACKEND_TAG" > .env.deploy
echo "FRONTEND_IMAGE=$FRONTEND_TAG" >> .env.deploy
# Rolling deploy dengan zero downtime
docker stack deploy --with-registry-auth --compose-file docker-stack.yml myapp
# Tunggu semua service stabil
sleep 20
docker stack ps myapp --filter desired-state=running
echo "✅ Deployment $BACKEND_TAG berhasil!" Deploy ke VPS/Cloud dengan Docker
Langkah terakhir adalah menyiapkan server production. Kita akan menggunakan sebuah VPS (bisa dari DigitalOcean, Hetzner, Vultr, atau AWS EC2) dan mengonfigurasinya dari awal menjadi server production yang aman dan siap menerima deployment.
Script Inisialisasi Server (Jalankan Sekali di VPS Baru)
#!/bin/bash
# server-setup.sh — Jalankan sebagai root di VPS baru
set -euo pipefail
echo "🔧 Memulai konfigurasi server production..."
# ── 1. Update sistem ─────────────────────────────────────────
apt update && apt upgrade -y
apt install -y curl wget git ufw fail2ban
# ── 2. Buat user deployment non-root ─────────────────────────
useradd -m -s /bin/bash deploy
usermod -aG docker deploy
mkdir -p /home/deploy/.ssh
cp /root/.ssh/authorized_keys /home/deploy/.ssh/
chown -R deploy:deploy /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keys
# ── 3. Install Docker Engine ──────────────────────────────────
curl -fsSL https://get.docker.com | sh
systemctl enable docker
systemctl start docker
# ── 4. Konfigurasi Docker daemon untuk production ─────────────
cat > /etc/docker/daemon.json << 'EOF'
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "5",
"compress": "true"
},
"live-restore": true,
"userland-proxy": false,
"no-new-privileges": true
}
EOF
systemctl restart docker
# ── 5. Inisialisasi Docker Swarm ──────────────────────────────
PRIVATE_IP=$(hostname -I | awk '{print $1}')
docker swarm init --advertise-addr $PRIVATE_IP
# ── 6. Buat direktori aplikasi ────────────────────────────────
mkdir -p /opt/myapp
chown deploy:deploy /opt/myapp
# ── 7. Setup firewall UFW ─────────────────────────────────────
ufw default deny incoming
ufw default allow outgoing
ufw allow ssh
ufw allow 80/tcp
ufw allow 443/tcp
ufw allow 2377/tcp # Docker Swarm manager
ufw --force enable
# ── 8. Hardening SSH ──────────────────────────────────────────
sed -i 's/#PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config
sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
systemctl restart sshd
echo "✅ Server siap! Login sebagai user 'deploy' untuk deployment selanjutnya." Nginx Reverse Proxy Config di Server (SSL + HTTP/2)
# nginx/nginx.conf — Production reverse proxy
worker_processes auto;
events { worker_connections 1024; }
http {
# ── Security headers ──────────────────────────────────────
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# ── Gzip compression ──────────────────────────────────────
gzip on;
gzip_comp_level 6;
gzip_types text/plain text/css application/json
application/javascript text/xml application/xml
image/svg+xml font/woff2;
# ── Rate limiting ─────────────────────────────────────────
limit_req_zone $binary_remote_addr zone=api:10m rate=30r/m;
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
# ── Upstream services ─────────────────────────────────────
upstream backend {
server backend:3000;
keepalive 32;
}
upstream frontend {
server frontend:80;
keepalive 16;
}
# ── Redirect HTTP → HTTPS ─────────────────────────────────
server {
listen 80;
server_name myapp.com www.myapp.com;
return 301 https://$host$request_uri;
}
# ── Main HTTPS server ─────────────────────────────────────
server {
listen 443 ssl http2;
server_name myapp.com www.myapp.com;
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_session_cache shared:SSL:10m;
client_max_body_size 20M;
# ── API routes → Backend ──────────────────────────────
location /api/ {
limit_req zone=api burst=10 nodelay;
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_read_timeout 60s;
}
# ── Rate limit khusus endpoint login ─────────────────
location /api/auth/login {
limit_req zone=login burst=3 nodelay;
proxy_pass http://backend;
}
# ── Frontend React SPA ────────────────────────────────
location / {
proxy_pass http://frontend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
}
}
} - Gunakan selective build di CI dengan tools seperti dorny/paths-filter — hanya rebuild service yang benar-benar berubah untuk memotong waktu pipeline dari 15 menit menjadi 3-5 menit.
- Pisahkan cache GitHub Actions per service dengan scope yang berbeda (scope=backend, scope=frontend) agar cache tidak saling invalidate dan build tetap cepat meski hanya satu service yang berubah.
- Buat user deploy non-root khusus di server production dan berikan hanya izin yang diperlukan — jangan gunakan root untuk deployment otomatis dari CI/CD.
- Terapkan rate limiting di Nginx untuk semua endpoint API dan batasi lebih ketat lagi untuk endpoint autentikasi — ini adalah pertahanan pertama terhadap brute force dan abuse.
- Gunakan live-restore: true di Docker daemon config agar container tetap berjalan saat Docker daemon di-restart atau di-upgrade, tanpa downtime pada aplikasi.
Dengan menyelesaikan studi kasus ini, Anda telah mempraktikkan seluruh spektrum Docker dari awal hingga akhir: struktur proyek yang scalable, multi-stage Dockerfile per service, konfigurasi Compose yang terpisah per environment, pipeline CI/CD yang cerdas dengan selective build, server setup yang aman, hingga Nginx production-grade dengan SSL dan rate limiting. Ini bukan sekadar teori — ini adalah arsitektur yang digunakan oleh tim engineering profesional di seluruh dunia. Selanjutnya, kita akan membahas bagaimana mengatasi masalah-masalah yang paling sering dijumpai saat bekerja dengan Docker di section troubleshooting.
Troubleshooting Docker yang Sering Terjadi
Bahkan developer Docker yang paling berpengalaman pun tidak lepas dari masalah — container yang tiba-tiba tidak mau start, koneksi antar service yang putus, image yang membengkak tanpa alasan jelas, atau port yang tiba-tiba konflik dengan proses lain. Yang membedakan developer junior dengan senior bukan pada apakah mereka menghadapi masalah, melainkan pada seberapa cepat dan sistematis mereka mendiagnosisnya. Section ini adalah panduan troubleshooting komprehensif untuk masalah-masalah Docker yang paling sering ditemui — lengkap dengan cara mendiagnosis penyebab akar dan solusi yang sudah terbukti.
Container Tidak Bisa Start: Penyebab dan Solusi
Container yang gagal start adalah masalah paling umum yang dihadapi.
Gejalanya bervariasi: container langsung berstatus
Exited
setelah dijalankan, status terus berputar antara
Restarting
dan Exited,
atau perintah docker run
langsung mengembalikan error. Berikut adalah langkah diagnosis
sistematis dari yang paling dasar:
Langkah 1 — Periksa Status dan Exit Code
# Lihat status semua container termasuk yang sudah berhenti
docker ps -a
# Output yang perlu diperhatikan:
# STATUS: Exited (1) — exit code 1 = error umum aplikasi
# STATUS: Exited (127) — perintah tidak ditemukan (command not found)
# STATUS: Exited (137) — container di-kill paksa (OOMKilled atau SIGKILL)
# STATUS: Exited (139) — segmentation fault
# STATUS: Exited (143) — container menerima SIGTERM (graceful shutdown)
# Cek apakah container di-kill karena kehabisan memory (OOMKilled)
docker inspect mycontainer --format '{{.State.OOMKilled}}'
# Output: true = container di-kill karena OOM → naikkan memory limit
# Lihat detail state container secara lengkap
docker inspect mycontainer --format '{{json .State}}' | python3 -m json.tool Langkah 2 — Baca Log Container
# Baca log dari container yang sudah berhenti
docker logs mycontainer
# Tampilkan log dengan timestamp untuk korelasi waktu
docker logs --timestamps mycontainer
# Tampilkan hanya 50 baris terakhir (untuk log yang sangat panjang)
docker logs --tail 50 mycontainer
# Jika container restart loop, tangkap log sebelum restart berikutnya
docker logs -f mycontainer 2>&1 | tee /tmp/container-debug.log Langkah 3 — Debug dengan Shell Interaktif
# Override CMD dengan shell untuk investigasi manual
docker run -it --entrypoint sh myapp:latest
# Jika menggunakan bash (tidak semua image punya bash)
docker run -it --entrypoint bash myapp:latest
# Periksa file dan struktur direktori di dalam image
ls -la /app
cat /app/package.json
which node && node --version
# Coba jalankan CMD secara manual untuk lihat error aslinya
node dist/server.js
# Debug image tanpa entrypoint (untuk image yang sangat minimal)
docker run -it --entrypoint sh --user root myapp:latest Penyebab Umum dan Solusinya
- Exit code 127 (command not found): File yang direferensikan di CMD atau ENTRYPOINT tidak ada di dalam image. Verifikasi path dengan masuk ke shell container dan cek keberadaan file tersebut.
- Exit code 1 dengan error 'permission denied': Proses non-root mencoba menulis ke direktori yang dimiliki root. Perbaiki dengan COPY --chown atau RUN chown di Dockerfile, atau tambahkan volume yang dimiliki user yang benar.
- Container restart loop: Aplikasi crash karena dependensi (database, cache) belum siap. Solusi: tambahkan depends_on dengan condition: service_healthy, atau implementasikan retry logic di aplikasi.
- Exit code 137 (OOMKilled): Container kehabisan memory dan di-kill oleh kernel. Naikkan memory limit atau investigasi memory leak di aplikasi dengan docker stats.
Masalah Koneksi Antar Container
Masalah networking adalah kategori kedua yang paling sering
ditemui setelah container gagal start. Error seperti
ECONNREFUSED,
getaddrinfo ENOTFOUND,
atau Connection timed out
semuanya menunjukkan masalah di lapisan jaringan. Pendekatan
diagnosis yang sistematis akan menghemat jam-jam debugging yang
tidak produktif.
# ── Diagnosis Network ─────────────────────────────────────────
# 1. Verifikasi kedua container ada di network yang sama
docker network inspect myapp-network
# Lihat bagian "Containers" — pastikan semua container yang
# perlu berkomunikasi terdaftar di network yang sama
# 2. Cek network yang dimiliki masing-masing container
docker inspect backend --format '{{json .NetworkSettings.Networks}}' | python3 -m json.tool
# 3. Test DNS resolution dari dalam container
docker exec backend nslookup db
docker exec backend nslookup redis
# Jika NXDOMAIN → container tidak ada di network yang sama
# atau nama service salah
# 4. Test konektivitas TCP secara langsung
docker exec backend wget -qO- http://db:5432 2>&1 || true
docker exec backend nc -zv db 5432 # Netcat TCP check
docker exec backend ping db -c 3 # ICMP ping
# 5. Lihat semua network yang ada
docker network ls
# 6. Lihat container mana yang terhubung ke network tertentu
docker network inspect bridge
docker network inspect myapp-network Toolkit Diagnostik Jaringan di Alpine Container
# Banyak image production menggunakan Alpine yang sangat minimal
# dan tidak memiliki tools seperti curl, ping, netcat, atau nslookup.
# Jalankan container debug sementara di network yang sama:
docker run --rm -it --network myapp-network --name debug-tools nicolaka/netshoot # Image khusus berisi semua network tools
bash
# Di dalam container nicolaka/netshoot, tersedia:
ping db
nslookup db
curl http://db:5432
nc -zv redis 6379
tcpdump -i eth0 port 5432 # Capture traffic untuk analisis mendalam
ss -tlnp # Lihat port yang sedang listen
traceroute db
# Alternatif: install tools sementara di container yang bermasalah
docker exec -u root backend sh -c "apk add --no-cache curl iputils && ping db -c 3" Masalah Umum dan Solusinya
- getaddrinfo ENOTFOUND db: Container backend tidak berada di network yang sama dengan container db. Pastikan keduanya terdaftar di network yang sama di docker-compose.yml atau gunakan docker network connect.
- Connection refused di port yang benar: Container target berjalan tapi aplikasinya belum listen. Ini sering terjadi saat database masih dalam proses inisialisasi. Tambahkan healthcheck dan depends_on condition: service_healthy.
- Bisa ping tapi tidak bisa HTTP: Firewall atau iptables rules memblokir traffic. Cek dengan docker network inspect dan pastikan tidak ada custom iptables rules yang konflik.
- Network internal tidak bisa akses internet: Anda menggunakan internal: true di network definition. Ini disengaja — pindahkan container yang butuh akses internet ke network yang tidak internal.
Image Size Terlalu Besar
Image yang gemuk memperlambat setiap bagian dari workflow Anda: build lebih lama, push ke registry lebih lambat, pull di server production memakan waktu lebih banyak, dan lebih banyak storage yang terpakai. Diagnosis dimulai dengan memahami apa yang sebenarnya ada di dalam image Anda.
# ── Diagnosis Ukuran Image ────────────────────────────────────
# Lihat ukuran semua image
docker images --format "table {{.Repository}} {{.Tag}} {{.Size}}" | sort -k3 -h
# Analisis layer demi layer — temukan layer mana yang paling berat
docker history myapp:latest
docker history --no-trunc myapp:latest # Tampilkan command lengkap
# Tool visual yang lebih powerful: dive
# Install: https://github.com/wagoodman/dive
dive myapp:latest
# dive menampilkan setiap layer, ukurannya, dan file yang ada
# Efficiency score < 90% menandakan ada optimasi yang bisa dilakukan
# Ekspor image dan analisis isinya secara manual
docker save myapp:latest | tar -tv | sort -k5 -rh | head -20
# Tampilkan file terbesar di dalam image Solusi: Checklist Optimasi Ukuran Image
# ── Sebelum: Dockerfile yang tidak dioptimasi (~900MB) ────────
FROM node:20 # ❌ Image full dengan semua tools
WORKDIR /app
COPY . . # ❌ Salin semua file termasuk node_modules
RUN npm install # ❌ Install semua dependency
RUN apt-get update && apt-get install -y vim curl # ❌ Tools debug masuk production
CMD ["npm", "start"]
# ── Sesudah: Dockerfile yang dioptimasi (~120MB) ──────────────
FROM node:20-alpine AS deps # ✅ Alpine base (~7MB vs ~350MB)
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production # ✅ Hanya production dependencies
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:20-alpine AS production # ✅ Multi-stage: mulai bersih
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
# ✅ Tidak ada source code, test file, atau devDependencies
CMD ["node", "dist/server.js"] - Gunakan .dockerignore yang komprehensif — pastikan node_modules, .git, coverage, *.log, .env, dan direktori build tidak ikut masuk ke build context yang dikirim ke Docker daemon.
- Gabungkan perintah RUN dengan && dan selalu bersihkan package cache di baris yang sama: RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*. Membersihkan di RUN terpisah tidak akan mengurangi ukuran layer.
- Pilih base image yang tepat: node:20-alpine (~170MB) vs node:20-slim (~240MB) vs node:20 (~1.1GB). Untuk Go, gunakan FROM scratch untuk binary yang self-contained.
- Gunakan tool 'dive' untuk mengaudit isi setiap layer secara visual — ini adalah cara tercepat menemukan file besar yang tidak seharusnya ada di image production.
Port Already in Use dan Cara Mengatasinya
Error Bind for 0.0.0.0:80 failed: port is already allocated
atau listen tcp 0.0.0.0:5432: bind: address already in use
adalah salah satu error yang paling sering ditemui, terutama
setelah restart sistem atau ketika ada sisa container dari
sesi sebelumnya yang belum dibersihkan.
# ── Diagnosis: Temukan Siapa yang Menggunakan Port ────────────
# Di Linux/macOS: cari proses yang menggunakan port tertentu
sudo lsof -i :80
sudo lsof -i :5432
sudo ss -tlnp | grep :80 # Alternatif dengan ss (lebih modern)
# Di Windows (PowerShell):
# netstat -ano | findstr :80
# tasklist /fi "PID eq <PID>"
# Cek apakah ada container Docker yang masih menggunakan port
docker ps -a --format "table {{.Names}} {{.Status}} {{.Ports}}"
# Cek container yang berstatus Exited tapi masih "reserving" port
# (ini terjadi jika container stopped tapi tidak di-rm)
docker ps -a | grep Exited # ── Solusi ────────────────────────────────────────────────────
# Solusi 1: Hentikan dan hapus container yang memakai port
docker stop $(docker ps -q --filter "publish=80")
docker rm $(docker ps -aq --filter "publish=80")
# Solusi 2: Bersihkan semua container yang sudah berhenti
docker container prune -f
# Solusi 3: Ganti port mapping ke port lain yang tersedia
docker run -p 8080:80 nginx # Gunakan port 8080 sebagai alternatif
# Solusi 4: Jika proses host (bukan Docker) yang menggunakan port
sudo systemctl stop apache2 # Hentikan Apache jika ada
sudo systemctl stop nginx # Hentikan Nginx host jika ada
sudo kill -9 $(sudo lsof -t -i:80) # Force kill proses di port 80
# Solusi 5: Reset network Docker jika ada konflik jaringan yang kompleks
docker network prune -f
sudo systemctl restart docker # Restart Docker daemon (last resort) Mencegah Konflik Port di Development
# Gunakan range port yang tidak umum untuk development
# agar tidak konflik dengan service sistem
services:
nginx:
ports:
- "8080:80" # Bukan 80 (sering dipakai Apache/Nginx host)
backend:
ports:
- "3001:3000" # Bukan 3000 (sering dipakai app lain)
db:
ports:
- "5433:5432" # Bukan 5432 (PostgreSQL host mungkin sudah jalan)
redis:
ports:
- "6380:6379" # Bukan 6379 (Redis host mungkin sudah jalan)
# Atau gunakan variabel environment untuk fleksibilitas
services:
nginx:
ports:
- "${HOST_PORT_HTTP:-8080}:80"
- "${HOST_PORT_HTTPS:-8443}:443" Permission Denied pada Volume Mount
Error permission denied
pada volume mount adalah masalah yang sering terjadi ketika
menggunakan non-root user di container — yang merupakan praktik
keamanan yang benar, namun memerlukan perhatian ekstra pada
manajemen file ownership. Akar masalahnya selalu sama: UID/GID
user di dalam container tidak cocok dengan kepemilikan file
atau direktori di host.
# ── Diagnosis ────────────────────────────────────────────────
# Cek UID/GID user di dalam container
docker exec mycontainer id
# Output: uid=1001(appuser) gid=1001(appgroup) groups=1001(appgroup)
# Cek kepemilikan direktori di host yang di-mount
ls -la ./data/
# Output: drwxr-xr-x 2 root root 4096 ...
# Masalah: direktori dimiliki root, tapi container jalan sebagai UID 1001
# Cek kepemilikan volume Docker
docker volume inspect mydata
# Lihat "Mountpoint" lalu: ls -la /var/lib/docker/volumes/mydata/_data # ── Solusi 1: Ubah kepemilikan direktori di host ─────────────
sudo chown -R 1001:1001 ./data/ # Sesuaikan dengan UID di container
sudo chmod -R 755 ./data/
# ── Solusi 2: Perbaiki kepemilikan di Dockerfile ──────────────
FROM node:20-alpine
RUN addgroup -g 1001 -S appgroup && adduser -u 1001 -S appuser -G appgroup
WORKDIR /app
RUN mkdir -p /app/data /app/logs && chown -R appuser:appgroup /app # Pastikan semua direktori dimiliki appuser
COPY --chown=appuser:appgroup . .
USER appuser
# ── Solusi 3: Init container untuk set permission ─────────────
services:
app:
image: myapp:latest
volumes:
- app_data:/app/data
depends_on:
init-permissions:
condition: service_completed_successfully
init-permissions:
image: alpine:latest
user: root # Jalankan sebagai root untuk chown
volumes:
- app_data:/data
command: >
sh -c "chown -R 1001:1001 /data && chmod -R 755 /data && echo 'Done'"
restart: "no" # Hanya jalankan sekali
volumes:
app_data: # ── Solusi 4: Bind mount dengan user mapping ──────────────────
# Jalankan container dengan UID/GID yang sama dengan user host
docker run -d --user $(id -u):$(id -g) # Gunakan UID/GID user saat ini
-v $(pwd)/data:/app/data myapp
# Di docker-compose.yml
services:
app:
image: myapp:latest
user: "${UID:-1000}:${GID:-1000}" # Dari variabel environment
volumes:
- ./data:/app/data
# Set di .env atau shell sebelum docker compose up:
# export UID=$(id -u)
# export GID=$(id -g)
# ── Solusi 5: Khusus masalah SELinux di RHEL/CentOS/Fedora ───
# SELinux memblokir akses container ke bind mount secara default
# Tambahkan label :z (shared) atau :Z (private) pada volume
docker run -d -v $(pwd)/data:/app/data:z # :z = shared SELinux label
myapp
# Di docker-compose.yml
volumes:
- ./data:/app/data:z - Selalu tentukan UID dan GID secara eksplisit saat membuat user di Dockerfile (adduser -u 1001) daripada membiarkan sistem memilih secara otomatis — ini memastikan konsistensi antara berbagai mesin dan environment.
- Untuk bind mount di Linux, UID user di dalam container harus cocok dengan UID pemilik direktori di host. Cara paling mudah: gunakan --user $(id -u):$(id -g) saat development.
- Di macOS dan Windows, masalah permission pada bind mount jarang terjadi karena Docker Desktop menggunakan layer virtualisasi yang menangani pemetaan UID secara otomatis.
- Jika menggunakan named volume (bukan bind mount), buat init container yang menjalankan chown sebagai root sebelum container aplikasi start — ini adalah pola yang bersih dan tidak memerlukan perubahan di host.
Toolkit Diagnostik Umum Docker
Selain masalah-masalah spesifik di atas, berikut adalah kumpulan perintah diagnostik yang berguna untuk berbagai situasi darurat lainnya — simpan sebagai referensi cepat saat menghadapi masalah yang tidak terduga di production.
# ── Informasi Sistem Docker ───────────────────────────────────
docker info # Info lengkap Docker daemon
docker system df # Penggunaan disk Docker
docker system df -v # Detail per image/container/volume
docker system events # Stream event Docker secara real-time
docker system events --since 1h # Event dari 1 jam terakhir
# ── Bersihkan Resource yang Tidak Terpakai ─────────────────────
docker system prune # Hapus semua resource unused
docker system prune -a # Termasuk image yang tidak ada container
docker system prune -a --volumes # Termasuk volume (HATI-HATI di production!)
docker system prune -a --filter "until=24h" # Hanya yang lebih dari 24 jam
# ── Debug Container yang Berjalan ─────────────────────────────
docker exec mycontainer env # Lihat semua env vars
docker exec mycontainer cat /etc/hosts # Lihat /etc/hosts di container
docker exec mycontainer df -h # Penggunaan disk di container
docker exec mycontainer free -m # Penggunaan memory di container
docker exec mycontainer ps aux # Proses yang berjalan
# ── Analisis Network ──────────────────────────────────────────
docker network ls --no-trunc
docker network inspect $(docker network ls -q) # Inspect semua network
# ── Export dan Import Container ───────────────────────────────
# Simpan state container yang berjalan sebagai image baru (untuk debugging)
docker commit mycontainer debug-snapshot:latest
docker save debug-snapshot:latest | gzip > debug-snapshot.tar.gz
# ── Cek Docker Daemon Log ─────────────────────────────────────
# Linux dengan systemd:
sudo journalctl -u docker.service -f
sudo journalctl -u docker.service --since "1 hour ago"
# Docker Desktop di macOS:
# ~/Library/Containers/com.docker.docker/Data/log/host/com.docker.driver.amd64-linux.log Kemampuan troubleshooting yang solid adalah hal yang membedakan deployment yang stabil dari deployment yang selalu "maintenance". Dengan memahami exit codes, menguasai diagnosis jaringan, mengoptimasi ukuran image, menangani konflik port, dan mengelola permission dengan benar, Anda memiliki bekal untuk mengatasi hampir semua masalah Docker yang akan ditemui di dunia nyata. Di section terakhir, kita akan merangkum seluruh perjalanan ini dan memetakan langkah selanjutnya yang bisa Anda ambil untuk terus berkembang di ekosistem container dan cloud-native.
Kesimpulan
Rangkuman Perjalanan Belajar Docker
Selamat — Anda telah menyelesaikan panduan lengkap Docker dari nol hingga production! Dimulai dari memahami apa itu container dan mengapa Docker mengubah cara dunia membangun software, hingga merancang pipeline CI/CD otomatis yang men-deploy aplikasi full-stack ke server production tanpa downtime. Ini bukan perjalanan yang pendek, dan setiap konsep yang Anda pelajari di sini adalah keterampilan nyata yang digunakan oleh tim engineering profesional di seluruh dunia setiap harinya.
- Docker adalah platform containerisasi yang memungkinkan aplikasi berjalan secara konsisten di environment mana pun — menghilangkan masalah klasik 'works on my machine' selamanya.
- Dockerfile adalah resep image yang mendefinisikan environment aplikasi sebagai kode, sementara Docker Compose menyederhanakan pengelolaan aplikasi multi-container menjadi satu perintah.
- Multi-stage build adalah teknik wajib untuk menghasilkan image production yang ramping dan aman — memisahkan stage build yang berat dari image final yang hanya berisi apa yang benar-benar diperlukan.
- Docker Networking dengan user-defined bridge memberikan DNS resolution otomatis antar container, sementara network segmentation memastikan isolasi yang aman antara lapisan frontend dan backend.
- Named volume adalah cara yang tepat untuk menyimpan data persisten seperti database di production, dilengkapi strategi backup dan restore menggunakan container helper yang elegan.
- Keamanan container bukan afterthought — non-root user, read-only filesystem, drop Linux capabilities, dan vulnerability scanning adalah lapisan pertahanan yang wajib diterapkan sebelum go-live.
- Docker Swarm memberikan orkestrasi multi-host dengan high availability, rolling update zero-downtime, dan self-healing otomatis tanpa kompleksitas operasional Kubernetes.
- Pipeline CI/CD yang terintegrasi dengan Docker mengotomatiskan seluruh perjalanan dari commit kode hingga running di production — memungkinkan tim merilis software berkali-kali dalam sehari dengan kepercayaan penuh.
- Troubleshooting Docker yang efektif dimulai dari membaca exit code, menginspeksi log, memverifikasi network, hingga mengaudit layer image — pendekatan sistematis menghemat jam-jam debugging yang tidak produktif.
Langkah Selanjutnya
Roadmap Lanjutan: Kubernetes, Helm, dan Cloud-Native
Docker adalah gerbang menuju ekosistem cloud-native yang jauh lebih luas. Setelah menguasai fondasi di panduan ini, berikut adalah roadmap yang direkomendasikan untuk melanjutkan perjalanan Anda:
- Kubernetes (K8s): Pelajari orkestrator container terpopuler di dunia — mulai dari Pod, Deployment, Service, Ingress, hingga ConfigMap dan Secret. Gunakan minikube atau kind untuk belajar di lokal.
- Helm: Package manager untuk Kubernetes yang memungkinkan Anda mendeploy aplikasi kompleks dengan satu perintah menggunakan chart yang sudah dikonfigurasi dan dapat di-versioning.
- GitOps dengan ArgoCD atau Flux: Paradigma deployment di mana Git repository adalah satu-satunya source of truth — setiap perubahan di repository otomatis tersinkronisasi ke cluster Kubernetes.
- Service Mesh dengan Istio atau Linkerd: Lapisan infrastruktur yang menangani komunikasi antar microservices — termasuk mTLS otomatis, traffic management, circuit breaker, dan distributed tracing.
- Managed Kubernetes di Cloud: Eksplorasi AWS EKS, Google GKE, atau Azure AKS untuk pengalaman mengelola cluster Kubernetes di infrastruktur cloud yang sesungguhnya.
- Observability Stack: Pelajari OpenTelemetry untuk distributed tracing, Loki untuk log aggregation, dan Grafana untuk visualisasi metrik — membangun visibilitas penuh ke dalam sistem terdistribusi.
Referensi
Resource Belajar Docker Terbaik
- Dokumentasi Resmi Docker (docs.docker.com): Sumber paling akurat dan selalu up-to-date. Referensi pertama yang harus dibuka sebelum mencari jawaban di tempat lain.
- Play with Docker (labs.play-with-docker.com): Lingkungan Docker gratis langsung di browser — ideal untuk bereksperimen tanpa perlu instalasi lokal, tersedia selama 4 jam per sesi.
- Docker Captain Blog Posts: Tulisan dari para Docker Captain (komunitas expert Docker resmi) yang membahas topik lanjutan dan best practice dari pengalaman production nyata.
- KodeKloud dan Linux Foundation: Platform kursus dengan lab interaktif berbasis terminal untuk belajar Docker, Kubernetes, dan DevOps secara hands-on dengan sertifikasi yang diakui industri.
- GitHub Awesome-Docker (github.com/veggiemonk/awesome-docker): Koleksi kurasi tools, tutorial, dan resource Docker terlengkap yang dikelola komunitas open-source global.
Docker bukan tujuan akhir — ia adalah fondasi. Setiap konsep yang Anda kuasai hari ini membuka pintu ke lapisan teknologi berikutnya yang lebih dalam dan lebih powerful. Mulailah dengan satu container, satu Dockerfile, satu pipeline. Iterasi secara konsisten, dan dalam waktu yang tidak terlalu lama Anda akan membangun infrastruktur yang sama dengan yang dijalankan perusahaan teknologi terbesar di dunia. Selamat bereksperimen! 🚀