Pipeline yang berjalan baik di satu komputer sering gagal di komputer lain karena versi Python dan pustakanya tidak sama. Docker menyelesaikan masalah itu dengan mengemas pipeline beserta seluruh dependensinya ke dalam image, sehingga eksekusinya sama di mana pun container itu dijalankan. Topik ini menyusun paket lengkap untuk sebuah pipeline Python, mulai dari Dockerfile dan requirements.txt, urutan instruksi yang memanfaatkan cache lapisan, sampai Docker Compose yang menjalankan pipeline beserta basis datanya sebagai satu unit.
Pipeline contoh yang dikemas#
Pipeline yang dipakai sepanjang topik ini adalah skrip transformasi suhu harian dengan target yang bergantung pada lingkungannya. Di komputer kerja biasa, skrip menghitung rata-rata suhu per tanggal lalu menuliskannya ke berkas DuckDB pada folder hasil. Di dalam container Compose, variabel lingkungan DB_HOST disuntikkan, dan skrip yang sama beralih memuat hasilnya ke PostgreSQL. Satu kode untuk dua target, dan peralihannya adalah lingkungan tempat kode itu berjalan. Pola semacam ini lazim dipakai pada pipeline produksi, yaitu kode yang sama untuk lingkungan latihan dan lingkungan produksi.
import os
import duckdb
# data contoh yang pada pipeline nyata datang dari tahap ekstraksi
PENGUKURAN = [
("2024-05-01", "S-1", 24.0),
("2024-05-01", "S-2", 25.5),
("2024-05-02", "S-1", 23.5),
("2024-05-02", "S-2", 26.5),
("2024-05-03", "S-1", 24.5),
("2024-05-03", "S-2", 27.5),
]
def hitung_rata_rata() -> list[tuple]:
"""Transformasi: rata-rata suhu harian dari data pengukuran."""
con = duckdb.connect()
con.execute("CREATE TABLE pengukuran (tanggal DATE, sensor VARCHAR, suhu_c DOUBLE)")
con.executemany("INSERT INTO pengukuran VALUES (?, ?, ?)", PENGUKURAN)
hasil = con.sql(
"SELECT tanggal, avg(suhu_c) AS rata_rata_suhu "
"FROM pengukuran GROUP BY tanggal ORDER BY tanggal"
)
return [(str(t), round(r, 4)) for t, r in hasil.fetchall()]
def muat_ke_postgres(baris: list[tuple]) -> None:
"""Target container: muat hasil ke PostgreSQL melalui variabel lingkungan."""
import psycopg2
koneksi = psycopg2.connect(
host=os.environ["DB_HOST"],
dbname=os.environ["DB_NAMA"],
user=os.environ["DB_PENGGUNA"],
password=os.environ["DB_SANDI"],
)
with koneksi:
with koneksi.cursor() as kursor:
kursor.execute(
"CREATE TABLE IF NOT EXISTS rata_rata_harian "
"(tanggal DATE PRIMARY KEY, rata_rata_suhu DOUBLE PRECISION)"
)
kursor.executemany(
"INSERT INTO rata_rata_harian VALUES (%s, %s) "
"ON CONFLICT (tanggal) DO UPDATE SET rata_rata_suhu = EXCLUDED.rata_rata_suhu",
baris,
)
koneksi.close()
print("hasil dimuat ke PostgreSQL pada host", os.environ["DB_HOST"])
def muat_lokal(baris: list[tuple]) -> None:
"""Target mode lokal: berkas DuckDB di folder hasil."""
os.makedirs("hasil", exist_ok=True)
con = duckdb.connect()
con.execute("CREATE TABLE rata_rata_harian (tanggal DATE, rata_rata_suhu DOUBLE)")
con.executemany("INSERT INTO rata_rata_harian VALUES (?, ?)", baris)
con.execute("COPY rata_rata_harian TO 'hasil/rata-rata-harian.csv' (FORMAT CSV, HEADER)")
print(con.sql("SELECT * FROM rata_rata_harian ORDER BY tanggal"))
if __name__ == "__main__":
hasil = hitung_rata_rata()
if os.environ.get("DB_HOST"):
muat_ke_postgres(hasil)
else:
muat_lokal(hasil)┌────────────┬────────────────┐
│ tanggal │ rata_rata_suhu │
│ date │ double │
├────────────┼────────────────┤
│ 2024-05-01 │ 24.75 │
│ 2024-05-02 │ 25.0 │
│ 2024-05-03 │ 26.0 │
└────────────┴────────────────┘Blok keluaran menampilkan hasil mode lokal, yaitu tabel rata-rata yang juga tertulis di hasil/rata-rata-harian.csv. Fungsi muat_ke_postgres hanya dieksekusi ketika DB_HOST tersedia, sehingga jalur container tidak pernah terpanggil pada mode lokal. Perhatikan pula bahwa skrip tidak membaca konfigurasi dari berkas apa pun, karena pada container konfigurasi masuk lewat variabel lingkungan.
Dockerfile untuk pipeline Python#
Empat berkas berikut membentuk proyek yang akan dikemas, dan semuanya ditulis utuh pada topik ini sehingga dapat disalin apa adanya.
proyek-pipeline/
├── Dockerfile
├── requirements.txt
├── docker-compose.yml
└── pipeline.pyDockerfile adalah resep pembangunan image, dan setiap barisnya adalah instruksi yang dijalankan berurutan saat build.
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY pipeline.py .
CMD ["python", "pipeline.py"]FROM memilih image dasar Python 3.12 varian slim, yakni varian yang tidak membawa perkakas tambahan sehingga ukurannya ramping. WORKDIR menetapkan folder kerja di dalam container. COPY requirements.txt . menyalin daftar paket lebih dahulu, RUN memasangnya, baru COPY pipeline.py . menyalin kode pipeline, dan CMD menyatakan perintah yang dijalankan ketika container dimulai. Alasan urutan COPY yang demikian dibahas pada bagian berikutnya.
Daftar paketnya menuliskan versi secara eksplisit dengan tanda ==.
duckdb==2.0.0
psycopg2-binary==2.9.9Menetapkan versi secara eksplisit seperti ini membuat image yang dibangun hari ini dan tahun depan memuat pustaka yang persis sama, dan sifat itulah inti dari reproduksibilitas. Tanpa pin versi, build yang dilakukan pada waktu berbeda dapat menghasilkan pustaka berbeda, dan keluaran pipeline ikut berbeda tanpa alasan yang terlihat. Versi duckdb==2.0.0 sekaligus menyamakan versi yang dipakai sepanjang jalur ini, sehingga keluaran pada topik lain tetap sebanding.
Urutan instruksi dan layer#
Setiap instruksi Dockerfile menghasilkan satu layer, yakni lapisan berisi perubahan berkas di atas lapisan sebelumnya, dan lapisan yang masukannya tidak berubah dipakai ulang dari cache. Docker menghitung masukan setiap instruksi, sehingga RUN pip install dianggap sama selama berkas requirements.txt-nya tidak berubah. Barulah urutan penulisan berperan, karena perubahan pada satu instruksi menggugurkan lapisannya dan seluruh lapisan setelahnya.
Skrip berikut meniru perhitungan tersebut dengan hash SHA-256 sebagai pengganti mekanisme cache Docker, sehingga efek perubahan kode terlihat tanpa perlu Docker sama sekali.
import hashlib
def ringkas(*teks: str) -> str:
"""Simulasi digest lapisan: hash dari instruksi beserta masukannya."""
return hashlib.sha256("\n".join(teks).encode()).hexdigest()[:12]
REQUIREMENTS = "duckdb==2.0.0\npsycopg2-binary==2.9.9\n"
SUMBER_LAMA = 'print("pengukuran diproses")'
SUMBER_BARU = 'print("pengukuran diproses")\nprint("jumlah baris dicatat")'
def bangun(sumber: str) -> dict[str, str]:
"""Bangun tumpukan lapisan dengan digest turunan dari lapisan sebelumnya."""
lapisan: dict[str, str] = {}
d = ringkas("FROM python:3.12-slim")
lapisan["FROM"] = d
d = ringkas(d, "COPY requirements.txt .", REQUIREMENTS)
lapisan["COPY requirements"] = d
d = ringkas(d, "RUN pip install -r requirements.txt")
lapisan["RUN pip install"] = d
d = ringkas(d, "COPY pipeline.py .", sumber)
lapisan["COPY pipeline.py"] = d
return lapisan
lama = bangun(SUMBER_LAMA)
baru = bangun(SUMBER_BARU)
print(f"{'instruksi':<18} {'build lama':<14} {'build baru':<14} sama")
for kunci in lama:
sama = "ya" if lama[kunci] == baru[kunci] else "tidak"
print(f"{kunci:<18} {lama[kunci]:<14} {baru[kunci]:<14} {sama}")instruksi build lama build baru sama
FROM d625e0936ca0 d625e0936ca0 ya
COPY requirements f34fd51211d1 f34fd51211d1 ya
RUN pip install 392ebb221f48 392ebb221f48 ya
COPY pipeline.py 03888f59a930 a307bde13985 tidakTiga lapisan pertama mempertahankan digest yang sama, sehingga pada build nyata ketiganya diambil dari cache dan pemasangan paket yang lambat itu dilewati. Hanya lapisan COPY pipeline.py yang digestnya berubah, karena masukannya memang berubah. Apabila urutannya ditukar, misalnya COPY pipeline.py . diletakkan sebelum pip install, setiap perubahan kode akan menggugurkan lapisan pemasangan paket juga, dan build berikutnya memasang ulang semuanya.
Build dan eksekusi container#
Perintah build dan run untuk pipeline ini adalah dua perintah Docker standar berikut. Keduanya membutuhkan Docker yang terpasang dan daemon yang berjalan, sehingga pada lingkungan latihan tanpa Docker keduanya cukup dibaca tanpa dieksekusi.
# tidak-dijalankan
# membangun image bernama pipeline-pengukuran dari Dockerfile
docker build -t pipeline-pengukuran .
# menjalankan pipeline sekali dan menghapus container setelah selesai
docker run --rm pipeline-pengukuranPerintah docker build membaca Dockerfile dari atas ke bawah, membangun lapisan demi lapisan, lalu menandai hasil akhirnya dengan nama pipeline-pengukuran. Perintah docker run membuat container dari image itu, menjalankan CMD-nya, dan menghapus container setelah selesai karena opsi --rm. Satu hal yang perlu dicatat, berkas hasil/ yang ditulis mode lokal hanya hidup di dalam sistem berkas container dan ikut terhapus bersamanya, sehingga hasil yang perlu dipertahankan dikirim ke target eksternal seperti basis data.
Compose untuk pipeline dan basis data#
Container yang terpisah sering perlu berjalan sebagai satu unit, dan Docker Compose menyelesaikannya dengan satu berkas YAML yang mendefinisikan beberapa layanan sekaligus. Berikut adalah Compose untuk pipeline beserta basis data PostgreSQL-nya.
services:
pipeline:
build: .
environment:
DB_HOST: db
DB_NAMA: pengukuran
DB_PENGGUNA: pipeline
DB_SANDI: sandi-latihan
depends_on:
db:
condition: service_healthy
db:
image: postgres:16
environment:
POSTGRES_DB: pengukuran
POSTGRES_USER: pipeline
POSTGRES_PASSWORD: sandi-latihan
healthcheck:
test: ["CMD-SHELL", "pg_isready -U pipeline -d pengukuran"]
interval: 5s
timeout: 3s
retries: 10Layanan pipeline dibangun dari Dockerfile pada folder yang sama melalui build: ., sedangkan layanan db memakai image postgres:16 yang siap pakai. Empat variabel lingkungan pada layanan pipeline adalah kunci peralihan target pada pipeline.py, yaitu host, nama basis data, pengguna, dan sandi. Bagian depends_on dengan condition: service_healthy menahan pipeline agar tidak berjalan sebelum PostgreSQL benar-benar siap menerima koneksi, dan keadaan itu diuji oleh healthcheck dengan perintah pg_isready. Tanpa penahannya, pipeline bisa berjalan lebih dulu, gagal terhubung, lalu mati hanya karena urutan kebetulan.
# tidak-dijalankan
# menjalankan kedua layanan beserta pembangunan image-nya
docker compose up --build
# menghentikan dan menghapus container yang dibuat compose
docker compose downPerintah docker compose up membangun image, membuat jaringan antar layanan, menunggu kesehatan basis data, lalu menjalankan pipeline hingga selesai. Perintah down menghentikan layanan dan merapikan container yang dibuatnya. Dengan dua perintah itu, lingkungan pipeline lengkap dengan basis datanya dapat dibangkitkan dan dimatikan secara berulang dalam keadaan yang sama.
Kesalahan umum#
Kesalahan berikut paling sering muncul saat pipeline pertama kali dikemas, dan masing-masing menyertakan penyebab beserta perbaikannya.
- Menyalin kode sebelum memasang paket. Pemilihan urutan
COPY pipeline.py .sebelumRUN pip installmembuat cache pemasangan paket hangus setiap kali kode berubah. Susun Dockerfile dari requirements.txt lebih dahulu, sebagaimana contoh topik ini. - Memasang paket tanpa pin versi. Build yang dilakukan pada waktu berbeda dapat mengunduh versi pustaka yang berbeda, sehingga sifat dapat direproduksi hilang tanpa terlihat. Tuliskan versi eksplisit pada requirements.txt dengan tanda
==. - Menyimpan keluaran penting di dalam container. Sistem berkas container dibuang ketika container dihapus, dan keluaran yang hanya tertulis di sana hilang bersamanya. Kirim hasilnya ke target eksternal seperti basis data, atau simpan pada volume yang melekat pada folder komputer tuan rumah.
Ringkasan#
- Dockerfile meresepkan image dari image dasar python:3.12-slim, pemasangan paket dari requirements.txt, dan kode pipeline, dengan
CMDsebagai perintah eksekusinya. - Layer dihitung dari masukan instruksinya, sehingga menempatkan
pip installsebelum penyalinan kode menjaga cache tetap hidup ketika kode berubah. - Variabel lingkungan memeralihkan target pipeline dari berkas DuckDB lokal ke PostgreSQL tanpa mengubah satu baris kode pun.
- Docker Compose menjalankan pipeline dan basis data sebagai satu unit, dengan
depends_onberbasis healthcheck sebagai penahannya.
Referensi#
- What is Docker? — Docker Docs — Ikhtisar arsitektur Docker, yaitu image, container, dan daemon.
- Dockerfile reference — Docker Docs — Daftar instruksi Dockerfile, termasuk FROM, COPY, RUN, dan CMD yang dipakai topik ini.
- Docker build cache — Docker Docs — Cara cache lapisan bekerja dan urutan instruksi yang efisien.
- Compose file reference — Docker Docs — Struktur berkas Compose, termasuk depends_on dan healthcheck yang dipakai di sini.