Studi Kasus: Aplikasi Python

Studi Kasus: Aplikasi Python

Bitnesia Sep 12, 2026 12 EN

Python jadi bahasa andalan banyak tim Developer untuk membangun web application, mulai dari monolith berbasis Django sampai microservice ringan berbasis FastAPI. Berbeda dengan Node.js yang punya satu runtime serba bisa, ekosistem Python terpecah jadi beberapa pendekatan pengelolaan dependency (pip, poetry, virtualenv) dan dua model server yang tidak saling kompatibel secara langsung: WSGI untuk aplikasi sinkron seperti Django dan Flask, serta ASGI untuk aplikasi asinkron seperti FastAPI. Bab ini membahas studi kasus penuh containerizing aplikasi Python: menyusun Dockerfile multi-stage untuk Django dan FastAPI, memilih strategi pengelolaan dependency yang tepat, menjalankan aplikasi lewat Gunicorn atau Uvicorn di production, sampai menjalankan background task lewat Celery dan Redis dalam container terpisah.

30.1 Containerizing Django/Flask/FastAPI

Django dan Flask dibangun di atas spesifikasi WSGI (Web Server Gateway Interface), model komunikasi sinkron antara web server dan aplikasi Python yang sudah jadi standar sejak lama. FastAPI dan Starlette sebaliknya dibangun di atas ASGI (Asynchronous Server Gateway Interface), spesifikasi yang mendukung request asinkron dan WebSocket. Perbedaan ini menentukan server seperti apa yang dipakai untuk menjalankan aplikasi di dalam container, sehingga Dockerfile untuk Django dan FastAPI punya struktur yang mirip di bagian dependency, tapi berbeda di bagian CMD.

30.1.1 Struktur Project dan Perbedaan WSGI vs ASGI

Project Django hasil django-admin startproject punya file wsgi.py dan asgi.py sekaligus di dalam folder konfigurasi, tapi yang lazim dipakai di production adalah wsgi.py lewat Gunicorn, kecuali aplikasi memang butuh dukungan asinkron seperti WebSocket. Flask punya pola serupa, cukup dengan objek app yang diekspor dari modul utama. FastAPI berbeda karena tidak menyediakan built-in WSGI, aplikasinya cuma bisa dijalankan lewat server ASGI seperti Uvicorn.

myproject/
├── config/
│   ├── settings.py
│   ├── urls.py
│   ├── wsgi.py
│   └── asgi.py
├── myapp/
│   ├── models.py
│   ├── views.py
│   └── tasks.py
├── manage.py
├── requirements.txt
├── .dockerignore
└── Dockerfile

Folder myapp berisi kode aplikasi Django, sementara manage.py tetap dipakai untuk menjalankan perintah administratif seperti migration di dalam container. Struktur FastAPI jauh lebih ringkas, biasanya cuma butuh satu file main.py yang mengekspos objek FastAPI() sebagai entry point-nya.

30.1.2 Dockerfile Multi-Stage untuk Django

Sama seperti studi kasus containerizing aplikasi lain, instalasi dependency Python sebaiknya dipisahkan dari image final lewat multi-stage build, supaya compiler dan file sementara hasil instalasi tidak ikut terbawa ke image production.

FROM python:3.12-slim AS builder
WORKDIR /app

RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

FROM python:3.12-slim AS production
WORKDIR /app

RUN groupadd -r django && useradd -r -g django django

COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH" \
    PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

COPY . .
RUN chown -R django:django /app

USER django
EXPOSE 8000
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000"]

Pola virtual environment di /opt/venv pada tahap builder lalu disalin utuh ke tahap production lewat COPY --from=builder membuat seluruh package yang sudah terinstal ikut berpindah tanpa perlu instalasi ulang, sekaligus menjaga environment Python tetap terisolasi dari system package milik image dasar. Variabel PYTHONDONTWRITEBYTECODE mencegah Python menulis file .pyc ke dalam layer image, sedangkan PYTHONUNBUFFERED membuat output print dan log langsung diteruskan ke stdout tanpa di-buffer, penting supaya docker logs menampilkan log aplikasi secara real-time.

Instruksi USER django menjalankan proses aplikasi sebagai non-root user, praktik keamanan dasar yang membatasi dampak kalau ada vulnerability pada aplikasi sampai dieksploitasi Attacker untuk mendapat akses ke dalam container. Base image python:3.12-slim dipilih karena ukurannya jauh lebih kecil dibanding python:3.12 biasa, tapi tetap menyertakan system library umum yang sering dibutuhkan package Python dengan native extension, berbeda dengan varian alpine yang kadang butuh kompilasi ulang dependency tersebut karena perbedaan C library.

