Setup Dockerized Development Environment untuk Aplikasi Python dan PostgreSQL
Memahami Masalah Lingkungan Development yang Tidak Konsisten
Salah satu sumber frustasi paling umum dalam pengembangan aplikasi adalah perbedaan perilaku antara environment developer, staging, dan production. Sebuah kode yang berjalan mulus di laptop kita bisa tiba-tiba error ketika dijalankan di server produksi. Fenomena ini sering disebut "works on my machine" dan penyebabnya biasanya sederhana: perbedaan versi sistem operasi, interpreter Python, library yang terinstal, atau bahkan konfigurasi database.
Docker hadir sebagai solusi untuk masalah konsistensi ini. Dengan containerization, kita mengemas kode aplikasi, runtime Python, seluruh dependensi, dan konfigurasi sistem ke dalam satu unit yang disebut container. Container ini berjalan identik di mana pun — laptop developer, server staging, maupun production. Tidak ada lagi alasan "beda versi library" atau "lupa install dependensi" karena semua sudah ditentukan di dalam Dockerfile.
Pendekatan ini membawa keuntungan besar lainnya: kita tidak perlu menginstal Python, PostgreSQL, atau tools development lainnya secara langsung di mesin kita. Cukup instal Docker, dan seluruh environment development siap digunakan dalam hitungan detik. Mari kita lihat bagaimana cara menyusun setup ini langkah demi langkah.

Gambar: Arsitektur stack container Docker — aplikasi dan dependensi berjalan di atas container engine, berbagi kernel OS host tanpa perlu guest OS terpisah — Sumber: [Microsoft Learn](https://learn.microsoft.com/en-us/dotnet/architecture/microservices/container-docker-introduction/docker-defined)
Menyusun Dockerfile untuk Aplikasi Python
Langkah pertama adalah membuat Dockerfile yang mendefinisikan bagaimana container aplikasi Python kita akan dibangun. Pilihan base image sangat penting di sini. Kita akan menggunakan python:3.11-slim — versi slim yang ringan namun tetap menyertakan toolchain dasar yang diperlukan untuk mengompilasi beberapa library Python.
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]Logika di balik Dockerfile ini cukup sederhana. Kita mulai dengan menetapkan direktori kerja /app menggunakan WORKDIR, lalu menyalin file requirements.txt terlebih dahulu. Ini penting karena Docker menyimpan cache untuk setiap layer — dengan menyalin requirements.txt sebelum kode aplikasi, kita memastikan proses pip install hanya berjalan ulang ketika ada perubahan dependensi, bukan setiap kali kita mengubah kode. Perintah pip install --no-cache-dir memastikan image tetap kecil dengan tidak menyimpan cache package.
Setelah dependensi terinstal, kita salin seluruh kode aplikasi, expose port 8000, dan jalankan server menggunakan uvicorn dengan main:app sebagai entry point. Base image slim sudah cukup untuk sebagian besar framework seperti FastAPI, Flask, atau Django dengan psycopg2-binary dan SQLAlchemy.
# Contoh requirements.txt
fastapi
uvicorn
psycopg2-binary
sqlalchemyMengorkestrasi Aplikasi dan Database dengan Docker Compose
Aplikasi kita membutuhkan PostgreSQL sebagai database. Di sinilah Docker Compose berperan — kita bisa mendefinisikan dua service (app untuk Python dan db untuk PostgreSQL) dalam satu file YAML dan menjalankannya bersamaan.
services:
app:
build: .
ports:
- "8000:8000"
env_file:
- .env
depends_on:
- db
db:
image: postgres:15-alpine
environment:
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ${DB_NAME}
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:Konfigurasi ini mendefinisikan service app yang dibuild dari Dockerfile di direktori saat ini, dengan port mapping 8000 dan environment variable dari file .env. Keyword depends_on memastikan container database dimulai terlebih dahulu sebelum aplikasi, meskipun koneksi database tetap perlu di-handle dengan retry logic di sisi aplikasi.
Service db menggunakan image postgres:15-alpine yang ringan dan sudah optimized. Environment variable untuk username, password, dan nama database dibaca dari file .env yang sama. Kita juga akan membahas volume pgdata di bagian selanjutnya.

