← Kembali ke Blog

Bikin Docker Image Production yang Kecil, Cepat, dan Aman

Kamu push image, nunggu 3 menit build, 2 menit push, dan ketika akhirnya sampai ke production, container yang mendarat masih bawa Ubuntu, Python, pip cache, build tools, dan semua dependensi dev yang kamu install berbulan-bulan lalu. Image itu makan disk, memperlambat rollout, dan membawa library dengan CVE yang bahkan tidak kamu pakai.

Saya pernah kerja dengan cluster di mana rata-rata ukuran image lebih dari 1GB. Itu bukan hal yang aneh. Tapi juga nggak perlu. Dengan beberapa teknik sederhana, kebanyakan image aplikasi bisa di bawah 200MB. Kadang di bawah 50MB.

Prerequisites

  • Docker Engine 24+ (cek dengan docker version)
  • Sebuah project untuk di-containerize
  • Familiar dengan Dockerfile dasar

Teknik 1: Multi-Stage Builds

Ini yang paling berdampak. Multi-stage build memungkinkan satu Dockerfile punya beberapa FROM. Tahap pertama isinya semua build tools. Tahap terakhir cuma naruh file yang diperlukan buat jalanin aplikasi.

Go

# Stage 1: Build
FROM golang:1.23 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o server ./cmd/api

# Stage 2: Runtime
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /app/server /server
ENTRYPOINT ["/server"]

golang:1.23 ukurannya sekitar 800MB. Distroless cuma 2MB. Image akhir kamu sekitar 15-20MB.

Node.js

# Stage 1: Install dependensi produksi aja
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production

# Stage 2: Runtime
FROM node:22-alpine AS runner
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
ENV NODE_ENV=production
CMD ["node", "dist/index.js"]

Dependensi dev kayak TypeScript, testing library, dan linter nggak ikut ke production. Base Alpine ~120MB. Image akhirnya tetap di kisaran itu karena cuma nambah source code di atasnya.

Python

# Stage 1: Build wheels
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt ./
RUN pip install --user --no-cache-dir -r requirements.txt

# Stage 2: Runtime
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY . .
ENV PATH=/root/.local/bin:$PATH
ENV PYTHONUNBUFFERED=1
RUN addgroup --system app && adduser --system --ingroup app app
USER app
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

Yang sering salah: pip install di stage runtime atau nge-copy seluruh filesystem builder. Copy cuma wheels yang diperlukan.

Kenapa Multi-Stage Efektif

Build tools itu berat. Go compiler, C toolchain, npm dengan dependency resolution-nya, semua ini nggak diperlukan saat runtime. Misahin build dari runtime artinya kamu ninggalin 99% beban di belakang.

Teknik 2: Pilih Base Image yang Tepat

Ukuran base image sangat bervariasi.

Image Size Cocok Untuk
debian:bookworm-slim ~80MB General purpose, butuh apt packages
alpine:3.20 ~7MB Minimal, pake musl libc (test dulu)
gcr.io/distroless/base ~10MB Binary statis, tanpa shell
gcr.io/distroless/static ~2MB Go, Rust, atau binary statis lain
scratch 0 bytes Binary fully static, tanpa OS layer

Aturan simpel: mulai dari varian -slim atau -alpine. Pake -full cuma kalau kamu beneran butuh build tools saat runtime (jarang terjadi).

Peringatan soal Alpine. Beberapa package Python (psycopg2, numpy, pandas) compile C extensions. Alpine pake musl libc, bukan glibc, dan perbedaan itu bisa bikin linking failure yang susah di-debug. Aplikasi Python biasanya lebih cocok pake varian -slim Debian.

Teknik 3: .dockerignore

Docker ngirim seluruh build context ke daemon sebelum ngejalanin Dockerfile. Repo dengan node_modules, Python virtual environment, atau direktori .git bisa ratusan megabyte.

node_modules/
.git/
*.md
.gitignore
.env
.env.*
dist/
.next/
coverage/
__pycache__/
*.pyc
.vscode/
.idea/

Tanpa file ini, Docker nggak tahu mana yang harus dikecualikan. Semua dikirim.

Teknik 4: Urutkan Layer untuk Caching

Docker cache setiap layer build. Kalau satu layer berubah, semua layer setelahnya di-rebuild. Triknya: taruh yang jarang berubah di atas.

# Jelek - cache invalid tiap kali source berubah
COPY . .
RUN pip install -r requirements.txt
# Bagus - pip cuma jalan ulang kalau dependensi berubah
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .

Ini paling kerasa di CI. Kalau pipeline rebuild di setiap commit, Dockerfile yang terurut bisa hemat 30-60 detik per build. Di monorepo dengan puluhan service, efeknya signifikan.

Teknik 5: Jangan Jalan sebagai Root

Container default-nya jalan sebagai root. Kalau aplikasi kamu berhasil dieksploitasi, attacker punya akses root di dalam container. Perbaikinya satu baris:

RUN addgroup --system app && adduser --system --ingroup app app
USER app

Distroless images dari Google punya varian nonroot. Pake itu.

Teknik 6: Scan Sebelum Push

Scan image buat cari CVE sebelum dikirim ke production.

Docker Scout (bawaan Docker Desktop):

docker scout quickview myapp:latest

Trivy (open source, cocok buat CI):

trivy image --severity HIGH,CRITICAL myapp:latest

Saya pasang Trivy di setiap pipeline CI. Kalau ketemu CVE CRITICAL, pipeline berhenti. Ini nangkep masalah kayak OpenSSL kedaluwarsa di base image sebelum masuk production.

# GitHub Actions
- name: Scan image
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: 'myapp:${{ github.sha }}'
    severity: 'CRITICAL,HIGH'
    exit-code: '1'

Praktek Langsung

Dockerfile lengkap untuk Go service:

FROM golang:1.23 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o server .

FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /app/server /server
ENTRYPOINT ["/server"]
docker build -t myapp:latest .
docker scout quickview myapp:latest
docker push myapp:latest

Ukuran akhir untuk binary Go? Sekitar 15MB. Bandingkan dengan image golang 800MB yang biasanya kamu kirim.

Kesalahan Umum

Build cache ikut ke image final. apt-get install ninggalin file .deb di /var/cache/apt/archives. Bersihin di layer RUN yang sama: apt-get clean && rm -rf /var/lib/apt/lists/*.

COPY . . terlalu awal di file. Ini nge-bust cache setiap kali source berubah. Taruh COPY yang jarang berubah (package.json, requirements.txt) di bagian atas.

Pake tag latest buat base image. node:22-alpine bisa nunjuk ke image yang berbeda seminggu kemudian. Pin digest atau minimal minor version: node:22.13-alpine@sha256:abc123.

Langkah Selanjutnya

Kalau image kamu sudah ramping, ini topik yang bisa dieksplor:

  • Cosign buat signing image dan verifikasi integritas di production
  • Kaniko atau BuildKit buat build image di dalam Kubernetes tanpa mounting Docker socket
  • Docker Slim sebagai automated optimizer buat image yang belum sempat direfaktor

Butuh Bantuan Implementasi?

Saya membantu tim mendesain dan membangun infrastruktur cloud scalable, pipeline DevOps, dan sistem production-grade.

Konsultasi Gratis