Chapter Pengenalan Docker di section Persiapan Lingkungan telah memperkenalkan image, container, dan registry, lalu menjalankan PostgreSQL lewat docker run dan docker-compose.yml. Kali ini kita pindah ke sisi produksi: membangun image milik sendiri untuk pipeline ETL TokoKita. Tujuannya satu artefak yang identik — versi Python, pustaka, dan kode yang sama persis — berjalan di laptop engineer, server staging, dan cluster production. Chapter ini menyusun Dockerfile yang memanfaatkan layer cache, mengamankan container dengan non-root user, lalu merangkai pipeline, PostgreSQL, dan MinIO dalam satu berkas compose.

Image, Layer, dan Cache #

Setiap instruksi dalam Dockerfile menghasilkan satu layer yang menumpuk menjadi image. Layer bersifat hanya-baca dan di-cache: saat kita membangun ulang, Docker memeriksa setiap instruksi dari atas ke bawah dan memakai cache selama instruksi serta berkas terkaitnya tidak berubah. Begitu satu instruksi berubah, cache untuk instruksi tersebut dan seluruh instruksi setelahnya dinyatakan tidak berlaku. Konsekuensinya urutan instruksi menentukan kecepatan build: salin dan pasang dependensi lebih dulu, salin kode aplikasi paling akhir — kode yang berubah setiap hari tidak akan membatalkan cache pemasangan dependensi.

Dockerfile untuk Pipeline Data #

Pipeline TokoKita memakai image dasar python:3.12-slim — ukuran kecil karena hanya memuat Python di atas Debian minimal:

dockerfile
FROM python:3.12-slim

# Layer 1-2: dependensi disalin dan dipasang lebih dulu.
# Selama requirements.txt tidak berubah, layer ini diambil dari cache.
COPY requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir -r /app/requirements.txt

# Layer 3: kode aplikasi disalin setelah dependensi.
WORKDIR /app
COPY etl_pesanan.py /app/etl_pesanan.py

# Layer 4: pengguna non-root agar container tidak berjalan sebagai admin.
RUN useradd --create-home pipeline
USER pipeline

CMD ["python", "etl_pesanan.py"]

Mari kita telaah baris-baris Dockerfile di atas:

  1. Baris 1 (FROM python:3.12-slim): image dasar dipin ke versi minor Python 3.12, bukan python:latest yang isinya bergeser tanpa kendali.
  2. Baris 5-6 (COPY requirements.txt, RUN pip install): dependensi dipasang sebelum kode disalin, sehingga pengubahan etl_pesanan.py tidak memicu pemasangan ulang ratusan megabyte pustaka.
  3. Baris 9-10 (WORKDIR, COPY etl_pesanan.py): kode pipeline masuk ke layer tersendiri; hanya layer inilah yang berubah pada iterasi harian.
  4. Baris 13-14 (useradd, USER pipeline): proses berjalan sebagai pengguna biasa, membatasi dampak bila pipeline dieksploitasi.
  5. Baris 16 (CMD): perintah bawaan container — image yang sama juga bisa dipakai sebagai job batch maupun service.

Berkas requirements.txt yang disalin di baris 5 memakai versi ter-pin persis (pandas==2.2.3, pyarrow==26.0.0), meniru disiplin dependency pinning dari section Python untuk Data Engineering. Untuk pipeline yang butuh kompilasi pustaka native, pola multi-stage build membantu: stage pertama membangun roda di image lengkap, stage kedua hanya menyalin hasilnya ke image ramping akhir — pipeline Python murni seperti ini biasanya cukup satu stage.

Apa yang disalin ke dalam image dikontrol berkas .dockerignore — penting karena COPY dan build context ikut mengirim seluruh isi folder proyek ke daemon:

text
.git
.venv/
__pycache__/
*.pyc
.env
data/
.ipynb_checkpoints/
scratch/

Baris .env wajib ada: berkas tersebut menyimpan kredensial lokal sebagaimana dibahas pada chapter Konfigurasi Environment, dan tanpa pengecualian ini kredensial bisa ikut terkirim ke build context.

Menyusun Stack dengan docker-compose #

Pipeline produksi jarang berdiri sendiri; pipeline TokoKita membaca event dari MinIO (object storage kompatibel S3 untuk development lokal) dan menulis hasil ke PostgreSQL. Ketiganya dideklarasikan dalam satu docker-compose.yml:

yaml
services:
  postgres:
    image: postgres:17
    environment:
      POSTGRES_USER: budi
      POSTGRES_PASSWORD: YOUR_DATABASE_PASSWORD
      POSTGRES_DB: tokokita
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U budi -d tokokita"]
      interval: 5s
      timeout: 3s
      retries: 5

  minio:
    image: minio/minio
    command: server /data --console-address ":9001"
    environment:
      MINIO_ROOT_USER: tokokita
      MINIO_ROOT_PASSWORD: YOUR_MINIO_PASSWORD
    ports:
      - "9000:9000"
      - "9001:9001"
    volumes:
      - minio_data:/data

  pipeline:
    build: .
    depends_on:
      postgres:
        condition: service_healthy
      minio:
        condition: service_started
    environment:
      DATABASE_URL: postgresql://budi:YOUR_DATABASE_PASSWORD@postgres:5432/tokokita
      S3_ENDPOINT: http://minio:9000
      S3_BUCKET: data-toko
    volumes:
      - ./data:/data

