Python #

Python terkenal sebagai bahasa yang “rumit di-container”. Tidak seperti Go yang menghasilkan static binary, atau Java yang punya JRE yang jelas, Python image sering membengkak karena kombinasi interpreter, dependency, dan native extension. Image Django sederhana bisa berakhir di 500-800 MB, padahal logikanya hanya CRUD API.

Tapi ini bukan takdir Python. Image Python yang ramping dan production-grade sangat mungkin — sama seperti Go dan Java. Yang membedakan adalah disiplin build boundary: pisahkan dengan tegas apa yang dibutuhkan untuk compile dependency dari apa yang dibutuhkan untuk runtime. Artikel ini membahas secara mendetail, realistis, dan engineering-driven bagaimana membangun Docker image Python (Django, Flask, FastAPI) yang ramping, aman, dan production-grade.

1. Realita Ukuran Image Python #

Mari kita lihat angka realistis di production, dari yang paling bermasalah hingga optimal.

Setup Ukuran Image
python:latest + pip install (semua) 600-900 MB
python:slim tanpa cleanup 350-500 MB
python:slim + multi-stage + cleanup 180-250 MB
python:slim + multi-stage + distroless 90-140 MB
PyInstaller bundled + distroless 50-80 MB

Insight: Selisih antara 800 MB dan 50 MB adalah satu order of magnitude. Jika image Python kamu > 300 MB, hampir pasti ada dependency atau OS package yang bocor ke runtime.

Cara audit:

docker history myapp:latest

Lihat layer mana yang terbesar. Biasanya culprit-nya adalah pip install tanpa multi-stage, atau apt-get install yang cache-nya tidak dibersihkan.

2. Kenapa Image Python Mudah Membengkak? #

Python punya beberapa karakteristik yang membuat image-nya rentan membengkak jika Dockerfile tidak disiplin.

Python Tidak Memisahkan Build vs Runtime #

Banyak package Python:

  • Butuh compiler untuk install dari source (misal gcc, make, g++).
  • Butuh header OS untuk link ke library native (misal libpq-dev, libssl-dev, libxml2-dev).
  • Compile-on-install lewat pip install jika wheel tidak tersedia untuk platform.

Jika dependency ini di-install di stage yang sama dengan runtime, semuanya ikut masuk image final. Compiler dan header file yang dibutuhkan untuk build tidak dibutuhkan untuk runtime — tapi ikut terbawa.

Dependency C Extension #

Beberapa package Python yang umum di production punya C extension:

  • psycopg2 / psycopg2-binary — driver PostgreSQL.
  • Pillow — image processing.
  • cryptography — kriptografi.
  • lxml — XML parsing.
  • numpy / pandas — scientific computing (sering).

Package ini butuh library native saat install, dan kadang saat runtime juga. Kalau di-handle asal-asalan, library dan header-nya akan numpuk di image.

pip Tidak Prune Otomatis #

Berbeda dengan npm yang punya devDependencies dan dependencies terpisah, pip punya konsep yang lebih longgar. Tidak ada requirements.txt otomatis yang memisahkan dev vs prod — kamu harus mengelolanya secara manual.

Plus, pip menyimpan cache download di image (/root/.cache/pip/ atau /tmp/pip-*). Cache ini bisa puluhan MB dan tidak otomatis dihapus.

Default Image python:* Penuh #

Image python:latest (atau python:3.12) membawa Debian lengkap dengan banyak utilitas OS. Untuk production, hampir tidak ada yang membutuhkan semua itu. python:slim adalah alternatif yang lebih ramping, dan python:alpine lebih kecil lagi (dengan trade-off musl libc).

3. Prinsip Utama: Build Tools Mati di Runtime #

Runtime image Python idealnya hanya berisi: Python interpreter + virtualenv (atau site-packages) + dependency runtime + source code aplikasi.

Titik. Tidak ada compiler, tidak ada header file OS, tidak ada pip cache, tidak ada test runner (kecuali dipakai di production), tidak ada dokumentasi package.

Kontras dengan image “asal jalan”:

// ✗ ANTI-PATTERN: semua di satu stage
FROM python:3.12
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "manage.py runserver"]

Image seperti ini punya pip (tool yang tidak dibutuhkan runtime), cache pip (file sementara), dan semua package yang dikompilasi (termasuk compiler yang mungkin di-install otomatis). Ukuran membengkak tanpa perlu.

4. Strategi Multi-Stage dengan Virtualenv #

Pola paling umum dan paling direkomendasikan untuk Python: multi-stage build dengan virtualenv di stage build, lalu copy virtualenv ke stage runtime yang ramping.