30.1.3 Dockerfile untuk FastAPI (ASGI)

Dockerfile FastAPI mengikuti pola multi-stage yang sama, cuma berbeda di bagian CMD karena FastAPI dijalankan lewat server ASGI, bukan WSGI.

FROM python:3.12-slim AS builder
WORKDIR /app

RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

FROM python:3.12-slim AS production
WORKDIR /app

RUN groupadd -r fastapi && useradd -r -g fastapi fastapi

COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH" \
    PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

COPY . .
RUN chown -R fastapi:fastapi /app

USER fastapi
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

Opsi --host 0.0.0.0 wajib disertakan karena Uvicorn secara default cuma mendengarkan koneksi dari 127.0.0.1, yang di dalam container berarti cuma bisa diakses dari dalam container itu sendiri. Tanpa opsi ini, port yang sudah dipetakan lewat docker run -p atau compose.yaml tetap tidak akan bisa diakses dari luar container, meskipun proses Uvicorn terlihat berjalan normal di log.

30.1.4 Static Files dan collectstatic pada Django

Django punya mekanisme static files yang mengumpulkan seluruh asset CSS, JavaScript, dan gambar dari berbagai app ke satu folder lewat perintah collectstatic. Perintah ini butuh akses ke DJANGO_SETTINGS_MODULE dan variabel konfigurasi lain yang biasanya cuma tersedia saat container benar-benar dijalankan, bukan saat image sedang dibangun, sehingga menjalankannya langsung di dalam Dockerfile lewat instruksi RUN berisiko gagal atau memakai nilai konfigurasi yang salah untuk environment tujuan.

Pendekatan yang lebih aman adalah menjalankan collectstatic lewat entrypoint script yang dieksekusi setiap kali container start, sebelum Gunicorn benar-benar menjalankan aplikasi.

#!/bin/sh
set -e

python manage.py collectstatic --noinput
python manage.py migrate --noinput

exec gunicorn config.wsgi:application --bind 0.0.0.0:8000

Simpan skrip ini sebagai entrypoint.sh, salin ke dalam image, dan jadikan ENTRYPOINT Dockerfile.

COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

Baris exec di akhir skrip penting supaya proses Gunicorn menggantikan proses shell sebagai process ID 1 di dalam container, bukan berjalan sebagai child process dari skrip sh. Tanpa exec, sinyal SIGTERM yang dikirim Docker saat container dihentikan tidak diteruskan dengan benar ke proses Gunicorn, sehingga container butuh waktu lebih lama untuk berhenti karena Docker terpaksa menunggu sampai grace period habis lalu mengirim SIGKILL secara paksa.

Menjalankan migrate otomatis di setiap start container nyaman untuk development, tapi di production dengan banyak replica aplikasi berjalan bersamaan, praktik ini berisiko menimbulkan race condition kalau beberapa container mencoba migrasi skema database secara bersamaan. Untuk production, pertimbangkan menjalankan migrate sebagai tahap terpisah di pipeline deployment sebelum replica baru mulai menerima traffic, bukan otomatis di setiap start.

30.2 Dependency Management dalam Image

Ekosistem Python punya beberapa tool pengelolaan dependency yang masing-masing punya cara berbeda untuk dipakai secara efisien di dalam image Docker. Memilih strategi yang tepat berpengaruh langsung pada ukuran image dan kecepatan build, terutama soal bagaimana Docker layer caching dimanfaatkan supaya instalasi dependency tidak perlu diulang setiap kali ada perubahan kecil di kode aplikasi.

30.2.1 pip dan requirements.txt

pip dengan file requirements.txt tetap jadi pendekatan paling sederhana dan paling umum dipakai. Kunci efisiensi build ada pada urutan instruksi COPY: salin requirements.txt lebih dulu sebelum menyalin seluruh kode aplikasi, supaya layer instalasi dependency cuma di-invalidate kalau file tersebut berubah, bukan setiap kali ada perubahan kode.

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

Opsi --no-cache-dir mencegah pip menyimpan cache hasil download package di dalam layer image, karena cache tersebut tidak berguna lagi setelah instalasi selesai dan cuma menambah ukuran image tanpa manfaat. Untuk mengunci versi transitive dependency secara pasti dan mencegah build yang tidak konsisten antar waktu, gunakan pip-compile dari package pip-tools untuk menghasilkan requirements.txt dengan versi yang benar-benar terkunci dari file requirements.in yang cuma berisi dependency utama.

