Masalah Klasik: "Di Laptop Gue Jalan"
Pernah denger ini?
"Kok error? Di laptop gue jalan kok."
Ini bukan cuma meme. Ini masalah nyata yang terjadi hampir di setiap tim development.
Kenapa bisa jalan di laptop A tapi ga jalan di laptop B? Atau jalan di development tapi crash di production?
- Versi Node.js beda (laptop: v18, server: v16)
- Library system yang beda versi
- Environment variable yang lupa di-set
- OS yang beda (dev di Mac, server Linux)
- Dependency yang ga ke-install
Semua masalah ini bermuara ke satu hal: environment-nya beda.
Container lahir untuk menyelesaikan ini.
Apa itu Container?
Container itu paket yang berisi aplikasi beserta semua yang dibutuhkan untuk jalan — kode, runtime, library, system tools, konfigurasi. Semuanya dibungkus jadi satu unit yang bisa jalan di mana aja.
Bayangin kamu pindahan rumah. Daripada angkut barang satu-satu (dan berharap semuanya muat di truk yang beda-beda), kamu masukin semuanya ke dalam kontainer pengiriman standar. Kontainer itu bisa dinaikkin ke truk, kapal, atau kereta api — ga peduli transportasinya apa, isi kontainer tetap aman dan utuh.
Itu exactly apa yang Docker container lakukan untuk software.
Container vs Virtual Machine
"Tapi kan udah ada VM (Virtual Machine)? Kenapa butuh container?"
Pertanyaan bagus. Keduanya menyelesaikan masalah isolasi, tapi caranya beda.
graph TB
subgraph Virtual Machine
H1[Hardware]
HV[Hypervisor]
subgraph VM1[VM 1]
OS1[Guest OS - 2GB+]
L1[Libraries]
A1[App A]
end
subgraph VM2[VM 2]
OS2[Guest OS - 2GB+]
L2[Libraries]
A2[App B]
end
H1 --> HV --> VM1
HV --> VM2
end
graph TB
subgraph Container
H2[Hardware]
HOS[Host OS]
DE[Container Engine - Docker]
subgraph C1[Container 1]
CL1[Libraries]
CA1[App A]
end
subgraph C2[Container 2]
CL2[Libraries]
CA2[App B]
end
H2 --> HOS --> DE --> C1
DE --> C2
end
Perbedaan utama:
| Aspek | VM | Container |
|---|---|---|
| OS | Setiap VM punya Guest OS sendiri | Sharing Host OS kernel |
| Ukuran | Gigabytes (OS + app) | Megabytes (app + dependencies aja) |
| Startup | Menit (harus boot OS) | Detik (langsung jalan) |
| Resource | Boros (setiap VM allocate CPU/RAM fixed) | Efisien (sharing kernel, overhead minimal) |
| Isolasi | Kuat (OS-level isolation) | Cukup kuat (process-level isolation) |
Container jauh lebih ringan karena ga perlu bawa OS sendiri. Dia cuma bawa aplikasi dan dependency-nya, lalu pakai kernel dari host OS.
Tapi VM punya isolasi lebih kuat karena setiap VM benar-benar terpisah di level hardware. Untuk workload yang butuh security isolation tinggi (multi-tenant, regulatory compliance), VM masih lebih cocok.
Banyak setup production sekarang pakai keduanya: container jalan di atas VM. VM untuk isolasi tenant, container untuk packaging aplikasi.
Gimana Docker Kerja?
Docker itu container runtime yang paling populer. Tapi Docker bukan satu-satunya — ada Podman, containerd, CRI-O. Docker cuma yang paling dikenal.
Komponen Docker
graph TB
CLI["Docker CLI (docker build, run, push)"]
Daemon["Docker Daemon (dockerd)"]
Registry["Container Registry (Docker Hub, GCR, ECR)"]
CLI -->|REST API| Daemon
Daemon -->|pull/push images| Registry
Daemon -->|manage| Containers["Running Containers"]
Daemon -->|build/store| Images["Docker Images"]
- Docker CLI: perintah yang kamu ketik (
docker run,docker build, dll) - Docker Daemon: service yang running di background, manage container dan image
- Docker Image: template read-only yang berisi blueprint container
- Docker Container: instance yang running dari image
- Docker Registry: tempat nyimpen dan share image (Docker Hub, GCR, ECR)
Analoginya: Image itu resep masakan, Container itu makanan yang udah dimasak. Dari satu resep bisa masak banyak porsi.
Dockerfile: Blueprint Container
Dockerfile itu file yang berisi instruksi step-by-step untuk bikin Docker image.
Contoh Dockerfile sederhana (Node.js)
# 1. Base image — mulai dari mana
FROM node:20-alpine
# 2. Set working directory di dalam container
WORKDIR /app
# 3. Copy dependency files dulu (biar bisa di-cache)
COPY package*.json ./
# 4. Install dependencies
RUN npm ci --omit=dev
# 5. Copy source code
COPY . .
# 6. Expose port
EXPOSE 3000
# 7. Perintah untuk jalankan app
CMD ["node", "server.js"]
Setiap baris itu satu instruksi yang bikin satu layer di image.
Layer System
Ini konsep penting di Docker. Setiap instruksi di Dockerfile bikin layer baru.
graph TB
L1["Layer 1: FROM node:20-alpine (base OS + Node.js)"]
L2["Layer 2: COPY package*.json (dependency files)"]
L3["Layer 3: RUN npm ci (installed dependencies)"]
L4["Layer 4: COPY . . (source code)"]
L1 --> L2 --> L3 --> L4
style L1 fill:#1a5276,color:#fff
style L2 fill:#1a6e4e,color:#fff
style L3 fill:#7d6608,color:#fff
style L4 fill:#922b21,color:#fff
Kenapa layer penting?
Caching: Docker cache setiap layer. Kalau layer ga berubah, ga perlu di-rebuild. Makanya kita
COPY package*.jsondulu sebelumCOPY . .— kalau source code berubah tapi dependencies ga berubah, layer npm install jadi ke-cache.Sharing: Dua image yang pakai base
node:20-alpineyang sama cuma perlu download base layer sekali. Layer yang sama di-share antar image.Efisiensi: Build lebih cepat, storage lebih hemat.
Image vs Container
Ini sering bikin bingung:
| Konsep | Penjelasan | Analoginya |
|---|---|---|
| Dockerfile | Instruksi untuk bikin image | Resep masakan |
| Image | Template read-only, hasil build | Cetakan kue |
| Container | Instance running dari image | Kue yang udah jadi |
| Registry | Tempat simpan dan share image | Toko cetakan kue |
Dari satu image, bisa bikin banyak container:
graph LR
I["Image: myapp:1.0"]
I --> C1["Container 1 (port 3001)"]
I --> C2["Container 2 (port 3002)"]
I --> C3["Container 3 (port 3003)"]
Setiap container punya filesystem sendiri, process sendiri, network sendiri. Mereka terisolasi satu sama lain meskipun berasal dari image yang sama.
Docker di Balik Layar: Teknologi Linux
Docker bukan magic. Dia pakai fitur-fitur Linux kernel yang udah ada:
1. Namespaces — Isolasi
Namespaces bikin container merasa dia punya sistem sendiri.
| Namespace | Isolasi Apa |
|---|---|
| PID | Process IDs — container ga bisa lihat proses host |
| NET | Network — container punya IP sendiri |
| MNT | Filesystem — container punya root filesystem sendiri |
| UTS | Hostname — container punya hostname sendiri |
| IPC | Inter-process communication |
| USER | User IDs — root di container ≠ root di host |
Container yang running sebagai PID 1 di dalamnya, di host bisa jadi PID 4829. Dari dalam container, dia merasa dia satu-satunya yang jalan. Padahal dia cuma satu dari banyak proses di host.
2. Cgroups — Resource Limits
Control Groups ngebatesin berapa resource yang boleh dipake container.
docker run --memory=512m --cpus=1.5 myapp
Tanpa cgroups, satu container bisa makan semua RAM host dan bikin container lain (atau host sendiri) crash. Cgroups mencegah ini.
- Memory: container melebihi limit → di-kill (OOMKilled)
- CPU: container dibatesin pemakaian CPU-nya
- I/O: disk read/write bisa di-throttle
3. Union Filesystem — Layered Storage
Ini yang bikin layer system Docker bisa kerja. Union filesystem (OverlayFS di Linux modern) nge-stack multiple layers jadi satu filesystem yang terlihat unified.
graph TB
subgraph Union Filesystem
RW["Read-Write Layer (container changes)"]
L3["Layer 3: Source code (read-only)"]
L2["Layer 2: npm packages (read-only)"]
L1["Layer 1: Node.js + Alpine (read-only)"]
end
RW --> L3 --> L2 --> L1
Image layers itu read-only. Saat container jalan, Docker tambahin satu read-write layer di atas. Semua perubahan yang dilakukan container (buat file baru, edit file) masuk ke layer ini.
Container dihapus → read-write layer hilang → image tetap bersih.
Networking di Docker
Container perlu komunikasi — ke dunia luar, dan ke container lain.
Network Modes
graph TB
Internet["🌐 Internet"]
Host["Host Machine<br/>eth0: 192.168.1.100"]
Internet --- Host
subgraph BridgeNet["Bridge Network (default)"]
B[docker0 bridge<br/>172.17.0.1]
C1["Container A<br/>172.17.0.2"]
C2["Container B<br/>172.17.0.3"]
C1 --- B
C2 --- B
end
Host --- B
- Bridge (default): container dapet IP di subnet internal Docker. Akses keluar lewat NAT. Antar container bisa komunikasi lewat bridge
- Host: container pakai network stack host langsung. Ga ada isolasi network, tapi performance lebih bagus
- None: ga ada network. Untuk container yang ga perlu network access
- Overlay: untuk multi-host networking (Docker Swarm, Kubernetes)
Port Mapping
Container punya network sendiri. Supaya bisa diakses dari luar:
docker run -p 8080:3000 myapp
Artinya: traffic ke host port 8080 diterusin ke container port 3000.
graph LR
User["User Browser<br/>localhost:8080"] -->|port 8080| Host[Host Machine]
Host -->|mapped to 3000| Container["Container<br/>App listening on :3000"]
DNS antar Container
Di Docker network yang sama, container bisa saling reference pakai nama:
# docker-compose.yml
services:
api:
image: myapi
depends_on:
- db
db:
image: postgres
Container api bisa akses database pakai hostname db — Docker handle DNS resolution-nya.
Volume: Data yang Persist
Ingat, container itu ephemeral. Container dihapus = data hilang.
Gimana kalau butuh data yang persist? (database, uploaded files, logs)
Tipe Volume
graph TB
subgraph Docker Host
V1["Named Volume<br/>/var/lib/docker/volumes/mydata"]
V2["Bind Mount<br/>/home/user/project"]
V3["tmpfs Mount<br/>(RAM only)"]
end
C1[Container] -->|named volume| V1
C2[Container] -->|bind mount| V2
C3[Container] -->|tmpfs| V3
| Tipe | Cara Kerja | Use Case |
|---|---|---|
| Named Volume | Docker manage lokasi storage-nya | Database data, persistent app data |
| Bind Mount | Map folder host langsung ke container | Development (live reload source code) |
| tmpfs | Data di RAM, hilang saat container stop | Sensitive data yang ga boleh ke disk |
Contoh
# Named volume — Docker manage path-nya
docker run -v mydata:/var/lib/postgresql/data postgres
# Bind mount — map folder lokal
docker run -v ./src:/app/src myapp
# tmpfs — data di RAM aja
docker run --tmpfs /tmp myapp
Yang paling sering dipakai di development: bind mount (supaya edit kode di host langsung ke-detect di container). Di production: named volume untuk database data.
Docker Compose: Multi-Container Setup
Real-world app jarang cuma satu container. Biasanya ada: app, database, cache, reverse proxy, dll.
Manage satu-satu pakai docker run? Ribet.
Docker Compose bikin kamu bisa define semua services dalam satu file YAML dan jalanin bareng.
Contoh docker-compose.yml
services:
app:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://user:pass@db:5432/myapp
- REDIS_URL=redis://cache:6379
depends_on:
- db
- cache
volumes:
- ./src:/app/src # live reload di development
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: myapp
volumes:
- pgdata:/var/lib/postgresql/data # persist data
ports:
- "5432:5432"
cache:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
pgdata: # named volume declaration
Satu command, semuanya jalan:
# Start semua services
docker compose up -d
# Lihat status
docker compose ps
# Lihat logs
docker compose logs -f app
# Stop semua
docker compose down
# Stop dan hapus volumes (hati-hati, data hilang!)
docker compose down -v
depends_on memastikan db dan cache start duluan sebelum app. Tapi ingat: depends_on cuma nunggu container start, bukan nunggu service ready. PostgreSQL bisa sudah start tapi belum siap terima koneksi. Untuk itu, butuh health check.
Best Practices
1. Pakai base image yang kecil
# ❌ Jangan
FROM node:20 # ~900MB
# ✅ Pakai alpine
FROM node:20-alpine # ~130MB
# ✅✅ Atau distroless (paling minimal)
FROM gcr.io/distroless/nodejs20 # ~120MB, tanpa shell
Image yang lebih kecil = pull lebih cepat, attack surface lebih kecil, disk usage lebih hemat.
2. Multi-stage build
Pisahkan build environment dan runtime environment:
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Production runtime
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/server.js"]
Stage 1 punya semua dev dependencies (TypeScript compiler, build tools, dll). Stage 2 cuma punya hasil build dan production dependencies. Image production jadi jauh lebih kecil.
3. Jangan jalankan sebagai root
# Bikin user non-root
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
# Switch ke user itu
USER appuser
Container yang jalan sebagai root itu risiko keamanan. Kalau container di-exploit, attacker punya akses root (meskipun di container, ada risiko container escape).
4. Satu container, satu concern
# ❌ Jangan: app + database + nginx di satu container
# ✅ Pisahkan jadi 3 container
Satu container harusnya jalanin satu proses/concern. Kenapa?
- Scaling: bisa scale app tanpa scale database
- Isolation: crash di satu ga affect yang lain
- Updates: bisa update app tanpa restart database
5. Pakai .dockerignore
# .dockerignore
node_modules
.git
.env
*.md
dist
.next
Tanpa .dockerignore, COPY . . bakal copy semuanya termasuk node_modules (yang mau di-install ulang anyway), .git (bisa ratusan MB), file .env (secrets!). Buang waktu dan bisa bocorkan data sensitif.
6. Pin versi, jangan pakai latest
# ❌ Ga tahu versi apa yang ke-pull
FROM node:latest
# ✅ Versi spesifik, reproducible
FROM node:20.11-alpine
latest bisa berubah kapan aja. Build hari ini bisa beda sama build besok. Ini bikin debugging susah dan deployment ga predictable.
Security Considerations
Image Scanning
Image bisa mengandung vulnerability. Scan sebelum deploy:
# Docker native scanning
docker scout cves myapp:1.0
# Trivy (open source, popular)
trivy image myapp:1.0
Secrets Management
# ❌ JANGAN hardcode secrets di Dockerfile
ENV DATABASE_PASSWORD=mysecretpassword
# ❌ JANGAN copy .env file ke image
COPY .env .
# ✅ Inject saat runtime
docker run -e DATABASE_PASSWORD=secret myapp
# ✅✅ Atau pakai Docker secrets / external secret manager
Apapun yang di-embed di image bisa dilihat oleh siapa aja yang punya akses ke image. docker history bisa reveal environment variables.
Least Privilege
- Jalankan sebagai non-root user
- Drop Linux capabilities yang ga diperlukan
- Pakai read-only filesystem kalau bisa
- Limit resource (memory, CPU)
docker run \
--user 1000:1000 \
--read-only \
--memory=256m \
--cpus=0.5 \
--cap-drop=ALL \
myapp
Container di Production
Container ≠ Production-Ready
Container memudahkan packaging dan deployment, tapi ada banyak hal lain yang perlu diurus untuk production:
- Orchestration: siapa yang manage ratusan container? → Kubernetes, Docker Swarm
- Monitoring: gimana tahu container mana yang bermasalah? → Prometheus, Grafana
- Logging: logs dari 50 container ke mana? → ELK Stack, Loki
- Networking: service discovery, load balancing antar container → Kubernetes Service, Traefik
- CI/CD: gimana automate build dan deploy image? → GitHub Actions, GitLab CI
Docker Compose cukup untuk small-scale production atau single-host deployment. Untuk multi-host, butuh orchestrator seperti Kubernetes.
Container Registry
Jangan cuma store image di lokal. Push ke registry supaya bisa di-pull dari server production:
# Build dan tag
docker build -t myapp:1.0 .
# Tag untuk registry
docker tag myapp:1.0 registry.example.com/myapp:1.0
# Push ke registry
docker push registry.example.com/myapp:1.0
Popular registries: Docker Hub, GitHub Container Registry (ghcr.io), AWS ECR, Google GCR, Azure ACR.
Ringkasan
| Konsep | Penjelasan |
|---|---|
| Container | Paket isolated yang berisi app + semua dependency |
| Image | Template read-only, blueprint untuk container |
| Dockerfile | Instruksi step-by-step untuk bikin image |
| Layer | Setiap instruksi Dockerfile bikin layer yang bisa di-cache |
| Namespaces | Isolasi proses, network, filesystem container |
| Cgroups | Limit resource (CPU, RAM) per container |
| Volume | Persistent storage yang survive container restart |
| Docker Compose | Manage multi-container setup via YAML file |
| Multi-stage build | Pisah build environment dan runtime untuk image kecil |
| Registry | Tempat simpan dan distribusi images |
Kesimpulan
Docker mengubah cara kita develop, test, dan deploy software. Intinya:
- Container = consistency — "di laptop gue jalan" bukan masalah lagi karena environment-nya identik
- Docker bukan VM — container sharing kernel host, jauh lebih ringan
- Layer system itu kunci — pahami caching supaya build cepat
- Security bukan optional — scan images, jangan root, jangan hardcode secrets
- Docker Compose untuk multi-service — satu YAML, satu command
- Container bukan solusi lengkap — di production butuh orchestration, monitoring, CI/CD
Docker itu foundation. Setelah paham Docker, langkah selanjutnya belajar Kubernetes — karena di dunia nyata, kamu ga manage 3 container. Kamu manage ratusan. Dan itu butuh orchestrator.
Tapi itu cerita untuk lain waktu.