volumes:
  postgres_data:
  minio_data:

Tiga hal patut dicermati. Service pipeline memakai build: . sehingga imagenya dibangun dari Dockerfile di folder yang sama, bukan diunduh dari registry. depends_on dengan condition: service_healthy menahan pipeline sampai pg_isready di healthcheck PostgreSQL sukses — tanpa ini pipeline bisa menyala sebelum database siap. Terakhir, volumes bernama membuat data bertahan melewati siklus hidup container.

Memvalidasi Konfigurasi tanpa Menjalankan Docker #

Lingkungan penulisan chapter ini tidak memasang Docker, sehingga perintah docker build dan docker compose up tidak dieksekusi. Yang bisa diverifikasi nyata: struktur YAML compose lolos parser, skrip pipeline lolos py_compile, dan seluruh dependensi ter-pin:

python
"""Verifikasi konfigurasi container tanpa Docker: parser YAML + py_compile.

Docker tidak terpasang di lingkungan penulisan, sehingga perintah build/up
tidak dieksekusi. Yang bisa diverifikasi nyata: struktur docker-compose.yml
(aman menurut parser YAML), sintaks etl_pesanan.py (py_compile), dan
pembacaan requirements.txt dengan versi ter-pin.
"""
import pathlib
import py_compile

import yaml

AKAR = pathlib.Path(__file__).resolve().parent

print("Validasi konfigurasi container (Docker tidak tersedia di lingkungan ini):")
print()

# [1] docker-compose.yml harus lolos parser YAML.
compose = yaml.safe_load((AKAR / "docker-compose.yml").read_text())
layanan = compose["services"]
print("[1] docker-compose.yml lolos parser YAML")
print("    layanan:", ", ".join(sorted(layanan)))
for nama in sorted(layanan):
    cfg = layanan[nama]
    sumber = cfg.get("image", "build dari Dockerfile")
    port = ",".join(p.split(":")[0] for p in cfg.get("ports", [])) or "-"
    volume = ",".join(v.split(":")[0] for v in cfg.get("volumes", [])) or "-"
    tunggu = ",".join(sorted(cfg.get("depends_on", {}))) or "-"
    print("    - {:<9}: {:<22} port {:<9} volume {:<14} depends_on {}".format(
        nama, sumber, port, volume, tunggu))
print("    volume deklaratif:", ", ".join(sorted(compose.get("volumes", {}))))

# [2] Sintaks pipeline yang disalin ke dalam image.
py_compile.compile(str(AKAR / "etl_pesanan.py"), doraise=True)
print()
print("[2] etl_pesanan.py lolos py_compile (sintaks Python 3 valid)")

# [3] Dependensi harus ter-pin persis.
baris_req = [b.strip() for b in (AKAR / "requirements.txt").read_text().splitlines()
             if b.strip() and not b.startswith("#")]
semua_pin = all("==" in b for b in baris_req)
print("[3] requirements.txt: {} dependensi, semuanya ter-pin (==): {}".format(
    len(baris_req), semua_pin))
for b in baris_req:
    print("    -", b)

Output yang Dihasilkan:

text
Validasi konfigurasi container (Docker tidak tersedia di lingkungan ini):

[1] docker-compose.yml lolos parser YAML
    layanan: minio, pipeline, postgres
    - minio    : minio/minio            port 9000,9001 volume minio_data     depends_on -
    - pipeline : build dari Dockerfile  port -         volume ./data         depends_on minio,postgres
    - postgres : postgres:17            port 5432      volume postgres_data  depends_on -
    volume deklaratif: minio_data, postgres_data

[2] etl_pesanan.py lolos py_compile (sintaks Python 3 valid)
[3] requirements.txt: 2 dependensi, semuanya ter-pin (==): True
    - pandas==2.2.3
    - pyarrow==26.0.0

Skrip pembaca ringkasan tersebut menegaskan tiga hal sekaligus: parser YAML menerima seluruh struktur compose, etl_pesanan.py yang disalin ke image valid secara sintaks, dan kedua dependensi ter-pin persis. Referensi lengkap instruksi Dockerfile tersedia di https://docs.docker.com/reference/dockerfile/ , sedangkan referensi Compose ada di https://docs.docker.com/compose/ .

Image dan compose kini menjelaskan isi container, tetapi container sendiri hanyalah satu jenis resource. Bucket, topic, jaringan, dan kebijakan akses juga harus dideklarasikan dan direproduksikan — pekerjaan chapter Infrastructure as Code.