The Skeleton of Python-Django Project Structure

Building a Django project from scratch can feel like moving into a house with no furniture—you have the walls, but you need to arrange the rooms for things to actually work.

Here is a professional, "best-practices" skeleton for a scalable Django project.

Project Folder Structure

A production-ready structure separates your configuration from your actual apps and keeps your environment clean.

my_project/
├── manage.py
├── .env                # Secret environment variables
├── .gitignore          # Files to exclude from Git
├── requirements.txt    # Project dependencies
├── core/               # Project Configuration (The "Brain")
│   ├── __init__.py
│   ├── settings.py     # Base settings
│   ├── urls.py         # Main URL routing
│   └── wsgi.py         # Deployment entry point
├── apps/               # Folder for your custom apps
│   ├── __init__.py
│   └── users/          # Example app (Custom User Model)
│       ├── migrations/
│       ├── models.py
│       ├── views.py
│       └── urls.py
├── static/             # CSS, JS, Images
├── templates/          # Global HTML templates
└── media/              # User-uploaded files

Installation Steps

Before writing code, you need a clean sandbox.

  1. Create a Virtual Environment: Keep your global Python installation clean. python -m venv venv

  2. Activate it:

    • Windows: venv\Scripts\activate

    • Mac/Linux: source venv/bin/activate

  3. Install Django: pip install django python-dotenv

  4. Initialize Project: django-admin startproject core . (The dot at the end prevents an extra nested folder).

Key Configuration Files

.env (Security First)

Never hardcode passwords or secret keys in your settings.

Plaintext

DEBUG=True
SECRET_KEY=your-super-secret-key
DATABASE_URL=postgres://user:password@localhost:5432/dbname

settings.py (The Roadmap)

You need to tell Django where to look for your apps and templates.

Python

import os
from pathlib import Path
from dotenv import load_dotenv

load_dotenv() # Loads variables from .env

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

SECRET_KEY = os.getenv('SECRET_KEY')
DEBUG = os.getenv('DEBUG') == 'True'

# Add your apps here
INSTALLED_APPS = [
    ...
    'apps.users', 
]

# Tell Django where global templates are
TEMPLATES = [{
    'DIRS': [BASE_DIR / 'templates'],
    ...
}]

# Configure Static and Media files
STATIC_URL = 'static/'
STATICFILES_DIRS = [BASE_DIR / 'static']
MEDIA_URL = 'media/'
MEDIA_ROOT = BASE_DIR / 'media'

Why This Structure?

  • Scalability: Putting apps inside an apps/ folder keeps the root directory from becoming a cluttered mess as the project grows.

  • Security: Using .env ensures you don't accidentally leak your database credentials on GitHub.

  • Organization: Separating static/ (your files) from media/ (user files) is essential for modern web hosting (like AWS S3).

Setting up a Custom User Model at the very beginning is the single most important "pro-tip" for Django. If you use the default user and decide you want to use Email instead of Username to log in later, it requires a painful database migration.

Here is how to wire it up correctly.

Create the User App

If you haven't already, create the app folder: python manage.py startapp users (Then move it into your apps/ folder if you are using that structure).

The Model Logic (apps/users/models.py)

We inherit from AbstractUser so we keep all the standard features (password hashing, permissions) but gain the ability to add our own fields.

Python

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

class CustomUser(AbstractUser):
    # Add any extra fields here
    email = models.EmailField(unique=True)
    bio = models.TextField(blank=True, null=True)
    birth_date = models.DateField(null=True, blank=True)

    # Use email for login instead of username
    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = ['username'] 

    def __str__(self):
        return self.email

Update Project Settings (core/settings.py)

Django needs to be told to stop looking at the default user table and look at yours instead.

Python

 

# Add this anywhere in settings.py
AUTH_USER_MODEL = 'users.CustomUser'

Admin Configuration (apps/users/admin.py)

To make sure your new user shows up nicely in the Django Admin panel, you need to register it.

Python

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import CustomUser

class CustomUserAdmin(UserAdmin):
    model = CustomUser
    # Define which fields to display in the admin list view
    list_display = ['email', 'username', 'is_staff',]