4.1 Pola Dasar (Slim Runtime) #

# Build stage
FROM python:3.12-slim AS builder

WORKDIR /app

# Install build dependency
RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential \
    libpq-dev \
 && rm -rf /var/lib/apt/lists/*

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

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

# Runtime stage
FROM python:3.12-slim

WORKDIR /app

# Copy virtualenv dari build stage
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

# Copy source code
COPY . .

# Non-root user
RUN useradd --create-home --shell /bin/bash app
USER app

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

Ukuran tipikal: 180-250 MB.

Penjelasan penting:

  • Build stage punya build-essential dan libpq-dev (header PostgreSQL). Ini dibutuhkan untuk compile psycopg2 dan package lain yang punya C extension.
  • Virtualenv di /opt/venv — semua Python package terisolasi di sini, terpisah dari system Python. Ini yang akan di-copy ke runtime stage.
  • Cache pip dihapus dengan --no-cache-dir di pip install. Tanpa flag ini, /root/.cache/pip/ akan menambah puluhan MB ke image.
  • Apt cache dihapus di layer yang sama dengan instalasi (rm -rf /var/lib/apt/lists/*).
  • Runtime stage tidak punya build-essential atau libpq-dev. Ia hanya punya Python interpreter, virtualenv dengan package Python yang sudah ter-install, dan source code.
  • Non-root user untuk security.

Kapan pakai: Default untuk production service Python. Trade-off ukuran vs debugging能力 masih seimbang — slim punya shell dan package manager.

4.2 Distroless Runtime — Production Mature #

Untuk image lebih kecil, gunakan distroless di runtime stage. Distroless hanya membawa glibc, ca-certificates, dan Python runtime essentials.

FROM python:3.12-slim AS builder

WORKDIR /app

RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential \
    libpq-dev \
 && rm -rf /var/lib/apt/lists/*

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

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

# Runtime stage — distroless Python
FROM gcr.io/distroless/python3-debian12:nonroot

WORKDIR /app
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

COPY . .

EXPOSE 8000
USER nonroot:nonroot
CMD ["/opt/venv/bin/gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000"]

Ukuran tipikal: 90-140 MB.

Catatan penting:

  • gcr.io/distroless/python3-debian12:nonroot sudah include Python interpreter + pip/setuptools + ca-certificates.
  • Tag :nonroot artinya image sudah dikonfigurasi untuk user nonroot (UID 65532). Image nonroot punya WORKDIR di /home/nonroot.
  • Path /opt/venv harus absolute, dan CMD juga absolute path karena distroless tidak punya shell untuk resolve PATH.
  • Tidak ada shell — debugging interaktif tidak memungkinkan. Invest di observability.
  • Tidak ada apt — package OS yang lupa di-include di build stage akan hilang.

Kapan pakai: Production high-maturity, security-first. Observability harus solid.

5. Memisahkan Production dan Dev Dependency #

Python tidak otomatis memisahkan dev vs prod dependency. Kamu harus mengelolanya secara eksplisit.

Pola 1: Dua File Requirements #

requirements/
├── base.txt       # Dependency yang dibutuhkan runtime
├── prod.txt       # base + production-only
└── dev.txt        # base + prod + development
# requirements/base.txt
django==5.0.6
gunicorn==22.0.0
psycopg[binary]==3.1.18
# requirements/prod.txt
-r base.txt
gunicorn==22.0.0
psycopg[binary]==3.1.18
# requirements/dev.txt
-r prod.txt
django-debug-toolbar==4.4.6
pytest==8.2.0
factory-boy==3.3.0

Di Dockerfile production:

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

Pola 2: Multi-Stage dengan Dev Tools Terpisah #

FROM python:3.12-slim AS builder-base
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential libpq-dev \
 && rm -rf /var/lib/apt/lists/*
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

# Base: dependency wajib runtime
COPY requirements/base.txt /tmp/
RUN pip install --no-cache-dir -r /tmp/base.txt

# Dev target: tambah dev tools
FROM builder-base AS dev
COPY requirements/dev.txt /tmp/
RUN pip install --no-cache-dir -r /tmp/dev.txt

# Prod target: tanpa dev tools
FROM builder-base AS prod
COPY requirements/prod.txt /tmp/
RUN pip install --no-cache-dir -r /tmp/prod.txt

# Runtime stage
FROM python:3.12-slim
COPY --from=prod /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

Build:

docker build --target dev -t myapp:dev .
docker build --target prod -t myapp:prod .

6. C Extension dan Binary Wheel #

Package Python dengan C extension (psycopg2, Pillow, cryptography) sering jadi sumber masalah di Docker. Ada dua pendekatan utama.

Pakai Binary Wheel #

Wheel adalah format distribusi pre-compiled untuk Python. Package yang menyediakan wheel akan ter-install tanpa compiler di build stage.

# requirements.txt
# Daripada psycopg2 (perlu compile), pakai psycopg[binary]
psycopg[binary]==3.1.18
cryptography==42.0.5
Pillow==10.3.0

Untuk package yang punya suffix -binary (misal psycopg2-binary), wheel sudah termasuk library native. Untuk package tanpa suffix binary, cek PyPI untuk availability wheel di platform yang kamu target.

Kapan wheel tidak cukup: Jika package yang kamu butuhkan belum menyediakan wheel untuk platform yang kamu target. Solusinya: tetap compile, tapi pastikan compiler tidak ikut ke runtime image.

Hindari *-dev Package di Runtime #

Package dengan suffix -dev di Debian/Ubuntu adalah header file untuk kompilasi. Contoh: libpq-dev, libssl-dev, libxml2-dev.

Saat build:

RUN apt-get install -y libpq-dev  # untuk compile psycopg

Tapi di runtime, package yang dibutuhkan biasanya library saja (tanpa -dev):

# Runtime: cukup libpq5, tanpa -dev
RUN apt-get install -y libpq5

Aturan praktis:

  • Build stage: install libpq-dev (header + library).
  • Runtime stage: install libpq5 (library saja) atau copy dari build stage.

Cara copy library dari build stage:

# Build stage: install -dev
RUN apt-get install -y libpq-dev

# Runtime stage: copy library dari build stage
COPY --from=builder /usr/lib/x86_64-linux-gnu/libpq.so* /usr/lib/x86_64-linux-gnu/

Ini lebih advanced, tapi hasilnya image lebih ramping karena -dev package biasanya lebih besar dari library saja.

7. Dependency Audit #

Proyek Python sering membawa dependency yang tidak benar-benar dipakai, atau yang hanya relevan di development. Audit berkala penting untuk menjaga image ramping.

Cara cek dependency yang tidak terpakai:

# Untuk proyek Python
pip install pip-check
pip-check

Atau manual:

# Cek import yang ada di source code
grep -rh "^from \|^import " src/ | sort -u

# Bandingkan dengan requirements.txt
diff <(cat requirements.txt | cut -d'=' -f1 | sort) \
     <(grep -rh "^from \|^import " src/ | awk '{print $2}' | sort -u)

Audit yang harus dilakukan:

  1. Hapus package yang di-import tapi tidak dipakai.
  2. Hapus package yang di-import hanya untuk type checking (kecuali di production).
  3. Hapus django-debug-toolbar, pytest, factory-boy dari production image.
  4. Cek apakah ada library yang punya alternative yang lebih ramping.

8. Production-Grade Logging #

Aplikasi Python production harus log ke STDOUT, bukan ke file. Konfigurasi logging ada di kode aplikasi, bukan di Dockerfile.

Contoh setup logging untuk Django:

# settings.py
import os
import sys
import logging.config

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'json': {
            '()': 'pythonjsonlogger.jsonlogger.JsonFormatter',
            'format': '%(asctime)s %(name)s %(levelname)s %(message)s'
        },
    },
    'handlers': {
        'stdout': {
            'class': 'logging.StreamHandler',
            'stream': sys.stdout,
            'formatter': 'json',
        },
    },
    'root': {
        'handlers': ['stdout'],
        'level': os.getenv('LOG_LEVEL', 'INFO'),
    },
}

Prinsip penting:

  • Log ke sys.stdout (bukan sys.stderr untuk log info, error ke stderr).
  • Format JSON untuk production (mudah di-parse aggregator).
  • Level via env var, bukan hardcode.
  • Tidak ada log ke file — biarkan orchestrator yang mengumpulkan log dari STDOUT.

9. Healthcheck dan Signal Handling #

Healthcheck #

Untuk aplikasi web Python, healthcheck harus HTTP-based:

# Django view contoh
from django.http import JsonResponse
from django.db import connection

def health(request):
    try:
        with connection.cursor() as cursor:
            cursor.execute("SELECT 1")
        return JsonResponse({"status": "ok"})
    except Exception:
        return JsonResponse({"status": "error"}, status=503)

Di Dockerfile (untuk slim, bukan distroless):

HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
  CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health').read()" || exit 1

Atau pakai curl jika ada di image (alternatif python -c lebih portable).

Signal Handling #

Gunicorn (dan uvicorn untuk ASGI) sudah handle SIGTERM dengan benar secara default. Tapi pastikan:

  • Exec form di CMD, bukan shell form.
  • Graceful timeout diset cukup: gunicorn --graceful-timeout 30.
  • Worker timeout sesuai kebutuhan aplikasi.
CMD ["gunicorn", "config.wsgi:application", \
     "--bind", "0.0.0.0:8000", \
     "--workers", "3", \
     "--timeout", "60", \
     "--graceful-timeout", "30"]

10. Security Hardening #

Non-Root User #

Wajib untuk production:

# Di runtime stage
RUN groupadd -r app && useradd -r -g app -d /home/app -s /bin/bash app
USER app

Catatan untuk distroless: Image gcr.io/distroless/python3-debian12:nonroot sudah include user nonroot (UID 65532). Cukup tambahkan USER nonroot:nonroot.

Tidak Ada Secret di Image #

SECRET_KEY, DATABASE_PASSWORD, API_KEY — semua harus di-inject saat runtime, tidak pernah di-bake ke image.

# ✓ Di settings.py
SECRET_KEY = os.environ['SECRET_KEY']
DATABASE_PASSWORD = os.environ['DB_PASSWORD']

Anti-pattern umum:

# ✗ Hardcode di settings.py
SECRET_KEY = "django-insecure-abc123..."
# ✗ Hardcode di Dockerfile (lebih buruk lagi)
ENV SECRET_KEY=django-insecure-abc123...

Vulnerability Scanning #

Integrasikan scanning ke CI:

- name: Build
  run: docker build -t myapp:${{ github.sha }} .
- name: Scan
  run: trivy image --exit-code 1 --severity HIGH,CRITICAL myapp:${{ github.sha }}

11. Anti-Pattern yang Harus Dihindari #

✗ Single Stage Build #

// ✗ Compiler, pip, cache semuanya ada di image final
FROM python:3.12
RUN apt-get install -y build-essential libpq-dev
RUN pip install -r requirements.txt

Solusi: Selalu pakai multi-stage.

✗ Tidak Hapus Cache pip #

// ✗ /root/.cache/pip menambah puluhan MB
RUN pip install -r requirements.txt

Solusi: Pakai pip install --no-cache-dir.

✗ Copy Seluruh Repo Tanpa Filter #

// ✗ .git, .env, venv/ ikut ke image
COPY . /app

Solusi: .dockerignore yang tegas, dan copy file eksplisit (terutama untuk requirements.txt yang harus di-cache terpisah dari source code).

✗ Pakai python:latest #

// ✗ Build tidak deterministik, image besar
FROM python:latest

Solusi: Pin tag: python:3.12.3-slim-bookworm.

✗ Tidak Pakai Virtualenv #

// ✗ Site-packages bercampur dengan system Python
RUN pip install -r requirements.txt

Solusi: Selalu pakai virtualenv di build stage, copy ke runtime stage.

✗ Build dengan C Compiler di Runtime Image #

// ✗ Runtime image punya gcc, make, header file
FROM python:3.12-slim
RUN apt-get install -y build-essential libpq-dev
RUN pip install -r requirements.txt

Solusi: Multi-stage dengan build stage punya compiler, runtime stage ramping.

12. Contoh Dockerfile Production-Grade #

Berikut contoh Dockerfile lengkap yang menggabungkan semua best practice di atas:

# syntax=docker/dockerfile:1.7

# ==== Stage 1: Build ====
FROM python:3.12.3-slim-bookworm AS builder

WORKDIR /app

# Build dependency
RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential \
    libpq-dev \
 && rm -rf /var/lib/apt/lists/*

# Setup virtualenv
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH" \
    PIP_NO_CACHE_DIR=1 \
    PYTHONDONTWRITEBYTECODE=1

# Install dependency (terpisah dari source code untuk cache)
COPY requirements/prod.txt /tmp/requirements.txt
RUN pip install --upgrade pip \
 && pip install -r /tmp/requirements.txt

# ==== Stage 2: Runtime ====
FROM gcr.io/distroless/python3-debian12:nonroot

WORKDIR /app

# Copy virtualenv dan source code
COPY --from=builder /opt/venv /opt/venv
COPY . /app

ENV PATH="/opt/venv/bin:$PATH" \
    PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1

EXPOSE 8000

USER nonroot:nonroot
ENTRYPOINT ["/opt/venv/bin/gunicorn", \
            "config.wsgi:application", \
            "--bind", "0.0.0.0:8000", \
            "--workers", "3", \
            "--access-logfile", "-", \
            "--error-logfile", "-"]

Catatan penting:

  • distroless/python3-debian12:nonroot sudah include Python interpreter. Tidak perlu FROM python:3.12-slim di runtime.
  • PYTHONDONTWRITEBYTECODE=1 mencegah Python membuat file .pyc di container.
  • PYTHONUNBUFFERED=1 memastikan log langsung di-flush ke STDOUT (tidak di-buffer).
  • gunicorn --access-logfile - artinya log ke STDOUT.
  • ENTRYPOINT dalam exec form — signal sampai langsung ke gunicorn.

Ukuran akhir: ~100-150 MB untuk Django API dengan dependency standar.

13. Kapan Pakai Strategi Mana #

Kondisi Pilihan Alasan
API production standar slim + multi-stage Ukuran wajar, debugging能力
Production high-maturity distroless Ukuran kecil, observability solid
Aplikasi dengan banyak native dep slim + audit Butuh library, audit dependency
Serverless (Lambda container) distroless atau alpine Cold start time penting
CLI tools / scripts alpine atau slim Butuh tool untuk eksekusi
Microservice kecil distroless Image kecil, attack surface minimal

14. Checklist Review Dockerfile Python #

BASE IMAGE:
  □ Tag eksplisit (python:3.12.3-slim-bookworm, bukan latest)
  □ Runtime stage pakai image ramping (slim, alpine, atau distroless)
  □ Bukan python:latest atau python:3.12 (terlalu besar)

BUILD:
  □ Multi-stage build (build stage vs runtime stage)
  □ Virtualenv (python -m venv) di build stage
  □ pip install --no-cache-dir (hapus cache)
  □ apt-get cache dihapus (rm -rf /var/lib/apt/lists/*)
  □ Build dependency (build-essential, *-dev) hanya di build stage
  □ requirements.txt copy terpisah dari source code (untuk cache)

RUNTIME:
  □ USER nonroot
  □ PYTHONUNBUFFERED=1
  □ PYTHONDONTWRITEBYTECODE=1
  □ Log ke STDOUT (bukan file)
  □ Healthcheck (jika shell ada)
  □ ENTRYPOINT/CMD dalam exec form

UKURAN:
  □ < 250 MB untuk slim runtime
  □ < 150 MB untuk distroless runtime
  □ docker history tidak ada layer aneh yang besar

KEAMANAN:
  □ Tidak ada secret di image
  □ .dockerignore tegas
  □ Image di-scan trivy/grype
  □ Non-root user
  □ Base image up-to-date

DEPENDENCY:
  □ Production vs dev dependency terpisah
  □ Audit dependency berkala
  □ Binary wheel dipakai untuk native extension
  □ Library runtime (tanpa -dev) di runtime stage

Ringkasan #

  • Image Python ramping sangat mungkin — bukan takdir. Yang membedakan image 800 MB dengan image 100 MB adalah disiplin build boundary.
  • Realita ukuran: 50-80 MB (bundled), 90-140 MB (distroless), 180-250 MB (slim multi-stage), 350-500 MB (slim tanpa optimasi), 600-900 MB (python:latest — anti-pattern).
  • Multi-stage build dengan virtualenv adalah fondasi. Build stage punya compiler dan header; runtime stage hanya punya interpreter + virtualenv.
  • Pakai binary wheel untuk package C extension (psycopg[binary], bukan psycopg2 yang perlu compile).
  • Pisahkan dev vs prod dependencyrequirements/prod.txt vs requirements/dev.txt. Production image tidak boleh bawa test runner dan debug tools.
  • Hapus cachepip install --no-cache-dir dan rm -rf /var/lib/apt/lists/* di layer yang sama dengan instalasi.
  • Distroless untuk production mature — image lebih kecil, attack surface minimal. Tapi observability harus solid karena tidak ada shell untuk debugging.
  • Slim untuk defaultpython:3.12-slim adalah kompromi yang baik antara ukuran dan debugging能力. Cocok untuk kebanyakan production service.
  • Tag eksplisit, bukan latestpython:3.12.3-slim-bookworm. Build reproducibility penting.
  • Log ke STDOUT, bukan file — konfigurasi logging di kode aplikasi, bukan di Dockerfile.
  • .dockerignore tegas — hindari __pycache__, .git, .env, venv/, *.pyc ikut ke build context.
  • Image ramping butuh observability yang baik — log structured, metrics endpoint, healthcheck, dan signal handling. Image kecil + observability solid > image besar + debugging manual.

← Sebelumnya: Golang   Berikutnya: Java (Spring Boot) →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact