Modern Django project setup for 2026.

Bootstrap

uv init myapp
cd myapp
uv add django djangorestframework django-environ psycopg[binary] gunicorn whitenoise \
  celery redis django-redis sentry-sdk structlog python-json-logger drf-spectacular \
  djangorestframework-simplejwt django-cors-headers

uv add --dev pytest pytest-django factory-boy ruff mypy django-stubs django-debug-toolbar

Layout

myapp/
├── pyproject.toml
├── manage.py
├── Dockerfile
├── docker-compose.yml
├── .env.example
├── config/
│   ├── settings/
│   │   ├── base.py
│   │   ├── dev.py
│   │   └── prod.py
│   ├── urls.py
│   ├── wsgi.py
│   ├── asgi.py
│   └── celery.py
├── apps/
│   ├── accounts/
│   ├── blog/
│   └── api/
└── tests/

settings/base.py

import environ
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent.parent
env = environ.Env()
environ.Env.read_env(BASE_DIR / ".env")

SECRET_KEY = env("SECRET_KEY")
DEBUG = env.bool("DEBUG", default=False)
ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", default=[])

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    
    "rest_framework",
    "rest_framework_simplejwt",
    "corsheaders",
    "drf_spectacular",
    
    "apps.accounts",
    "apps.blog",
]

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "whitenoise.middleware.WhiteNoiseMiddleware",
    "corsheaders.middleware.CorsMiddleware",
    "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",
]

DATABASES = {"default": env.db()}
CACHES = {"default": env.cache()}

AUTH_USER_MODEL = "accounts.User"

STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"

MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"

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

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework_simplejwt.authentication.JWTAuthentication",
    ],
    "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
    "PAGE_SIZE": 20,
    "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
    "DEFAULT_THROTTLE_RATES": {"anon": "100/hour", "user": "1000/hour"},
}

CELERY_BROKER_URL = env("REDIS_URL")
CELERY_RESULT_BACKEND = env("REDIS_URL")
CELERY_TASK_ACKS_LATE = True
CELERY_WORKER_PREFETCH_MULTIPLIER = 1

settings/dev.py

from .base import *

DEBUG = True
ALLOWED_HOSTS = ["*"]

INSTALLED_APPS += ["debug_toolbar"]
MIDDLEWARE = ["debug_toolbar.middleware.DebugToolbarMiddleware"] + MIDDLEWARE
INTERNAL_IPS = ["127.0.0.1"]

CORS_ALLOW_ALL_ORIGINS = True

settings/prod.py

from .base import *
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration

DEBUG = False
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True

sentry_sdk.init(
    dsn=env("SENTRY_DSN"),
    environment="production",
    traces_sample_rate=0.1,
    integrations=[DjangoIntegration()],
)

.env.example

DJANGO_SETTINGS_MODULE=config.settings.dev
SECRET_KEY=change-me
DEBUG=1
DATABASE_URL=postgres://postgres:postgres@db:5432/myapp
REDIS_URL=redis://redis:6379/0
ALLOWED_HOSTS=*
SENTRY_DSN=

Dockerfile

FROM python:3.13-slim AS builder
WORKDIR /app
RUN pip install uv
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev

FROM python:3.13-slim
WORKDIR /app
RUN apt-get update && apt-get install -y libpq5 curl && rm -rf /var/lib/apt/lists/*
RUN useradd -u 1000 -m app

COPY --from=builder --chown=app:app /app/.venv ./.venv
COPY --chown=app:app . .

ENV PATH=/app/.venv/bin:$PATH PYTHONUNBUFFERED=1

USER app
EXPOSE 8000

HEALTHCHECK --interval=30s --timeout=3s CMD curl -fsS http://localhost:8000/health/ || exit 1

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

docker-compose.yml

services:
  web:
    build: .
    ports: ["8000:8000"]
    env_file: .env
    depends_on: [db, redis]
    volumes: [".:/app"]
  
  worker:
    build: .
    command: celery -A config worker -l info
    env_file: .env
    depends_on: [db, redis]
  
  beat:
    build: .
    command: celery -A config beat -l info
    env_file: .env
    depends_on: [db, redis]
  
  db:
    image: postgres:16
    environment:
      POSTGRES_DB: myapp
      POSTGRES_PASSWORD: postgres
    volumes: ["postgres_data:/var/lib/postgresql/data"]
  
  redis:
    image: redis:7-alpine

volumes:
  postgres_data:

pyproject.toml essentials

[project]
name = "myapp"
version = "0.1.0"
requires-python = ">=3.13"

[tool.ruff]
target-version = "py313"
line-length = 100

[tool.ruff.lint]
select = ["E", "F", "I", "B", "DJ"]

[tool.mypy]
python_version = "3.13"
plugins = ["mypy_django_plugin.main"]

[tool.django-stubs]
django_settings_module = "config.settings.dev"

[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "config.settings.dev"
addopts = "-ra --reuse-db"

.github/workflows/ci.yml

name: CI
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env: { POSTGRES_PASSWORD: postgres }
        ports: ["5432:5432"]
        options: --health-cmd pg_isready
      redis:
        image: redis:7
        ports: ["6379:6379"]
    
    env:
      DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres
      REDIS_URL: redis://localhost:6379/0
      SECRET_KEY: test
      DJANGO_SETTINGS_MODULE: config.settings.dev
    
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/setup-uv@v3
      - run: uv sync
      - run: uv run ruff check .
      - run: uv run mypy .
      - run: uv run python manage.py migrate
      - run: uv run pytest --cov

scripts (Makefile)

.PHONY: dev test lint migrate

dev:
	docker compose up

test:
	uv run pytest

lint:
	uv run ruff check . && uv run mypy .

migrate:
	uv run python manage.py migrate

Conventions

  • One app per bounded domain.
  • Custom User from day 1.
  • DRF + JWT for API.
  • Celery for background work.
  • Settings split: base/dev/prod.
  • Tests in pytest.
  • All migrations committed.
  • python manage.py check --deploy passes in CI for prod.

Read this next

That’s 20 Django cheatsheets. Next category: Docker.

If you want my full Django starter (uv + DRF + Celery + Docker + CI), it’s at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .