Mengemas service menjadi artefak portable dengan Docker: menulis Dockerfile yang benar untuk Node.js, strategi optimasi ukuran image, prinsip image minimal dan non-root, serta mengorkestrasi banyak service dengan docker-compose

Di episode 12-13 kalian mendesain arsitektur dan gerbang sistem. Sekarang tiba saatnya mengemas aplikasi menjadi artefak yang bisa dideploy di mana saja — inilah yang membuat "berjalan di mesin saya" tidak lagi jadi alasan. Container mengunci runtime, dependency, dan konfigurasi dalam satu image, sehingga apa yang berjalan di laptop kalian sama persis dengan yang berjalan di server.
Mengapa ini penting? Karena konsistensi environment adalah akar dari sebagian besar masalah deployment: "di lokal jalan, di server tidak". Docker menyelesaikan ini dengan mengemas semuanya. Episode ini membangun Dockerfile yang benar, strategi optimasi image, dan docker-compose untuk mengorkestrasi service toko online kita.
Mari bangun image untuk API Node.js kita. Perhatikan strategi multi-stage build: tahap build men-download dan meng-compile, tahap runtime hanya mengambil hasilnya — image akhir tetap kecil.
# --- Tahap 1: build ---
FROM node:22-alpine AS build
WORKDIR /app
# 1. Salin hanya file dependency dulu (cache layer efektif)
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
# 2. Salin source dan build (TypeScript)
COPY tsconfig.json ./
COPY src ./src
RUN bun run build
# --- Tahap 2: runtime ---
FROM node:22-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
# 3. Hanya production dependencies
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile --production
# 4. Hasil build, tanpa source & tanpa tool dev
COPY --from=build /app/dist ./dist
# 5. Non-root user (keamanan)
RUN addgroup -S app && adduser -S app -G app
USER app
EXPOSE 3000
CMD ["node", "dist/server.js"]Docker mem-build layer demi layer dan meng-cache layer yang tidak berubah. Dengan menyalin package.json terlebih dahulu, perubahan kode tidak memicu re-install dependency — build jadi jauh lebih cepat. Urutan di atas adalah urutan optimal: dependency dulu, source belakangan.
Ukuran image memengaruhi waktu push/pull dan biaya storage. Tiga strategi paling efektif:
Base node:22-alpine (~50 MB) jauh lebih kecil dari node:22 full. Multi-stage membuang tool build dari image final — image kita berisi hanya hasil build dan production dependencies.
node_modules
dist
.git
.env
*.lognode_modules di-rebuild di dalam image, jadi tidak perlu disalin. .env jangan pernah masuk image — itu secrets (episode 17).
Jika memakai Go/Rust, image bisa jadi se-ramping beberapa MB — hanya binary tanpa runtime sama sekali. Inilah salah satu alasan Go populer untuk microservices (episode 3).
Note
Lihat ukuran image dengan docker images. Image production yang sehat untuk API Node: 100-200 MB (Node + dependencies). Jika melebihi 500 MB, ada yang salah — cek apakah tool dev ikut ter-copy atau base image terlalu gemuk.
Dua kebijakan keamanan wajib dalam container:
curl, bash, atau package manager di image production.USER app.FROM python:3.12-alpine AS runtime
WORKDIR /app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN addgroup -S app && adduser -S app -G app
USER app
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]Aplikasi nyata tidak berdiri sendiri — API butuh PostgreSQL, Redis, dan RabbitMQ (dari episode 0, 9, 10). docker-compose mendefinisikan seluruh stack dalam satu file dan menjalankannya bersama.
services:
api:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://dev:dev@db:5432/shop
REDIS_URL: redis://redis:6379
RABBITMQ_URL: amqp://guest:guest@rabbitmq:5672
depends_on:
- db
- redis
- rabbitmq
restart: unless-stopped
db:
image: postgres:16
environment:
POSTGRES_USER: dev
POSTGRES_PASSWORD: dev
POSTGRES_DB: shop
volumes:
- pgdata:/var/lib/postgresql/data
redis:
image: redis:7-alpine
rabbitmq:
image: rabbitmq:3-management
ports:
- "15672:15672"
volumes:
pgdata:docker compose up -d --build
docker compose ps
docker compose logs -f apidepends_on menjamin urutan start, dan volumes: pgdata membuat data database tetap hidup meski container di-restart.
Tip
Untuk developer, compose adalah "production mini" — environment yang sama persis dengan server. Biasakan: kalau sesuatu jalan di compose, hampir pasti jalan di produksi. Mulai dari sini sebelum menyentuh Kubernetes — 90% tim tidak akan pernah butuh K8s (episode 15).
Container harus bisa diperiksa kesehatannya dan pulih sendiri:
services:
api:
build: .
healthcheck:
test: ["CMD", "node", "-e", "fetch('http://localhost:3000/health').catch(() => process.exit(1))"]
interval: 30s
timeout: 5s
retries: 3
restart: unless-stoppedrestart: unless-stopped membuat container hidup kembali setelah crash — mekanisme self-healing pertama sebelum orkestrator besar.
Env file berisi secrets. Jangan pernah menyalinnya ke image; inject saat runtime via environment (episode 15 dan 17).
Di dalam container, aplikasi harus listen di 0.0.0.0 — bukan 127.0.0.1 — karena port dipetakan dari host. Ini penyebab paling umum "sudah expose tapi tidak bisa diakses".
Container berjalan root = risiko keamanan besar. Selalu USER app.
Tanpa .dockerignore, Docker mengirim seluruh folder proyek (termasuk node_modules dan .git) sebagai build context — build lambat dan memori boros.
Warning
Prinsip immutable image: image yang sudah di-build tidak boleh diubah dengan cara "masuk dan edit file". Kalau ada perubahan, bangun image baru. Image immutability adalah fondasi rollback yang aman — jika versi baru rusak, deploy versi lama, bukan "memperbaiki" yang berjalan.
Episode 14 mengemas service menjadi artefak portable: Dockerfile multi-stage yang efisien, optimasi ukuran image, kebijakan non-root, dan docker-compose untuk menjalankan seluruh stack toko online.
Inti yang harus dibawa pulang:
.env.depends_on, volume, healthcheck, dan restart policy.Di episode 15 selanjutnya kita akan mengirim image itu ke produksi: deployment & CI/CD backend — GitHub Actions, environment, dan zero-downtime deployment. Sampai jumpa di episode 15!