admin.site.register(CustomUser, CustomUserAdmin)

Finalize the Database

Now that the code is ready, you need to tell the database to create the tables.

  1. Make migrations: python manage.py makemigrations users

  2. Apply migrations: python manage.py migrate

  3. Create your first user: python manage.py createsuperuser

Why this matters

By doing this now, you've future-proofed your app. Whether you want to add a profile picture, a subscription tier, or social media links to your users later, you can do it in models.py without breaking your entire database.

Now that your database and user model are ready, we need to wire up the "plumbing" so that when you visit http://127.0.0.1:8000, you actually see a webpage instead of a 404 error.

Create a "Pages" App

For general views like a Home page, About page, or Contact page, it’s best to create a dedicated app.

  1. Run: python manage.py startapp pages

  2. Add to Settings: Open core/settings.py and add 'pages', to your INSTALLED_APPS list.

Create the View (pages/views.py)

This is the logic that handles the request.

Python

from django.views.generic import TemplateView

class HomePageView(TemplateView):
    template_name = 'home.html'

Define the URL Routing

We need to map the web address to that view. We do this in two steps to keep things modular.

Step A: App-level URLs (pages/urls.py)

Create a new file named urls.py inside your pages folder:

Python

from django.urls import path
from .views import HomePageView

urlpatterns = [
    path('', HomePageView.as_view(), name='home'),
]

Step B: Project-level URLs (core/urls.py)

Tell the main project to look at your app's URLs.

Python

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

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('pages.urls')), # This connects your pages app
]

Create the Template (templates/home.html)

In your root folder (as defined in your BASE_DIR), create a templates folder and add home.html:

HTML

<!DOCTYPE html>
<html>
<head>
    <title>Django Skeleton</title>
</head>
<body>
    <h1>Welcome to your Django Project!</h1>
    <p>The skeleton is alive.</p>
    
    {% if user.is_authenticated %}
        <p>Hello, {{ user.email }}!</p>
    {% else %}
        <p>You are not logged in.</p>
    {% endif %}
</body>
</html>

Run the Server

Now, fire it up! python manage.py runserver

Navigate to http://127.0.0.1:8000/. You should see your "Welcome" message. If you log in via the admin panel (/admin) and come back here, the template will recognize you by your email!

What's next?

The skeleton is now fully functional with a custom user system and a homepage.

Smart move. In Django, the DRY (Don't Repeat Yourself) principle is king. Instead of writing the <html> and <head> tags for every single page, you create one Base Template and "inject" your unique content into it.

I'll show you how to set this up using Bootstrap-5 for instant styling.

Create the Base Template (templates/base.html)

This file acts as the "frame" for your entire website.

HTML

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}Django App{% endblock %}</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">

    <nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-4">
        <div class="container">
            <a class="navbar-brand" href="{% url 'home' %}">Django Skeleton</a>
            <div class="navbar-nav ms-auto">
                {% if user.is_authenticated %}
                    <span class="nav-link text-white">Hi, {{ user.username }}</span>
                    <a class="nav-item nav-link" href="/admin/logout/">Logout</a>
                {% else %}
                    <a class="nav-item nav-link" href="/admin/login/">Login</a>
                {% endif %}
            </div>
        </div>
    </nav>

    <main class="container">
        {% block content %}
        {% endblock %}
    </main>

    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

Update your Home Page (templates/home.html)

Now, your home.html becomes much shorter because it "extends" the base.

HTML

{% extends 'base.html' %}

{% block title %}Home | My Project{% endblock %}

{% block content %}
<div class="p-5 mb-4 bg-white rounded-3 border shadow-sm">
    <div class="container-fluid py-5">
        <h1 class="display-5 fw-bold">Success!</h1>
        <p class="col-md-8 fs-4">Your Django skeleton is now using a master base template with Bootstrap styling.</p>
        <hr class="my-4">
        <p>This page only contains the unique "middle" content. The header and footer are inherited.</p>
        <a class="btn btn-primary btn-lg" href="https://docs.djangoproject.com/" target="_blank">Read Django Docs</a>
    </div>
</div>
{% endblock %}

How it Works

  1. {% extends 'base.html' %}: Tells Django to grab the code from the base file first.

  2. {% block content %}: Acts as a placeholder. Anything you put between these tags in home.html gets teleported into the same tags in base.html.

Next Step: Static Files

To make your site truly custom, you'll eventually want your own CSS file.

  1. Create a folder: static/css/

  2. Create a file: static/css/style.css

  3. Add {% load static %} to the top of base.html and link it: <link rel="stylesheet" href="{% static 'css/style.css' %}">

To wrap up your project skeleton, you need these two files to ensure portability and security. Without a .gitignore, you’ll accidentally upload thousands of library files and sensitive passwords to the web.

The requirements.txt File

This file lists every library your project needs. When someone else (or your deployment server) downloads your code, they just run one command to install everything.

Create requirements.txt in your root folder:

Plaintext

django>=5.0,<6.0
python-dotenv
psycopg2-binary  # For PostgreSQL (if using)
pillow           # For image handling
gunicorn         # For production deployment

Professional-Tip: You can generate this automatically from your current environment by running: pip freeze > requirements.txt

The .gitignore File

This tells Git which files to ignore. You never want to upload your virtual environment (venv/), your local database (db.sqlite3), or your secret keys (.env).

Create .gitignore in your root folder:

Plaintext

# Environments
.venv/
venv/
ENV/

# Secrets
.env

# Databases
*.sqlite3

# Python cache
__pycache__/
*.py[cod]
*$py.class

# Static/Media
/static_root/
/media/

# OS files
.DS_Store
Thumbs.db

Best Practices Checklist

Before you start building features, check these off:

Task Status Reason
Secret Key Moved to .env Prevents hacking if code is public.
Custom User Created & Migrated Avoids database rebuilds later.
Base Template Set up Ensures UI consistency across all pages.
Requirements Documented Makes the project "installable" elsewhere.

Summary of Commands

To hand this off to a teammate or move to a new machine:

  1. git clone <repository_url>

  2. python -m venv venv

  3. source venv/bin/activate

  4. pip install -r requirements.txt

  5. python manage.py migrate

  6. python manage.py runserver

To make your project production-ready and keep your storage tidy, you should implement these two "utility" powerhouses.

WhiteNoise allows your Python app to serve its own static files (CSS/JS) efficiently, and Django-Cleanup ensures that when you delete a user or a post, their uploaded images are actually deleted from your hard drive too.

Efficient Static Files with WhiteNoise

In production, Django normally refuses to serve static files. WhiteNoise fixes this by wrapping your application so it can serve them itself, with compression and caching.

Installation

pip install whitenoise

Configuration (core/settings.py)

Add the middleware immediately after SecurityMiddleware:

Python

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware',  # <--- Add this!
    # ...
]

# Add this at the bottom to enable compression and permanent caching
STORAGES = {
    "staticfiles": {
        "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
    },
}

Automatic File Cleanup with Django-Cleanup

By default, if you delete a User who has a profile picture, Django deletes the database record but leaves the file on your server. This leads to "orphaned files" that eat up space.

Installation

pip install django-cleanup

Configuration (core/settings.py)

Simply add it to your apps:

Python

INSTALLED_APPS = [
    ...
    'django_cleanup.apps.CleanupConfig', # <--- Add this!
]

That’s it. Now, whenever a model instance is deleted, its associated files are automatically scrubbed from your storage.

How the Lifecycle Works

  1. Development: You write CSS in static/ and upload images to media/.

  2. Deployment: You run python manage.py collectstatic. WhiteNoise gathers everything into a staticfiles/ folder, compresses them (Gzip/Brotli), and gives them unique "fingerprint" names for caching.

  3. Runtime: When a user visits your site, WhiteNoise serves the compressed CSS instantly.

  4. Maintenance: When you delete an old blog post, Django-Cleanup triggers a signal that deletes the specific .jpg from your media/ folder.

Final Project Check

Your requirements.txt should now look like this:

Plaintext

django>=5.0
python-dotenv
whitenoise
django-cleanup

Your project skeleton is now a robust, professional-grade foundation.

The Django Debug Toolbar (DjDT) is essentially a "backstage-pass" for your website. It adds a side panel to your browser that shows you exactly what’s happening under the hood for every page load—most importantly, it tells you which database queries are slow and why.

Installation

First, add it to your environment: pip install django-debug-toolbar

Configuration (core/settings.py)

Since the toolbar shows sensitive data, it should only run in development.

Step A: Apps and Middleware

Add debug_toolbar to your apps and its middleware as high as possible (but after any encoding middleware like GZip).

Python

INSTALLED_APPS = [
    ...
    'debug_toolbar',
]

MIDDLEWARE = [
    'debug_toolbar.middleware.DebugToolbarMiddleware', # <--- Top of the list
    'django.middleware.security.SecurityMiddleware',
    ...
]

Step B: Internal IPs

The toolbar will only show up if your IP address is in the INTERNAL_IPS list. For local development, this is your localhost.

Python

 

INTERNAL_IPS = [
    "127.0.0.1",
]

URL Routing (core/urls.py)

You need to provide a path for the toolbar's own logic to work. Wrap this in a DEBUG check so it’s never active in production.

Python

from django.conf import settings
from django.urls import include, path

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('pages.urls')),
]

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

Why Use It?

Once configured, you'll see a small "DjDT" tab on the right side of your browser. Here is what it reveals:

  • SQL Panel: Shows every database query triggered by that page. It highlights "duplicate" queries—allowing you to fix N+1 performance issues immediately.

  • Timer Panel: Breaks down how long the server spent processing vs. how long the browser took to render.

  • Templates Panel: Lists every template and "context variable" used on the page.

Your Skeleton is Now "Elite"

You have a custom user model, a base template with Bootstrap, secure configuration, static/media file management, and a performance debugger.

To wrap this up, we’ll build the "bridge" between your database and your users. Since we added a bio and birth_date to your Custom User Model earlier, users need a way to update that information without going into the Admin panel.

Here is the final piece of the puzzle: a Profile Update system.

Create the Form (apps/users/forms.py)

Django’s UserChangeForm is great, but it includes too many fields for a regular user. We’ll create a focused version.

Python

from django import forms
from .models import CustomUser

class ProfileUpdateForm(forms.ModelForm):
    class Meta:
        model = CustomUser
        fields = ['username', 'email', 'bio', 'birth_date']
        widgets = {
            'birth_date': forms.DateInput(attrs={'type': 'date'}),
        }

Create the View (apps/users/views.py)

We’ll use a LoginRequiredMixin to ensure only logged-in users can access this page.

Python

from django.urls import reverse_lazy
from django.views.generic import UpdateView
from django.contrib.auth.mixins import LoginRequiredMixin
from .models import CustomUser
from .forms import ProfileUpdateForm

class ProfileView(LoginRequiredMixin, UpdateView):
    model = CustomUser
    form_class = ProfileUpdateForm
    template_name = 'profile.html'
    success_url = reverse_lazy('profile')

    def get_object(self):
        return self.request.user # Ensures they only edit their own profile

The Profile Template (templates/profile.html)

Because we used Bootstrap in our base.html, we can make this look professional with very little code.

HTML

{% extends 'base.html' %}

{% block content %}
<div class="row justify-content-center">
    <div class="col-md-6">
        <div class="card shadow">
            <div class="card-header bg-primary text-white">
                <h4 class="mb-0">Update Profile</h4>
            </div>
            <div class="card-body">
                <form method="post">
                    {% csrf_token %}
                    {{ form.as_p }}
                    <button type="submit" class="btn btn-success w-100">Save Changes</button>
                </form>
            </div>
        </div>
    </div>
</div>
{% endblock %}

Final Routing (core/urls.py)

Add the profile path to your main URL configuration.

Python

from apps.users.views import ProfileView

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('pages.urls')),
    path('profile/', ProfileView.as_view(), name='profile'), # Add this
]

Project Wrap-Up

You now have a production-ready Python-Django skeleton. Here is the final "map" of what you've built:

  • Security: .env for secrets and .gitignore for safety.

  • Database: A flexible Custom User Model that scales with you.

  • Performance: WhiteNoise for fast file serving and Debug Toolbar to find bottlenecks.

  • UX: Bootstrap 5 integrated via a master base.html.

  • Maintenance: Django-Cleanup to prevent storage bloat.

