Django #

Django adalah web framework Python yang paling matang dan stabil. Dengan filosofi “batteries included”, Django menyertakan ORM, admin panel, sistem authentication, form handling, dan masih banyak lagi. Untuk aplikasi web dengan business logic kompleks — CMS, dashboard internal, e-commerce backend, sistem ERP — Django adalah pilihan yang sangat aman karena hampir semua yang kamu butuhkan sudah ada di dalam framework.

Docker Compose melengkapi Django dengan cara yang elegan: aplikasi, database, cache, dan message broker berjalan dalam container terpisah. Tidak perlu install Postgres atau Redis di laptop setiap developer. Tidak ada lagi “tapi di laptop saya jalan”. Artikel ini membahas setup Docker Compose untuk local development Django, dari Dockerfile untuk Python, docker-compose dengan healthcheck, hingga best practice production-readiness.

Prasyarat #

Pastikan sudah terinstall:

  • Docker dan Docker Compose versi terbaru
  • Python 3.12+ (opsional, untuk development di host)
  • Poetry atau pip (opsional)

Struktur project Django standar (single project layout):

my-django-app/
├── manage.py
├── requirements.txt
├── Dockerfile
├── Dockerfile.dev
├── docker-compose.yml
├── .env
├── .dockerignore
├── app/                       # Django project (settings)
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   ├── wsgi.py
│   └── asgi.py
├── core/                      # Main app
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── models.py
│   ├── views.py
│   ├── urls.py
│   ├── serializers.py         # DRF
│   ├── migrations/
│   └── tests.py
└── db/
    └── init/                  # SQL init scripts
        └── 01-extensions.sql

Atau dengan layout yang lebih scalable (multi-project):

my-django-app/
├── manage.py
├── requirements/
│   ├── base.txt
│   ├── dev.txt
│   └── prod.txt
├── config/                    # Project config
│   ├── settings/
│   │   ├── __init__.py
│   │   ├── base.py
│   │   ├── dev.py
│   │   └── prod.py
│   ├── urls.py
│   ├── wsgi.py
│   └── asgi.py
├── apps/
│   ├── users/
│   ├── products/
│   └── orders/
└── ...

Pemisahan requirements/base.txt, dev.txt, prod.txt adalah pola umum untuk proyek skala menengah. File base.txt berisi dependency inti, dev.txt menambahkan tooling development, prod.txt berisi driver production seperti Gunicorn.

Dockerfile untuk Production #

Untuk production, Dockerfile multi-stage dengan base image ramping.