30.2.2 Virtual Environment di Dalam Container

Di lingkungan non-container, virtualenv atau modul venv bawaan Python dipakai untuk mengisolasi dependency antar-project pada satu mesin yang sama. Di dalam container, isolasi semacam itu sebenarnya sudah didapat secara otomatis karena tiap container punya filesystem sendiri, sehingga venv tidak lagi wajib dipakai untuk alasan isolasi antar-project.

Meski begitu, venv tetap punya manfaat praktis dalam konteks multi-stage build: seluruh package yang terinstal terkumpul rapi dalam satu folder, sehingga bisa disalin utuh dari tahap builder ke tahap production lewat satu instruksi COPY --from, seperti yang sudah dipraktikkan pada Dockerfile Django dan FastAPI sebelumnya. Tanpa venv, package yang terinstal lewat pip install biasa akan tersebar ke lokasi site-packages bawaan Python di sistem, membuat proses penyalinan selektif antar-stage jadi lebih rumit.

30.2.3 Poetry dalam Multi-Stage Build

Poetry mengelola dependency lewat file pyproject.toml dan mengunci versi pastinya di poetry.lock, mirip konsep package-lock.json di ekosistem Node.js. Dockerfile berikut memakai Poetry untuk menghasilkan virtual environment di dalam folder project, lalu menyalin folder tersebut ke image final.

FROM python:3.12-slim AS builder
WORKDIR /app

RUN pip install --no-cache-dir poetry==1.8.3
RUN poetry config virtualenvs.in-project true

COPY pyproject.toml poetry.lock ./
RUN poetry install --no-root --without dev

FROM python:3.12-slim AS production
WORKDIR /app

RUN groupadd -r django && useradd -r -g django django

COPY --from=builder /app/.venv /app/.venv
ENV PATH="/app/.venv/bin:$PATH" \
    PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

COPY . .
RUN chown -R django:django /app

USER django
EXPOSE 8000
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000"]

Opsi virtualenvs.in-project true membuat Poetry menaruh virtual environment-nya di folder .venv di dalam direktori project, bukan di lokasi tersembunyi di luar project seperti default-nya, sehingga folder tersebut mudah ditemukan dan disalin ke tahap berikutnya. Opsi --no-root pada poetry install membuat Poetry cuma menginstal dependency yang terdaftar tanpa ikut menginstal package project itu sendiri dalam mode editable, sesuai dokumentasi resmi Poetry, karena source code aplikasi baru disalin lewat COPY . . setelah tahap instalasi dependency ini. Opsi --without dev mengecualikan dependency group dev seperti testing tool atau linter yang cuma dibutuhkan saat development, bukan saat aplikasi berjalan di production.

Verifikasi dependency yang terinstal sesuai poetry.lock lewat perintah berikut di dalam container.

docker compose exec app /app/.venv/bin/pip list

30.3 WSGI/ASGI Server Setup

Development server bawaan Django (runserver) atau Flask (flask run) sengaja dirancang cuma untuk kebutuhan development, tidak untuk menangani concurrent request dalam jumlah besar di production. Gunicorn dan Uvicorn jadi dua pilihan server production yang lazim dipakai, dipilih sesuai model WSGI atau ASGI aplikasi yang dijalankan.

30.3.1 Gunicorn untuk Aplikasi WSGI

Gunicorn (Green Unicorn) adalah server WSGI dengan model pre-fork worker, di mana satu proses master mengatur beberapa proses worker yang masing-masing menangani request secara independen. Jalankan Gunicorn dengan menentukan jumlah worker secara eksplisit lewat opsi --workers.

gunicorn config.wsgi:application \
    --bind 0.0.0.0:8000 \
    --workers 4 \
    --timeout 30 \
    --access-logfile - \
    --error-logfile -

Opsi --access-logfile - dan --error-logfile - mengarahkan log akses dan error Gunicorn ke stdout/stderr alih-alih ke file, supaya log tersebut bisa ditangkap lewat docker logs dan sistem log aggregation yang sudah lazim dipakai untuk container. Opsi --timeout 30 menentukan batas waktu sebelum Gunicorn menganggap worker hang dan me-restart-nya secara paksa, nilai yang perlu disesuaikan naik kalau aplikasi memang punya endpoint dengan proses berat yang butuh waktu lebih lama dari 30 detik.

