DevOps & Cloud

Docker Adalah: Panduan Lengkap dari Instalasi hingga CI/CD

Panduan lengkap Docker untuk pemula dan praktisi DevOps. Membahas konsep Docker, container, instalasi, Docker Compose, microservices, hingga CI/CD secara mendalam.

M Muhammad Mabruri 23 Februari 2026
Docker untuk DevOps dan CI/CD
Menghitung... 23 Februari 2026
Daftar Isi

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! 🚀

FAQ

Pertanyaan yang Sering Diajukan

Temukan jawaban atas pertanyaan umum di bawah ini.

Apa perbedaan utama antara Docker Container dan Virtual Machine?
Container berbagi kernel OS dengan host sehingga jauh lebih ringan (hanya beberapa MB) dan bisa dijalankan dalam hitungan detik. Virtual Machine menjalankan sistem operasi penuh di atas hypervisor sehingga membutuhkan memori dan storage besar (beberapa GB). Docker unggul dalam kecepatan dan efisiensi resource, sementara VM menawarkan isolasi yang lebih kuat.
Apakah data di dalam container akan hilang jika container dihapus?
Ya, secara default semua data yang ditulis di dalam container bersifat ephemeral — akan hilang saat container dihapus. Untuk menyimpan data secara persisten, gunakan Docker Volume (named volume untuk production seperti database) atau Bind Mount (untuk development dengan hot-reload). Data di volume tidak akan terhapus meski containernya dihapus.
Berapa banyak container yang bisa dijalankan dalam satu server?
Tidak ada batasan hard limit dari Docker sendiri. Jumlah container yang bisa berjalan bergantung pada resource server: CPU, RAM, dan storage. Karena container jauh lebih ringan dari VM, satu server dengan 8GB RAM bisa menjalankan puluhan container sekaligus. Gunakan resource limiting (--memory dan --cpus) untuk memastikan satu container tidak menghabiskan semua resource.
Apa perbedaan antara CMD dan ENTRYPOINT di Dockerfile?
CMD mendefinisikan perintah default yang bisa di-override saat docker run — misalnya docker run myapp npm test akan mengganti CMD yang ada. ENTRYPOINT mendefinisikan perintah utama yang tidak bisa di-override, hanya bisa ditambah argumen. Pola terbaik adalah kombinasi keduanya: ENTRYPOINT untuk executable utama dan CMD untuk argumen defaultnya.
Kapan sebaiknya menggunakan Docker Compose vs Docker Swarm?
Gunakan Docker Compose untuk mengelola multi-container di satu host — ideal untuk development lokal, testing, dan aplikasi skala kecil hingga menengah. Gunakan Docker Swarm ketika Anda membutuhkan orkestrasi di beberapa server sekaligus, high availability, dan rolling update zero-downtime di production. Swarm adalah langkah natural setelah outgrow single-host Docker Compose.
Bagaimana cara terbaik menyimpan secret dan password di Docker?
Jangan pernah hardcode secret di Dockerfile atau image. Untuk development, gunakan file .env yang tidak di-commit ke Git. Untuk production single-host, inject via environment variable saat runtime. Untuk Docker Swarm, gunakan Docker Secrets yang menyimpan data sensitif secara terenkripsi dan hanya dapat diakses oleh service yang diberi izin eksplisit.
Mengapa ukuran Docker image saya sangat besar dan bagaimana mengecilkannya?
Penyebab umum image besar: menggunakan base image yang terlalu besar (node:20 vs node:20-alpine), menyalin node_modules dari host, tidak menghapus cache package manager, dan tidak menggunakan multi-stage build. Solusinya: gunakan varian -alpine sebagai base image, terapkan multi-stage build untuk memisahkan stage build dari image final, buat .dockerignore yang komprehensif, dan gabungkan perintah RUN dengan && untuk meminimalkan jumlah layer.
Apa itu Docker Hub dan apakah aman menggunakan image dari sana?
Docker Hub adalah registry container publik terbesar yang menyimpan jutaan image. Image dengan label 'Official Image' (seperti nginx, postgres, node) dikelola langsung oleh vendor dan tim Docker — relatif aman digunakan. Untuk image komunitas, selalu periksa jumlah download, rating, dan tanggal update terakhir. Sebaiknya selalu scan image dengan Trivy atau Docker Scout sebelum digunakan di production.
Bagaimana Docker menangani komunikasi antar container?
Container yang berada di user-defined bridge network yang sama dapat berkomunikasi langsung menggunakan nama container sebagai hostname — Docker menyediakan DNS resolution otomatis. Ini berbeda dari default bridge network yang hanya mendukung komunikasi via IP address. Di Docker Compose, semua service otomatis terhubung ke satu network dan bisa saling memanggil berdasarkan nama service.
Apakah Docker bisa digunakan di production atau hanya untuk development?
Docker sangat layak dan bahkan sangat direkomendasikan untuk production. Perusahaan seperti Google, Netflix, Spotify, dan ribuan startup menggunakan Docker di production setiap harinya. Kunci suksesnya: gunakan non-root user, terapkan resource limiting, aktifkan health check, scan vulnerability secara berkala, dan integrasikan dengan pipeline CI/CD untuk deployment yang otomatis dan konsisten.

Siap Mengembangkan Sistem Digital Anda?

Percayakan pengembangan website, aplikasi mobile, dan ERP bisnis Anda kepada tim profesional kami.