Gambar: Perbandingan arsitektur aplikasi tradisional (kiri) dengan aplikasi containerized (kanan) — container mengemas aplikasi beserta filesystem dan dependensinya secara terisolasi — Sumber: [Wikimedia Commons](https://commons.wikimedia.org/wiki/File:Containers.png) (CC BY-SA 4.0)
Untuk menguji koneksi antara aplikasi dan database, kita bisa menggunakan script Python sederhana:
import psycopg2
import os
conn = psycopg2.connect(
host=os.getenv("DB_HOST", "db"),
port=os.getenv("DB_PORT", "5432"),
user=os.getenv("DB_USER"),
password=os.getenv("DB_PASSWORD"),
dbname=os.getenv("DB_NAME")
)
cur = conn.cursor()
cur.execute("SELECT version();")
version = cur.fetchone()
print(f"Terhubung ke PostgreSQL: {version[0]}")
cur.close()
conn.close()Script ini membaca konfigurasi dari environment variable yang di-set oleh Docker Compose. Host diisi dengan "db" — ini adalah nama service, yang secara otomatis di-resolve oleh internal Docker network ke alamat IP container PostgreSQL. Ketika script dijalankan, kita akan melihat output versi PostgreSQL yang menandakan koneksi berhasil.
Mengelola Persistent Data dan Konfigurasi Lingkungan
Data di dalam container bersifat ephemeral — ketika container dihapus, semua data ikut hilang. Untuk PostgreSQL, ini tentu tidak bisa diterima. Solusinya adalah Docker Volume, yang menyimpan data di luar container dan tetap ada meskipun container di-restart atau dihapus.
Dalam konfigurasi Compose sebelumnya, kita mendefinisikan volume pgdata dan me-mount-nya ke direktori data PostgreSQL di dalam container. Volume ini dikelola oleh Docker dan disimpan di host filesystem. Ketika container db di-restart, semua data tetap utuh.

Gambar: Konsep virtualisasi filesystem — setiap proses/container memiliki tampilan filesystem sendiri yang terisolasi, sehingga data di dalam container bersifat ephemeral dan volume diperlukan untuk persistensi — Sumber: [Wikimedia Commons](https://commons.wikimedia.org/wiki/File:How_file_system_virtualization_works.png) (CC BY 4.0)
Selain volume, kita juga perlu memisahkan konfigurasi dari kode menggunakan file .env. Ini adalah praktik keamanan dasar — credential database tidak boleh di-hardcode di dalam kode atau Dockerfile.
# .env
DB_USER=myapp_user
DB_PASSWORD=rahasia123
DB_NAME=myapp_db
DB_HOST=db
DB_PORT=5432Docker Compose juga secara otomatis membuat network internal untuk menghubungkan service-service yang didefinisikan. Service app bisa mengakses service db melalui hostname yang sama dengan nama service (db). Kita tidak perlu mendefinisikan network secara eksplisit kecuali ingin kustomisasi lebih lanjut, seperti memisahkan network untuk frontend dan backend.
Docker menyediakan dua tipe volume yang perlu kita pahami. Named volume, seperti pgdata yang kita gunakan, dikelola sepenuhnya oleh Docker dan disimpan di lokasi khusus di host. Ini adalah pilihan terbaik untuk data database karena portabel dan mudah di-backup. Bind mount memetakan direktori spesifik di host ke dalam container — cocok untuk file konfigurasi atau data yang perlu kita akses langsung dari host.
Selain volume, kita juga bisa menambahkan initialization script untuk PostgreSQL. Docker akan menjalankan file .sql atau .sh yang ditempatkan di direktori /docker-entrypoint-initdb.d/ saat pertama kali container database dijalankan. Ini berguna untuk membuat tabel awal atau mengisi data seed tanpa harus menjalankan migrasi secara manual.
Menerapkan Workflow Development dengan Hot Reload
Salah satu keunggulan development dengan Docker adalah kemampuan untuk melakukan hot reload — perubahan kode langsung terlihat tanpa perlu membangun ulang container. Caranya dengan menggunakan bind mount, yang memetakan direktori kode di host ke direktori kerja di container.
services:
app:
build: .
ports:
- "8000:8000"
env_file:
- .env
depends_on:
- db
volumes:
- .:/app
command: uvicorn main:app --host 0.0.0.0 --port 8000 --reloadDengan volumes: - .:/app, setiap perubahan pada file di direktori proyek host langsung tercermin di dalam container. Flag --reload pada uvicorn membuat server otomatis restart ketika mendeteksi perubahan file. Hasilnya, workflow development terasa seperti bekerja secara lokal — edit kode, simpan, dan langsung lihat hasilnya di browser tanpa perlu docker-compose up --build setiap saat.
Penting untuk dipahami bahwa strategi bind mount ini hanya untuk development. Di production, kita menggunakan COPY di Dockerfile untuk menyalin kode ke dalam image, bukan bind mount. Image production harus self-contained dan tidak bergantung pada filesystem host.
Selain hot reload, Docker Compose juga memudahkan kita menjalankan perintah di dalam container yang sedang berjalan. Gunakan docker-compose exec app pytest untuk menjalankan test suite tanpa harus menginstal Python di host. Atau docker-compose exec db psql -U myapp_user myapp_db untuk mengakses PostgreSQL CLI langsung dari container database. Semua interaksi ini terjadi di dalam network internal yang sama, sehingga kita tidak perlu khawatir tentang konfigurasi koneksi.
Kita juga bisa menambahkan service tambahan untuk development, misalnya adminer untuk GUI database atau redis untuk caching. Dengan Docker Compose, menambahkan service baru cukup dengan beberapa baris konfigurasi — tidak perlu instalasi manual atau konfigurasi port yang rumit.
Best Practices untuk Dockerized Python Development
Beberapa praktik terbaik akan membuat pengalaman development dengan Docker lebih efisien dan aman.
Pertama, gunakan .dockerignore untuk mengecualikan file yang tidak diperlukan dalam image. File seperti __pycache__, .git, venv, dan node_modules tidak perlu masuk ke dalam container. Ini mempercepat proses build dan mengurangi ukuran image.
__pycache__
.git
.gitignore
.env
venv
*.pycKedua, jalankan container sebagai non-root user untuk keamanan. Secara default, container berjalan sebagai root. Ini tidak ideal karena jika ada celah keamanan, attacker bisa mendapatkan akses root ke container. Tambahkan user khusus di Dockerfile:
RUN useradd --create-home appuser
USER appuserKetiga, pisahkan production dan development dependencies. Gunakan dua file requirements.txt — satu untuk production dan satu untuk development — atau gunakan fitur grouping pada package manager seperti Poetry. Di development, kita membutuhkan library seperti pytest atau httpx yang tidak diperlukan di production.
Keempat, manfaatkan Docker logs untuk debugging. Semua output yang dikirim ke stdout/stderr akan otomatis ditangkap oleh Docker. Kita bisa melihat log container dengan docker logs <container-name> atau docker-compose logs -f untuk streaming. Ini sangat membantu ketika aplikasi tidak berjalan seperti yang diharapkan dan kita perlu melihat traceback error.
Kelima, tambahkan health check pada service database. Docker Compose mendukung konfigurasi healthcheck yang secara periodik memeriksa apakah service sudah siap menerima koneksi. Kita bisa menggunakannya bersama depends_on dengan kondisi condition: service_healthy untuk memastikan aplikasi tidak mencoba terkoneksi sebelum database benar-benar siap.
services:
db:
image: postgres:15-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
interval: 5s
timeout: 5s
retries: 5
app:
depends_on:
db:
condition: service_healthyKeenam, tetapkan resource limits untuk container. Meskipun kita menggunakan environment development, membatasi penggunaan CPU dan memory membantu mensimulasikan kondisi production dan mencegah satu container mengonsumsi seluruh resource host.
services:
app:
deploy:
resources:
limits:
cpus: '0.5'
memory: 512MKetujuh, optimalkan Docker layer caching dengan menyusun instruksi Dockerfile dari yang paling jarang berubah ke yang paling sering berubah. Urutan COPY requirements.txt sebelum COPY . yang kita gunakan di awal sudah tepat. Kita juga bisa memisahkan instalasi dependencies production dan dev ke dalam layer yang berbeda untuk memaksimalkan cache. Gunakan dua perintah pip install terpisah jika ada dependensi yang hanya dibutuhkan di development.
Dengan setup Dockerized ini, kita mendapatkan environment development yang konsisten, portable, dan mudah direproduksi. Tim developer baru cukup menjalankan docker-compose up, dan seluruh stack — aplikasi Python plus database PostgreSQL — langsung siap digunakan. Tidak perlu install Python, pip, atau PostgreSQL secara manual. Jika ingin memperdalam skill container orchestration dan production deployment, lihat Bootcamp Docker & DevOps di Rumah Coding untuk pembelajaran yang lebih komprehensif.