Di lapangan, jumlah worker yang pas jadi salah satu sumber troubleshooting paling sering ditemui Sysadmin/DevOps Engineer. Dokumentasi resmi Gunicorn merekomendasikan formula (2 x jumlah CPU core) + 1 sebagai titik awal, bukan angka yang asal besar, karena tiap worker Gunicorn berjalan sebagai proses terpisah yang memakan memori sendiri-sendiri. Menetapkan jumlah worker terlalu banyak pada container dengan resource limit kecil justru berisiko membuat container kehabisan memori dan dimatikan paksa oleh Docker dengan status OOMKilled.

30.3.2 Uvicorn dan Gunicorn dengan Worker ASGI

Uvicorn bisa dijalankan berdiri sendiri untuk menjalankan aplikasi ASGI seperti FastAPI, dan sejak beberapa versi terakhir juga mendukung opsi --workers untuk menjalankan beberapa proses worker sekaligus dari satu perintah.

uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4

Untuk kebutuhan production yang lebih kompleks, misalnya butuh graceful restart tanpa downtime saat deployment atau pengelolaan worker yang lebih matang, dokumentasi resmi Uvicorn merekomendasikan menjalankan Uvicorn di bawah Gunicorn sebagai process manager, memakai package tambahan uvicorn-worker yang menyediakan worker class khusus ASGI untuk Gunicorn.

pip install uvicorn-worker
gunicorn main:app \
    --bind 0.0.0.0:8000 \
    --workers 4 \
    --worker-class uvicorn.workers.UvicornWorker

Pendekatan ini menggabungkan kekuatan manajemen proses Gunicorn (misalnya graceful reload lewat sinyal SIGHUP) dengan performa event loop asinkron Uvicorn dalam menangani request ASGI. Dokumentasi resmi Uvicorn menandai implementasi worker class yang dulu langsung dibawa dalam package utamanya sebagai deprecated dan mengarahkan pengguna untuk menginstal package terpisah uvicorn-worker supaya tetap mendapat implementasi yang terawat, meski opsi --worker-class yang dipakai masih tetap merujuk path uvicorn.workers.UvicornWorker yang sama. Kalau opsi ini gagal dikenali Gunicorn dengan pesan ModuleNotFoundError, pastikan package uvicorn-worker sudah masuk daftar requirements.txt atau pyproject.toml, bukan cuma uvicorn saja.

30.3.3 Health Check dan Verifikasi Server

Tambahkan HEALTHCHECK di Dockerfile atau healthcheck di compose.yaml supaya Docker bisa mendeteksi kalau proses Gunicorn atau Uvicorn berjalan tapi sebenarnya tidak lagi merespons request dengan benar.

services:
  app:
    build: .
    ports:
      - "8000:8000"
    healthcheck:
      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/healthz')"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s

Perintah healthcheck di atas memakai modul urllib bawaan Python karena image python:3.12-slim tidak menyertakan curl secara default, sehingga tidak perlu menambah package baru cuma untuk keperluan health check. Endpoint /healthz pada contoh ini perlu didefinisikan sendiri di kode aplikasi, umumnya berupa view atau route sederhana yang mengembalikan status 200 OK tanpa memeriksa dependency eksternal seperti database, supaya health check tetap cepat dan tidak ikut gagal kalau database sedang lambat merespons.

Kalau container berulang kali berstatus unhealthy, periksa dulu apakah aplikasi benar-benar mendengarkan di 0.0.0.0 dan bukan 127.0.0.1, lalu periksa log Gunicorn atau Uvicorn untuk melihat apakah ada worker yang berulang kali crash saat startup.

docker compose logs app
docker compose ps

30.4 Background Task dengan Celery dan Redis

Aplikasi Python yang punya proses berat seperti pengiriman email, pemrosesan gambar, atau pemanggilan API eksternal yang lambat, umumnya memindahkan proses tersebut ke background task supaya tidak memblokir response ke pengguna. Celery jadi task queue paling populer di ekosistem Python untuk kebutuhan ini, dan butuh message broker seperti Redis untuk mengantre task antara aplikasi utama dengan proses worker yang mengeksekusinya.

30.4.1 Service Redis dan Celery Worker di Compose

Tambahkan service redis dan worker ke compose.yaml yang sudah dibuat sebelumnya. Celery worker memakai image yang sama dengan aplikasi utama, cuma beda command yang dijalankan.

