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.
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
Before writing code, you need a clean sandbox.
Create a Virtual Environment: Keep your global Python installation clean. python -m venv venv
Activate it:
Windows: venv\Scripts\activate
Mac/Linux: source venv/bin/activate
Install Django: pip install django python-dotenv
Initialize Project: django-admin startproject core . (The dot at the end prevents an extra nested folder).
.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'
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.
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).
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
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'
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)
Now that the code is ready, you need to tell the database to create the tables.
Make migrations: python manage.py makemigrations users
Apply migrations: python manage.py migrate
Create your first user: python manage.py createsuperuser
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.
For general views like a Home page, About page, or Contact page, it’s best to create a dedicated app.
Run: python manage.py startapp pages
Add to Settings: Open core/settings.py and add 'pages', to your INSTALLED_APPS list.
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'
We need to map the web address to that view. We do this in two steps to keep things modular.
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'),
]
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
]
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>
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!
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.
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>
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 %}
{% extends 'base.html' %}: Tells Django to grab the code from the base file first.
{% block content %}: Acts as a placeholder. Anything you put between these tags in home.html gets teleported into the same tags in base.html.
To make your site truly custom, you'll eventually want your own CSS file.
Create a folder: static/css/
Create a file: static/css/style.css
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.
requirements.txt FileThis 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
.gitignore FileThis 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
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. |
To hand this off to a teammate or move to a new machine:
git clone <repository_url>
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
python manage.py migrate
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.
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.
pip install whitenoise
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",
},
}
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.
pip install django-cleanup
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.
Development: You write CSS in static/ and upload images to media/.
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.
Runtime: When a user visits your site, WhiteNoise serves the compressed CSS instantly.
Maintenance: When you delete an old blog post, Django-Cleanup triggers a signal that deletes the specific .jpg from your media/ folder.
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.
First, add it to your environment: pip install django-debug-toolbar
core/settings.py)Since the toolbar shows sensitive data, it should only run in development.
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',
...
]
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",
]
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
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.
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.
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'}),
}
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
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 %}
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
]
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.
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."
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.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
Once you have Docker Desktop installed, you only need one command to start your entire project:
docker-compose up --build
This will:
Build your Python environment.
Install all your requirements.txt.
Start the Django server at http://localhost:8000.
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 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
Bash
python manage.py migrate
python manage.py createsuperuser
python manage.py runserver
If you have Docker installed, simply run:
Bash
docker-compose up --build
The app will be available at http://localhost:8000.
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.
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.