The Last Step

Run your server one last time: python manage.py runserver

Navigate to http://127.0.0.1:8000/profile/. You can now update your bio and see it saved directly to your custom user table!

To make this project truly "portable," we’ll wrap it in a Docker container. This ensures that if it works on your machine, it will work on any server (AWS, DigitalOcean, etc.) without you ever having to worry about "but I have Python 3.12 and the server has 3.10."

The Dockerfile

This is the recipe for your "container." Create a file named Dockerfile (no extension) in your root directory.

Dockerfile

# Use the official Python image
FROM python:3.11-slim

# Set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

# Set work directory
WORKDIR /code

# Install system dependencies
RUN apt-get update && apt-get install -y libpq-dev gcc

# Install Python dependencies
COPY requirements.txt /code/
RUN pip install --no-cache-dir -r requirements.txt

# Copy project
COPY . /code/

# Run the application
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]

Docker Compose (docker-compose.yml)

If you want to add a real database like PostgreSQL later, Docker Compose manages both the app and the database together.

YAML

version: '3.8'

services:
  web:
    build: .
    command: python manage.py runserver 0.0.0.0:8000
    volumes:
      - .:/code
    ports:
      - "8000:8000"
    env_file:
      - .env

How to Launch

Once you have Docker Desktop installed, you only need one command to start your entire project:

docker-compose up --build

This will:

  1. Build your Python environment.

  2. Install all your requirements.txt.

  3. Start the Django server at http://localhost:8000.

Final Folder Structure Overview

Your project is now a professional-grade fortress. Here is the final bird's-eye view:

File/Folder Purpose
core/ Global settings, URLs, and WSGI/ASGI entry points.
apps/ Contains users and pages. Keeps logic modular.
.env Keeps your SECRET_KEY and DB credentials safe.
static/ & media/ Managed by WhiteNoise and Django-Cleanup.
Dockerfile Makes your project run anywhere instantly.
templates/ Uses base.html for clean, DRY frontend code.

You're all set!

You have moved from a blank folder to a containerized, secure, scalable Django architecture with a custom user system and performance monitoring.

The skeleton is complete.

A professional README.md is the "front door" of your project. It helps other developers (and your future self) understand how to get the project running in minutes rather than hours.

In your root directory, create a file named README.md and paste the following:

Markdown

# 🚀 Professional Django Skeleton

A production-ready, scalable Django project structure featuring a custom user model, Docker integration, and optimized static file management.

## 🛠 Features
* **Custom User Model:** Email-based authentication (future-proofed).
* **Dockerized:** Includes `Dockerfile` and `docker-compose.yml` for instant setup.
* **Production Ready:** WhiteNoise for static files and Django-Cleanup for media management.
* **Modern UI:** Bootstrap 5 integrated via a master `base.html` template.
* **Dev Tools:** Django Debug Toolbar included for SQL and performance monitoring.

---

## ⚡ Quick Start (Local)

### 1. Prerequisites
- Python 3.11+
- Virtualenv

### 2. Installation
```bash
# Clone the repository
git clone <your-repo-url>
cd my_project

# Create and activate virtual environment
python -m venv venv
source venv/bin/activate  # Windows: venv\Scripts\activate

# Install dependencies
pip install -r requirements.txt

# Setup Environment Variables
# Create a .env file and add your SECRET_KEY and DEBUG=True

Database & Superuser

Bash

python manage.py migrate
python manage.py createsuperuser
python manage.py runserver

🐳 Quick Start (Docker)

If you have Docker installed, simply run:

Bash

docker-compose up --build

The app will be available at http://localhost:8000.

📂 Project Structure

  • core/ - Project configuration and settings.

  • apps/ - Custom Django applications (Users, Pages).

  • static/ - Global CSS, JavaScript, and images.

  • templates/ - HTML templates with inheritance.

  • .env - (Excluded) Sensitive configuration variables.

🧪 Development Notes

  • Debug Toolbar: Only visible when DEBUG=True and your IP is in INTERNAL_IPS.

  • Static Files: Run python manage.py collectstatic before deploying to production.