Dockerfile multi-stage
Dockerfile yang salah urutan membuat setiap perubahan satu baris kode memicu instalasi dependensi dari nol. Urutannya yang menentukan, bukan isinya.
Intisari
- Multi-stage: build di satu stage, salin hasilnya saja ke stage runtime.
- Salin berkas lock lebih dulu, install, baru salin kode โ supaya cache layer bekerja.
- Jangan pernah berjalan sebagai
root. Image Node punya penggunanode. - Sematkan versi image dasar.
node:22bergerak;node:22.14-alpinetidak. .dockerignoresama pentingnya dengan Dockerfile โ tanpanyanode_moduleslokalmu ikut terkirim.
Dockerfile
#syntax=docker/dockerfile:1.9
# โโ Stage 1: dependensi โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
FROM node:22.14-alpine AS deps
WORKDIR /app
RUN corepack enable pnpm
# Berkas lock DULU: layer ini hanya dibangun ulang kalau dependensi berubah.
COPY package.json pnpm-lock.yaml .npmrc ./
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
pnpm install --frozen-lockfile
# โโ Stage 2: build โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
FROM node:22.14-alpine AS build
WORKDIR /app
RUN corepack enable pnpm
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN pnpm astro check
RUN pnpm build
# Buang dependensi dev โ hanya yang produksi yang ikut.
RUN pnpm prune --prod
# โโ Stage 3: runtime โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
FROM node:22.14-alpine AS runtime
WORKDIR /app
RUN apk add --no-cache tini && \
addgroup -g 10001 app && \
adduser -u 10001 -G app -s /bin/sh -D app
ENV NODE_ENV=production \
HOST=0.0.0.0 \
PORT=4321 \
NODE_OPTIONS="--max-old-space-size=512"
COPY --from=build --chown=app:app /app/dist ./dist
COPY --from=build --chown=app:app /app/node_modules ./node_modules
COPY --from=build --chown=app:app /app/server.mjs ./server.mjs
COPY --from=build --chown=app:app /app/package.json ./package.json
USER app
EXPOSE 4321
# tini sebagai PID 1: meneruskan SIGTERM dengan benar dan menuai proses zombie.
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "server.mjs"]
tini menyelesaikan masalah yang membatalkan graceful shutdown-mu. Proses PID 1 di
Linux punya penanganan sinyal khusus: kalau tidak ada handler eksplisit, sinyal diabaikan.
Node sebagai PID 1 bisa tidak menerima SIGTERM sama sekali โ jadi seluruh kode shutdown
yang kamu tulis di materi sebelumnya tidak pernah berjalan, dan ECS akhirnya mengirim
SIGKILL. Alternatifnya: initProcessEnabled: true di task definition ECS.
Urutan layer
SALAH โ tiap perubahan kode memicu install ulang:
COPY . .
RUN pnpm install
BENAR โ install hanya kalau lockfile berubah:
COPY package.json pnpm-lock.yaml ./
RUN pnpm install
COPY . .
| Perubahan | Salah | Benar |
|---|---|---|
| Satu baris di komponen | ~3 menit | ~30 detik |
| Menambah dependensi | ~3 menit | ~3 menit |
.dockerignore
node_modules
dist
.git
.github
.env
.env.*
!.env.example
*.log
.astro
.vscode
coverage
db/dump-*.sql
README.md
Tanpa .dockerignore, node_modules lokalmu ikut ke konteks build. Itu
bisa ratusan megabita yang dikirim ke daemon Docker setiap build โ dan lebih buruk, biner yang
dikompilasi untuk macOS bisa menimpa yang benar di dalam image. Baris .env juga bukan
formalitas: tanpanya rahasia produksimu masuk ke layer image dan bisa dibaca siapa pun yang menarik
image itu.
Membangun dan mendorong ke ECR
AKUN=123456789012
REGION=ap-southeast-3
REPO=$AKUN.dkr.ecr.$REGION.amazonaws.com/portal
TAG=$(git rev-parse --short HEAD)
aws ecr get-login-password --region $REGION \
| docker login --username AWS --password-stdin "$AKUN.dkr.ecr.$REGION.amazonaws.com"
docker buildx build \
--platform linux/arm64 \
--cache-from type=registry,ref=$REPO:cache \
--cache-to type=registry,ref=$REPO:cache,mode=max \
-t $REPO:$TAG \
--push .
Pakai linux/arm64 dan jalankan di Fargate Graviton. Harganya sekitar 20% lebih
murah dengan performa setara atau lebih baik untuk beban Node. Yang perlu diperhatikan: bangun di runner
ARM juga โ membangun ARM lewat emulasi QEMU di runner x86 bisa lima kali lebih lambat.
Dan jangan pernah memakai tag latest. Tag ber-SHA git membuat kamu tahu persis
komit mana yang berjalan, dan rollback berarti men-deploy tag lama โ bukan berharap
latest masih menunjuk ke tempat yang benar.
Verifikasi image sebelum deploy
# Ukuran
docker images $REPO:$TAG --format '{{.Size}}'
# Target wajar: 150โ250 MB untuk Astro + Vue + mysql2
# Tidak berjalan sebagai root
docker run --rm $REPO:$TAG id
# uid=10001(app) gid=10001(app)
# Tidak ada rahasia di dalam layer
docker history --no-trunc $REPO:$TAG | grep -iE 'password|secret|key' && echo BAHAYA
# Pindai kerentanan
trivy image --severity HIGH,CRITICAL --exit-code 1 $REPO:$TAG
# Jalankan dan uji secara lokal
docker run --rm -p 4321:4321 --env-file .env $REPO:$TAG &
sleep 3
curl -sf http://localhost:4321/sehat || echo "health check GAGAL"
Batas memori
ENV NODE_OPTIONS="--max-old-space-size=512"
Setel batas heap Node di bawah batas memori container. Tanpa itu, Node akan terus menumbuhkan heap-nya sampai kernel membunuh container dengan OOM โ dan itu terjadi tanpa satu pun baris log. Yang kamu lihat hanya task yang mati dan digantikan. Dengan batas heap yang lebih rendah dari batas container, Node melakukan pengumpulan sampah lebih agresif dan, kalau memang kehabisan, melempar error yang bisa kamu baca.
Aturan praktis: max-old-space-size sekitar 60โ70% dari memori container. Container 1 GB โ
512โ700 MB.
Latihan: tulis Dockerfile di atas dan bangun. Catat waktunya. Ubah satu baris di sebuah
komponen, bangun lagi, dan catat waktunya โ selisihnya menunjukkan cache layer bekerja. Lalu jalankan
keempat perintah verifikasi, dan pastikan docker run --rm $REPO:$TAG id tidak menunjukkan
uid=0.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.