# syntax=docker/dockerfile:1.6
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/*

# Install Python dependency ke prefix terpisah
COPY requirements/base.txt requirements.txt
RUN pip install --upgrade pip \
    && pip install --prefix=/install --no-cache-dir -r requirements.txt

# Stage kedua: runtime ramping
FROM python:3.12-slim

WORKDIR /app

# Runtime dependency OS
RUN apt-get update && apt-get install -y --no-install-recommends \
    libpq5 \
    curl \
    && rm -rf /var/lib/apt/lists/*

# Copy installed Python packages dari builder
COPY --from=builder /install /usr/local

# Copy source code
COPY . .

# Non-root user
RUN addgroup -S django && adduser -S django -G django
USER django

EXPOSE 8000

# Gunicorn untuk production
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "3"]

Image akhir berbasis python:3.12-slim (~150 MB), berisi source code + Gunicorn. Non-root user untuk keamanan.

Dockerfile untuk Development #

Untuk development, single-stage dengan auto-reload.

# Dockerfile.dev
FROM python:3.12-slim

WORKDIR /app

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PIP_NO_CACHE_DIR=1

# OS dependency untuk build Python packages
RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential \
    libpq-dev \
    curl \
    git \
    && rm -rf /var/lib/apt/lists/*

# Install Python dependency
COPY requirements/dev.txt requirements.txt
RUN pip install --upgrade pip \
    && pip install -r requirements.txt

# Source code di-mount via volume
EXPOSE 8000

# Runserver auto-reload aktif
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]

PYTHONDONTWRITEBYTECODE=1 mencegah file .pyc (tidak perlu di development, mengurangi inkonsistensi antara host dan container). PYTHONUNBUFFERED=1 membuat log langsung muncul di stdout tanpa buffering — penting untuk debug.

docker-compose.yml #

# docker-compose.yml
services:
  web:
    build:
      context: .
      dockerfile: Dockerfile.dev
    image: django-app:dev
    container_name: django-web
    command: python manage.py runserver 0.0.0.0:8000
    volumes:
      - ./:/app
    ports:
      - "8000:8000"
    environment:
      - DJANGO_SETTINGS_MODULE=config.settings.dev
      - DATABASE_URL=postgres://django:dev@db:5432/djangoapp
      - REDIS_URL=redis://cache:6379/0
      - DEBUG=1
      - SECRET_KEY=local-dev-secret-change-me
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    container_name: django-db
    environment:
      - POSTGRES_USER=django
      - POSTGRES_PASSWORD=dev
      - POSTGRES_DB=djangoapp
    volumes:
      - db-data:/var/lib/postgresql/data
      - ./db/init:/docker-entrypoint-initdb.d:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U django -d djangoapp"]
      interval: 10s
      timeout: 5s
      retries: 5
    ports:
      - "5432:5432"

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

  # Opsional: MailHog untuk testing email
  mailhog:
    image: mailhog/mailhog:latest
    container_name: django-mailhog
    ports:
      - "1025:1025"
      - "8025:8025"

  # Opsional: pgAdmin untuk UI database
  pgadmin:
    image: dpage/pgadmin4:latest
    container_name: django-pgadmin
    environment:
      - [email protected]
      - PGADMIN_DEFAULT_PASSWORD=admin
    ports:
      - "5050:80"
    depends_on:
      - db
    profiles: ["tools"]

volumes:
  db-data:
  cache-data:

Penjelasan Service #

web — service utama Django. Pakai Dockerfile.dev dengan runserver. Mount source code sebagai bind mount — setiap save file, Django otomatis reload. Port 8000 di-expose ke host.

db — PostgreSQL untuk data persisten. Healthcheck pg_isready memastikan service web tidak start sebelum database siap. Folder db/init untuk SQL script extension atau schema yang di-load sekali saat container pertama start.

cache — Redis untuk cache, session, dan Celery broker. Mode default sudah cukup untuk development.

mailhog — mock SMTP server untuk testing email. Django mengirim email ke MailHog di port 1025, dan UI web di http://localhost:8025 menampilkan email yang “dikirim”.

pgadmin — opsional (aktifkan dengan docker compose --profile tools up -d). UI web untuk Postgres.

Konfigurasi Settings #

Pisahkan settings per environment untuk clean code.

config/settings/base.py:

import os
from pathlib import Path
from urllib.parse import urlparse

BASE_DIR = Path(__file__).resolve().parent.parent.parent

SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", "insecure-default-key-change-me")
DEBUG = os.environ.get("DJANGO_DEBUG", "0") == "1"
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "localhost,127.0.0.1").split(",")

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "django.contrib.postgres",

    # Third-party
    "rest_framework",
    "django_celery_results",

    # Local
    "apps.users",
    "apps.products",
]

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]

ROOT_URLCONF = "config.urls"

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [BASE_DIR / "templates"],
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.debug",
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
            ],
        },
    },
]

WSGI_APPLICATION = "config.wsgi.application"
ASGI_APPLICATION = "config.asgi.application"

# Database
DATABASE_URL = os.environ.get("DATABASE_URL", "postgres://django:dev@db:5432/djangoapp")
db_params = urlparse(DATABASE_URL)
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": db_params.path.lstrip("/"),
        "USER": db_params.username,
        "PASSWORD": db_params.password,
        "HOST": db_params.hostname,
        "PORT": db_params.port or "5432",
    }
}

# Cache
REDIS_URL = os.environ.get("REDIS_URL", "redis://cache:6379/0")
CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.redis.RedisCache",
        "LOCATION": REDIS_URL,
    }
}

# Celery
CELERY_BROKER_URL = REDIS_URL
CELERY_RESULT_BACKEND = "django-db"
CELERY_CACHE_BACKEND = "django-cache"
CELERY_TIMEZONE = "UTC"
CELERY_TASK_TRACK_STARTED = True
CELERY_TASK_TIME_LIMIT = 30 * 60

# Internationalization
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True

# Static files
STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"

# Default primary key field type
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

# Email untuk development
EMAIL_BACKEND = os.environ.get(
    "EMAIL_BACKEND",
    "django.core.mail.backends.console.EmailBackend",
)
EMAIL_HOST = os.environ.get("EMAIL_HOST", "mailhog")
EMAIL_PORT = int(os.environ.get("EMAIL_PORT", "1025"))

config/settings/dev.py:

from .base import *  # noqa

DEBUG = True

# Detail error page
INTERNAL_IPS = ["127.0.0.1", "host.docker.internal"]

# Django Debug Toolbar
if os.environ.get("ENABLE_DEBUG_TOOLBAR", "1") == "1":
    INSTALLED_APPS += ["debug_toolbar"]  # noqa
    MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware")  # noqa
    import socket
    hostname, _, ips = socket.gethostbyname_ex(socket.gethostname())
    INTERNAL_IPS = [ip[: ip.rfind(".")] + ".1" for ip in ips] + ["127.0.0.1", "10.0.2.2"]

# Console email backend kalau tidak pakai MailHog
# EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"

# Allow all hosts di dev
ALLOWED_HOSTS = ["*"]

# Print SQL queries di dev
LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,
    "handlers": {
        "console": {"class": "logging.StreamHandler"},
    },
    "loggers": {
        "django.db.backends": {
            "handlers": ["console"],
            "level": "DEBUG",
        },
    },
}

Models, Views, dan Serializer #

Model (apps/users/models.py):

import uuid
from django.contrib.auth.models import AbstractUser
from django.db import models


class User(AbstractUser):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    email = models.EmailField(unique=True)
    full_name = models.CharField(max_length=255)
    bio = models.TextField(blank=True)
    avatar = models.ImageField(upload_to="avatars/", blank=True, null=True)
    is_verified = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    USERNAME_FIELD = "email"
    REQUIRED_FIELDS = ["username", "full_name"]

    class Meta:
        db_table = "users"
        indexes = [
            models.Index(fields=["email"]),
            models.Index(fields=["created_at"]),
        ]

    def __str__(self):
        return self.email


class Post(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name="posts")
    title = models.CharField(max_length=200)
    slug = models.SlugField(max_length=200, unique=True)
    content = models.TextField()
    is_published = models.BooleanField(default=False)
    published_at = models.DateTimeField(blank=True, null=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = "posts"
        ordering = ["-created_at"]
        indexes = [
            models.Index(fields=["slug"]),
            models.Index(fields=["is_published", "-published_at"]),
        ]

Serializer (apps/users/serializers.py):

from rest_framework import serializers
from .models import User


class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ["id", "email", "full_name", "bio", "is_verified", "created_at"]
        read_only_fields = ["id", "is_verified", "created_at"]


class RegisterSerializer(serializers.ModelSerializer):
    password = serializers.CharField(write_only=True, min_length=8)
    password_confirm = serializers.CharField(write_only=True)

    class Meta:
        model = User
        fields = ["email", "username", "full_name", "password", "password_confirm"]

    def validate(self, data):
        if data["password"] != data["password_confirm"]:
            raise serializers.ValidationError("passwords do not match")
        return data

    def create(self, validated_data):
        validated_data.pop("password_confirm")
        password = validated_data.pop("password")
        user = User(**validated_data)
        user.set_password(password)
        user.save()
        return user

ViewSet (apps/users/views.py):

from rest_framework import viewsets, status, permissions
from rest_framework.decorators import action
from rest_framework.response import Response
from .models import User
from .serializers import UserSerializer, RegisterSerializer


class UserViewSet(viewsets.ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly]
    lookup_field = "id"

    def get_permissions(self):
        if self.action == "create":
            return [permissions.AllowAny()]
        return super().get_permissions()

    def create(self, request, *args, **kwargs):
        serializer = RegisterSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        user = serializer.save()
        return Response(
            UserSerializer(user).data,
            status=status.HTTP_201_CREATED,
        )

    @action(detail=False, methods=["get"], permission_classes=[permissions.IsAuthenticated])
    def me(self, request):
        serializer = self.get_serializer(request.user)
        return Response(serializer.data)

URL (apps/users/urls.py):

from rest_framework.routers import DefaultRouter
from .views import UserViewSet

router = DefaultRouter()
router.register(r"users", UserViewSet, basename="user")

urlpatterns = router.urls

Root URL (config/urls.py):

from django.contrib import admin
from django.urls import path, include
from django.http import JsonResponse


def healthz(request):
    return JsonResponse({"status": "ok"})


urlpatterns = [
    path("admin/", admin.site.urls),
    path("healthz", healthz),
    path("api/v1/", include("apps.users.urls")),
]

Database Migration #

Django punya sistem migration built-in yang sangat solid.

# Buat migration setelah ubah model
docker compose exec web python manage.py makemigrations

# Apply migration
docker compose exec web python manage.py migrate

# Lihat status migration
docker compose exec web python manage.py showmigrations

# Buat superuser untuk admin
docker compose exec web python manage.py createsuperuser

Untuk environment production, migrate di-run saat container start sebagai entrypoint script.

entrypoint.sh:

#!/bin/sh
set -e

echo "Waiting for database..."
while ! nc -z db 5432; do
  sleep 1
done
echo "Database ready."

echo "Applying migrations..."
python manage.py migrate --noinput

echo "Collecting static files..."
python manage.py collectstatic --noinput

echo "Starting server..."
exec "$@"
# Tambahkan di Dockerfile.prod
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000"]

Django Debug Toolbar #

Django Debug Toolbar adalah tool wajib di development. Menampilkan SQL queries, request/response headers, template context, dan banyak lagi.

requirements/dev.txt:

-r base.txt
django-debug-toolbar==4.2.0
django-extensions==3.2.3
ipython==8.12.0

Pastikan middleware dan URL-nya terdaftar. Di dev.py:

INSTALLED_APPS += ["debug_toolbar"]
MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware")

import socket
hostname, _, ips = socket.gethostbyname_ex(socket.gethostname())
INTERNAL_IPS = [ip[: ip.rfind(".")] + ".1" for ip in ips] + ["127.0.0.1", "10.0.2.2", "host.docker.internal"]

Di config/urls.py:

if settings.DEBUG:
    import debug_toolbar
    urlpatterns = [path("__debug__/", include(debug_toolbar.urls))] + urlpatterns

Akses di http://localhost:8000 — toolbar muncul di kanan atas. Klik untuk expand dan lihat SQL queries, template, signals, dll.

Build dan Run #

# Build image
docker compose build

# Jalankan semua service
docker compose up -d

# Lihat log
docker compose logs -f web

# Jalankan migration
docker compose exec web python manage.py migrate

# Buat superuser
docker compose exec web python manage.py createsuperuser

# Shell Django
docker compose exec web python manage.py shell

# Collect static files (untuk production)
docker compose exec web python manage.py collectstatic

# Stop
docker compose down

# Reset total
docker compose down -v

Akses:

  • Web: http://localhost:8000
  • Admin: http://localhost:8000/admin/
  • API: http://localhost:8000/api/v1/
  • PostgreSQL: localhost:5432
  • Redis: localhost:6379
  • MailHog UI: http://localhost:8025
  • pgAdmin (opsional): http://localhost:5050 (login: [email protected] / admin)

Test endpoint:

# Register user
curl -X POST http://localhost:8000/api/v1/users/ \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "username": "rina",
    "full_name": "Rina Wati",
    "password": "password123",
    "password_confirm": "password123"
  }'

# List users
curl http://localhost:8000/api/v1/users/

# Get current user (perlu auth)
curl -H "Authorization: Bearer <token>" http://localhost:8000/api/v1/users/me/

Testing dengan Pytest-Django #

Pytest lebih ergonomis dari unittest bawaan Django.

requirements/dev.txt:

-r base.txt
pytest==8.0.0
pytest-django==4.7.0
pytest-cov==4.1.0
factory-boy==3.3.0

pytest.ini:

[pytest]
DJANGO_SETTINGS_MODULE = config.settings.test
python_files = test_*.py *_test.py tests.py
addopts = -ra -q --strict-markers
testpaths = apps

apps/users/tests/test_models.py:

import pytest
from apps.users.models import User


@pytest.mark.django_db
def test_create_user():
    user = User.objects.create_user(
        email="[email protected]",
        username="test",
        full_name="Test User",
        password="password123",
    )
    assert user.email == "[email protected]"
    assert user.check_password("password123")
    assert not user.is_verified


@pytest.mark.django_db
def test_user_str():
    user = User.objects.create_user(
        email="[email protected]",
        username="test",
        full_name="Test",
        password="password123",
    )
    assert str(user) == "[email protected]"

apps/users/tests/test_api.py:

import pytest
from rest_framework.test import APIClient


@pytest.fixture
def client():
    return APIClient()


@pytest.mark.django_db
def test_register_user(client):
    response = client.post(
        "/api/v1/users/",
        {
            "email": "[email protected]",
            "username": "test",
            "full_name": "Test",
            "password": "password123",
            "password_confirm": "password123",
        },
        format="json",
    )
    assert response.status_code == 201
    assert response.data["email"] == "[email protected]"
    assert "id" in response.data
docker compose exec web pytest

Kapan Django Tepat, Kapan Tidak #

Pakai Django jika:
  ✓ Aplikasi web dengan banyak model data
  ✓ Butuh admin panel out-of-the-box
  ✓ Sistem authentication kompleks
  ✓ Aplikasi tradisional (server-rendered, form-heavy)
  ✓ Tim yang sudah familiar dengan Django

Hindari Django jika:
  ✗ Hanya butuh API microservice ringan (pakai FastAPI/Flask)
  ✗ Real-time app dengan WebSocket berat
  ✗ Frontend SPA murni (Django jadi overkill)
  ✗ Startup butuh iterasi cepat tanpa admin/ORM

Django sangat kuat tapi juga membawa banyak asumsi. Untuk API microservice yang sangat ringan, FastAPI atau Flask lebih cocok. Untuk SPA, Django REST Framework + React/Vue adalah kombinasi umum — tapi pastikan kamu memang butuh ORM dan admin.

Best Practice #

Pisahkan Settings per Environment #

Jangan hardcode konfigurasi di satu settings.py. Pakai base.py + dev.py + prod.py + test.py. Override via environment variable.

Pakai Environment Variable untuk Secret #

SECRET_KEY, password database, API key — semua dari environment. Sediakan .env.example di repo, tapi .env di-.gitignore.

Pakai Custom User Model #

Sejak awal proyek, definisikan custom User model yang extend AbstractUser. Mengganti user model setelah aplikasi jalan itu mahal.

# settings/base.py
AUTH_USER_MODEL = "users.User"

Migration di Version Control #

Selalu commit file migration ke Git. Jangan generate migration di production — selalu develop dulu, test, baru deploy.

Index di Query yang Sering Dipakai #

Untuk field yang sering di-filter atau di-sort, tambahkan db_index=True atau Meta.indexes. Ini mencegah full table scan.

Healthcheck dan Retry #

Database harus punya healthcheck. Service web pakai depends_on: condition: service_healthy. Tambahkan retry loop atau entrypoint script yang tunggu database siap.

Bind Mount Source Code di Dev #

Mount source code ke container. Django runserver akan auto-reload setiap save file. Untuk production, COPY source ke image final.

Pakai MailHog untuk Testing Email #

Jangan kirim email sungguhan di development. Pakai MailHog (atau MailPit di Docker) untuk capture email dan lihat di UI web.

Troubleshooting #

Port 8000 Sudah Dipakai #

lsof -i :8000
# atau
netstat -ano | findstr :8000

Stop proses atau ubah port mapping.

Migration Gagal #

# Reset database
docker compose down -v
docker compose up -d
docker compose exec web python manage.py migrate

Static Files 404 #

Di development, Django serve static files otomatis. Di production, jalankan collectstatic dan serve dari Nginx atau CDN.

Hot Reload Tidak Jalan #

Pastikan bind mount include seluruh project. Cek log untuk error Python — kadang ada error import yang阻止 reload.

Database Connection Refused #

Pakai depends_on: condition: service_healthy. Tambahkan entrypoint script yang tunggu database. Atau pakai wait-for-it.sh.

Ringkasan #

  • Django ideal untuk aplikasi web dengan business logic kompleks — ORM, admin, dan auth out-of-the-box.
  • Dockerfile multi-stage untuk production: builder stage install dependency, runtime stage ramping. Image < 200 MB.
  • Dockerfile.dev untuk development: full Python + tools. Pakai runserver untuk auto-reload.
  • Pisahkan settings per environment: base.py + dev.py + prod.py + test.py. Override via environment variable.
  • Pakai custom User model sejak awal. Mengganti model setelah aplikasi jalan itu mahal.
  • Healthcheck untuk service dependent: Postgres pg_isready, Redis redis-cli ping. Pakai depends_on: condition: service_healthy.
  • MailHog untuk testing email di development. UI di http://localhost:8025.
  • Django Debug Toolbar untuk SQL queries, request/response inspection, dan template debugging.
  • Pytest-Django untuk testing. @pytest.mark.django_db untuk test yang akses database. Factory-boy untuk fixture.
  • Migration di version control selalu commit. Generate di dev, apply di prod.
  • Index di query yang sering dipakai untuk performa. db_index=True atau Meta.indexes.
  • Best practice: pisahkan settings, env variable untuk secret, custom User model, healthcheck, bind mount di dev, MailHog untuk email.
  • Alternatif: Flask untuk API ringan, FastAPI untuk API modern dengan type hints, Pylons/Pyramid untuk fleksibilitas.
  • Gunakan Django jika butuh ORM, admin, dan form handling. Hindari jika hanya butuh API microservice ringan.

← Sebelumnya: Chi   Berikutnya: Flask →

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