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:
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:
- Baris 1 (
FROM python:3.12-slim): image dasar dipin ke versi minor Python 3.12, bukanpython:latestyang isinya bergeser tanpa kendali. - Baris 5-6 (
COPY requirements.txt,RUN pip install): dependensi dipasang sebelum kode disalin, sehingga pengubahanetl_pesanan.pytidak memicu pemasangan ulang ratusan megabyte pustaka. - Baris 9-10 (
WORKDIR,COPY etl_pesanan.py): kode pipeline masuk ke layer tersendiri; hanya layer inilah yang berubah pada iterasi harian. - Baris 13-14 (
useradd,USER pipeline): proses berjalan sebagai pengguna biasa, membatasi dampak bila pipeline dieksploitasi. - 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:
.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:
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:
"""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:
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.0Skrip 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.