How to Build a Production-Ready Django Project Structure
This article explains why the default Django project layout is unsuitable for production, then presents a detailed, battle‑tested directory structure, split settings, environment variable management, organized apps, services, selectors, testing layout, Makefile shortcuts, and Django 5.2 considerations to help developers create maintainable, secure, and scalable Django applications.
Why the default Django layout fails
Running django-admin startproject creates a simple layout that works for tutorials but quickly becomes problematic in production: a single settings.py with many if DEBUG checks, tangled code, hard‑coded secrets, and no place for tests or documentation.
Production‑ready directory layout
The author proposes a concrete layout that separates configuration, apps, static assets, and documentation.
myproject/
├── .env.example
├── .gitignore
├── docker-compose.yml
├── Dockerfile
├── Makefile
├── README.md
├── requirements/
│ ├── base.txt
│ ├── development.txt
│ ├── production.txt
│ └── testing.txt
├── config/
│ ├── __init__.py
│ ├── settings/
│ │ ├── __init__.py
│ │ ├── base.py
│ │ ├── development.py
│ │ ├── production.py
│ │ └── testing.py
│ ├── urls.py
│ ├── wsgi.py
│ └── asgi.py
├── apps/
│ ├── __init__.py
│ ├── core/
│ │ ├── __init__.py
│ │ ├── models.py
│ │ ├── utils.py
│ │ └── exceptions.py
│ └── users/
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── models.py
│ ├── views.py
│ ├── urls.py
│ ├── serializers.py
│ ├── services.py
│ ├── selectors.py
│ └── tests/
│ ├── __init__.py
│ ├── test_models.py
│ ├── test_views.py
│ └── test_services.py
├── static/
├── media/
├── templates/
├── scripts/
├── docs/
└── manage.pySplit settings per environment
Create a config/settings package with base.py holding shared values and separate development.py, production.py, and testing.py files that import the base and override specifics. Secrets are read from environment variables using python‑decouple.
# config/settings/base.py
import os
from pathlib import Path
from decouple import config, Csv
BASE_DIR = Path(__file__).resolve().parent.parent.parent
SECRET_KEY = config('SECRET_KEY')
# ...rest of shared settings...Environment switching
Set the DJANGO_SETTINGS_MODULE variable to the desired module, either in the shell or in a .env file.
# .env
DJANGO_SETTINGS_MODULE=config.settings.development
# or production, testingOrganising apps
All reusable Django apps live under apps/. A dedicated core app stores shared models, utilities, and custom exceptions. Business logic resides in services, while query helpers live in selectors, keeping views thin.
Makefile for common commands
# Makefile
.PHONY: install run migrate test lint clean docker-up docker-down
install:
pip install -r requirements/development.txt
run:
python manage.py runserver
migrate:
python manage.py makemigrations
python manage.py migrate
test:
pytest --cov=apps --cov-report=html
lint:
ruff check apps/
ruff format apps/
docker-up:
docker-compose up -d
docker-down:
docker-compose downTesting layout
Each app contains a tests/ package with separate files for models, views, services, and selectors. A top‑level conftest.py provides shared fixtures.
Django 5.2 considerations
Long‑term support until April 2028.
Automatic model imports in the shell.
Composite primary key support (future topic).
Requires Python 3.10 or newer.
Quick bootstrap script
#!/bin/bash
mkdir -p myproject/{config/settings,apps/core,apps/users/tests,requirements,static,media,templates,scripts,docs}
cd myproject
git init
python -m venv .venv
source .venv/bin/activate
pip install django python-decouple
django-admin startproject config .
mkdir -p config/settings
mv config/settings.py config/settings/base.py
touch config/settings/__init__.py
touch config/settings/{development,production,testing}.py
touch apps/__init__.py apps/core/__init__.py apps/users/__init__.py apps/users/tests/__init__.py
touch requirements/{base,development,production,testing}.txt
touch .env .env.example .gitignore Makefile README.md
echo 'Project structure created!'Key takeaways
Split settings per environment to avoid tangled if DEBUG blocks.
Store secrets in environment variables, never in source control.
Group all apps under a single apps/ directory for clean imports.
Use a core app for shared utilities and base models.
Move business logic to services and selectors, keeping views lightweight.
Separate dependency files for each environment.
Provide a Makefile to run common tasks with a single command.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
