Docker image adalah paket berisi aplikasimu plus file sistem operasi dan pengaturan yang dibutuhkannya, dan container adalah image itu saat dijalankan. Resepnya ditulis di Dockerfile. Bayangin kamu mengirim barang lewat kontainer kapal. Siapa pun yang memegang kontainer itu bisa membuka setiap lapisan isinya, jadi kunci rumah jangan ikut dikemas. Dan petugas di dalam kontainer gak perlu memegang kunci induk pelabuhan.
🧪 Tebakan cepat: Dockerfile-mu menulis
COPY . .(termasuk.env), lalu di baris berikutnyaRUN rm .env. Apakah.envsudah hilang dari image? Tebak dulu, jawabannya ada di bagian "Secret jangan masuk image".
Dockerfile yang diperiksa
Ini Dockerfile proyek tokokopi, sama dengan yang ada di Lab:
FROM node:latest
WORKDIR /app
ENV STRIPE_SECRET_KEY=sk_live_abc123
COPY . .
RUN npm ci
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/server.js"]
Jalan, tapi ada lima temuan:
| Baris | Temuan | Kenapa jadi masalah |
|---|---|---|
| 1 | node:latest |
isinya berganti tiap rilis Node baru, jadi build hari ini dan bulan depan beda |
| 3 | secret di ENV |
tersimpan di image, siapa pun yang punya image bisa membacanya |
| 4 | COPY . . tanpa .dockerignore |
.env dan .git ikut tersalin ke image |
| 5 | satu stage | devDependencies, source, dan alat build ikut ke image produksi |
| semua | gak ada USER |
proses jalan sebagai root |
Jalankan sebagai user biasa: USER
Tanpa baris USER, proses di container jalan sebagai root (uid 0), user dengan akses tertinggi. Kalau aplikasimu dibobol, penyerang langsung pegang root di container, dan satu salah konfigurasi lagi bisa jadi jalan ke mesin host.
Image Node resmi sudah punya user biasa bernama node (uid 1000), jadi cukup:
USER node
CMD ["node", "dist/server.js"]
Untuk image yang belum punya user, buat dulu, lalu pindah ke user itu:
# Debian atau Ubuntu
RUN adduser --disabled-password --gecos "" appuser
USER appuser
adduser --disabled-passwordmembuat user tanpa password, jadi gak bisa dipakai login.USER appusermembuat semua instruksi berikutnya, dan proses container nanti, jalan sebagai user itu.
Secret jangan masuk image
Setiap instruksi di Dockerfile membuat satu layer, dan image adalah tumpukan layer itu. Siapa pun yang bisa menarik (pull) image-mu bisa membuka setiap layer.
ENV STRIPE_SECRET_KEY=...tersimpan di konfigurasi image.docker inspectmenampilkannya.COPY . .menyalin seluruh folder proyek, termasuk.env. Begitu jugaCOPY .env /app/.env: file.envmasuk ke image.RUN rm .envdi baris berikutnya cuma membuat layer baru yang "menyembunyikan" file itu. File-nya tetap utuh di layerCOPYsebelumnya dan bisa diambil lagi. Itu jawaban tebakannya: belum hilang.
Perbaikannya ada dua. Pertama, file .dockerignore mencegah file ikut dikirim ke proses build sama sekali:
# .dockerignore
.env
.git
node_modules
dist
Kedua, secret dikirim saat container dijalankan, bukan saat build:
docker run --env-file .env -p 3000:3000 tokokopi:1.0
Nilainya dibaca aplikasi lewat process.env dan gak ikut tersimpan di image yang kamu push. Di platform hosting, pakai fitur secret dari platformnya.
Pin tag dan multi-stage build
- Pin tag.
node:latestmenunjuk versi Node terbaru, jadi isinya berubah sendiri.node:24-alpinemengunci major Node 24 di atas Alpine Linux yang kecil: unduhan base-nya sekitar 62 MB, dibanding sekitar 444 MB untuknode:latest(Oktober 2026). Lebih sedikit paket berarti lebih sedikit paket yang bisa punya celah (CVE). - Multi-stage build. Satu Dockerfile dengan dua
FROM. Stage pertama memasang semua dependency dan menjalankan build. Stage terakhir cuma mengambil hasil build dan dependency produksi. Alat build, devDependencies, dan source gak pernah sampai ke image produksi.
Dockerfile setelah dibenahi
# Stage 1: build, devDependencies boleh ada di sini
FROM node:24-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: image yang dikirim ke produksi
FROM node:24-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]
FROM node:24-alpine AS build: stage pertama, diberi namabuild.RUN npm ci --omit=dev: stage kedua cuma memasang dependency produksi.COPY --from=build /app/dist ./dist: ambil hasil build saja dari stage pertama.USER node: semuanya sudah terpasang, sekarang turun ke user biasa sebelum aplikasi jalan.- Gak ada
ENVberisi secret, dan.dockerignoremenjaga.envtetap di luar.
Setelah build, periksa juga paket di dalam image dengan scanner seperti docker scout cves tokokopi:1.0 atau trivy image tokokopi:1.0.
⚠️ Jebakan yang sering ketemu
RUN rmuntuk menghapus secret. File-nya masih ada di layer sebelumnya. Cegah dari awal dengan.dockerignore.- Secret lewat
ARG. Nilai build argument tercatat didocker history. Untuk secret saat build, pakaiRUN --mount=type=secret. USERterlalu awal. KalauUSER nodeditaruh sebelumRUN npm ci, npm gagal dengan error EACCES karena folder/appmilik root. TaruhUSERsetelah langkah yang butuh menulis.FROM ubuntu:latestlaluUSER root. Dua masalah sekaligus: tag gak dipin dan container jalan sebagai root.
TL;DR: Image adalah tumpukan layer yang bisa dibuka siapa pun yang memegangnya, jadi secret gak boleh masuk lewat ENV, ARG, atau COPY, dan RUN rm gak menghapusnya dari layer lama. Pakai .dockerignore (isi .env, .git, node_modules) dan kirim secret saat jalan dengan docker run --env-file .env. Jalankan sebagai user biasa dengan USER node (atau adduser lalu USER appuser), pin tag seperti node:24-alpine, dan pakai multi-stage build supaya image produksi cuma berisi hasil build dan dependency produksi.