services:
  app:
    build: .
    ports:
      - "8000:8000"
    environment:
      CELERY_BROKER_URL: redis://redis:6379/0
      CELERY_RESULT_BACKEND: redis://redis:6379/0
    depends_on:
      redis:
        condition: service_healthy

  worker:
    build: .
    command: celery -A config worker --loglevel=info --concurrency=4
    environment:
      CELERY_BROKER_URL: redis://redis:6379/0
      CELERY_RESULT_BACKEND: redis://redis:6379/0
    depends_on:
      redis:
        condition: service_healthy
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    volumes:
      - redis-data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 10

volumes:
  redis-data:

Opsi -A config pada perintah celery worker merujuk ke modul Celery application instance yang biasanya didefinisikan di file config/celery.py pada project Django, sesuai konvensi integrasi Celery dengan Django yang dijelaskan di dokumentasi resminya. Opsi --concurrency=4 menentukan jumlah child process yang dipakai worker untuk mengeksekusi task secara paralel, nilai yang perlu disesuaikan dengan sifat task itu sendiri, apakah lebih banyak menghabiskan CPU atau cuma menunggu I/O seperti panggilan API eksternal.

Redis dipilih sebagai broker pada contoh ini karena instalasinya sederhana dan sudah cukup untuk kebutuhan task queue pada umumnya, meskipun Celery juga mendukung broker lain seperti RabbitMQ untuk kebutuhan jaminan pengiriman pesan yang lebih ketat, sesuai dokumentasi resmi Celery.

30.4.2 Celery Beat untuk Scheduled Task

Celery beat adalah scheduler terpisah yang bertugas mengirim task ke antrean secara berkala, misalnya untuk membersihkan data lama setiap tengah malam. Sama seperti pola scheduler pada studi kasus Laravel, beat sebaiknya dijalankan sebagai container tersendiri, terpisah dari worker yang mengeksekusi task.

services:
  beat:
    build: .
    command: celery -A config beat --loglevel=info
    environment:
      CELERY_BROKER_URL: redis://redis:6379/0
      CELERY_RESULT_BACKEND: redis://redis:6379/0
    depends_on:
      redis:
        condition: service_healthy
    restart: unless-stopped

Dokumentasi resmi Celery menegaskan bahwa proses beat cuma boleh dijalankan sebagai satu instance saja, karena menjalankannya lebih dari sekali akan membuat setiap periodic task terkirim berkali-kali ke antrean sesuai jumlah instance beat yang aktif. Jebakan ini sering ditemui Sysadmin/DevOps Engineer saat men-scale service lewat docker compose up --scale tanpa memperhatikan bahwa service beat harus dikecualikan dari proses scaling tersebut, berbeda dengan service worker yang justru aman untuk dijalankan dalam banyak replica sekaligus.

Alternatif lain adalah menggabungkan beat ke dalam proses worker lewat opsi -B, tapi dokumentasi resmi Celery secara eksplisit menyebut pola ini cuma cocok untuk single worker node dan tidak direkomendasikan untuk production, sehingga container terpisah tetap jadi pilihan yang lebih aman untuk kebutuhan production.

30.4.3 Monitoring dan Troubleshooting Task

Verifikasi worker Celery aktif dan bisa dijangkau lewat perintah inspect ping, yang mengirim sinyal ke seluruh worker yang terhubung ke broker yang sama.

docker compose exec worker celery -A config inspect ping

Response pong dari worker menandakan koneksi antara worker dan broker Redis berjalan normal. Kalau perintah ini timeout tanpa response, periksa dulu apakah nilai CELERY_BROKER_URL di container worker sudah benar mengarah ke nama service redis, bukan localhost, kesalahan yang sama persis dengan jebakan koneksi database pada studi kasus Laravel.

Untuk memeriksa task yang sedang berjalan atau task yang gagal, gunakan opsi inspect active dan inspect reserved.

docker compose exec worker celery -A config inspect active
docker compose exec worker celery -A config inspect reserved

Task yang gagal berulang kali biasanya meninggalkan jejak traceback di log container worker, sehingga langkah pertama troubleshooting paling praktis tetap memeriksa log secara langsung.

docker compose logs -f worker

Satu hal yang perlu diingat soal trustworthiness data: kalau CELERY_RESULT_BACKEND memakai Redis yang sama dengan broker tanpa persistence yang memadai, hasil task yang tersimpan di backend bisa hilang kalau container Redis restart tanpa volume yang benar. Volume redis-data pada contoh compose.yaml di atas penting untuk menjaga data Redis tetap ada meski container-nya dibuat ulang, tapi tetap bukan pengganti strategi backup yang lebih menyeluruh untuk data yang sifatnya kritikal.