โ† Semua pembelajaran / Astro Nol โ†’ Portal Berita
Fase 10 ยท Deploy di AWS

Dockerfile multi-stage

Dockerfile yang salah urutan membuat setiap perubahan satu baris kode memicu instalasi dependensi dari nol. Urutannya yang menentukan, bukan isinya.

Sumber asli docs.docker.com Resmi Rangkuman ~8 menit baca

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 pengguna node.
  • Sematkan versi image dasar. node:22 bergerak; node:22.14-alpine tidak.
  • .dockerignore sama pentingnya dengan Dockerfile โ€” tanpanya node_modules lokalmu 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 . .
PerubahanSalahBenar
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.