Set up a Django Project Locally
This guide explains how to set up a Django project and local environment on macOS to prepare for future cloud deployment. It uses PostgreSQL for both models and caching to mirror the production setup.
NOTE
You can follow the steps in this guide as written, but replace the following placeholders with your own names:
my-project: Your package namemy_project: Your Django project nameusers: Your Django app name for custom user modelmy_db_user: Your Postgres database user namemy_db_password: Your Postgres database passwordmy_database: Your Postgres database namemy_logger: Your logger nameprod-public-ip: Your production server's public IP addressexample.com: Your domain name (includingExample.Com)pirate_django_cache: Your cache table name in PostgreSQL databasepirate_token: Your Http-Only cookie name in browser
You should also uncomment any super-admin related lines and configure them as needed.
Create a New Project
First, install the uv package manager with this command:
curl -LsSf https://astral.sh/uv/install.sh | shWARNING
Ensure your local Python version matches your cloud server's version to avoid issues with mod_wsgi. As of April 2026, the recommended version is Python 3.13.
Initialize your project and navigate into the directory:
uv init my-project && cd my-projectInstall Dependencies
Run the following commands to add the necessary packages for the new project:
# Standard Django and utility packages
uv add django django-filter django-cors-headers django-redis django-storages
# Database, environment, and other packages
uv add python-dotenv boto3 requests psycopg2-binary pillow 'qrcode[pil]' pyotp
# Use uuid6 for uuid7 support (ignore if using Python 3.14+)
uv add uuid6
# Device and user-agent tracking
uv add pyyaml ua-parser user-agents django-user-agents
# Needed to serve S3 files via CloudFront (recommended)
uv add cryptography
# Django Rest Framework and JWT
uv add djangorestframework djangorestframework-simplejwt drf-spectacularInitiate a new Django project:
django-admin startproject my_project .Set Up a Local Database
Install Postgres database locally using the instructions from this link.
Once installed, add the Postgres binaries to your system path:
export PATH="/Library/PostgreSQL/18/bin/:$PATH"Access the Postgres terminal:
psql -U postgresRun these SQL commands to create your database and a dedicated user:
-- Replace my_db_password with a strong password of your choice
CREATE USER my_db_user ENCRYPTED PASSWORD 'my_db_password';
CREATE DATABASE my_database WITH OWNER=my_db_user;Configure the Environment
Create a new .env file in your project root to store sensitive credentials and environment-specific settings:
DJ_ENV=TEST
SECRET_KEY="your-secret-key-here"
POSTGRES_DATABASE=my_database
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_USER=my_db_user
POSTGRES_PASSWORD=my_db_password
EMAIL_HOST_USER=host@example.com
EMAIL_HOST_PASSWORD=your-password
SERVER_EMAIL="Server Admin <host@example.com>"
ADMIN_EMAIL="Admin <admin@example.com>"
MANAGER_EMAIL="Manager <manager@example.com>"
DRF_MFA_ENCRYPTION_KEY=your_mfa_encryption_key
# Any other key-value pairsCreate Static & Media Directories
Create these directories in your project root to handle file uploads and static assets:
mkdir static staticfiles mediaCreate a new app
Create a new app for custom User model:
uv run manage.py startapp usersCustom Models
Base Models
Add these abstract base models to a models.py file in your project root to provide UUIDs and automatic timestamps:
import uuid6
from django.db.models import DateTimeField, Model, UUIDField
class UUIDModel(Model):
"""Base model to add a UUID primary key."""
id = UUIDField(
primary_key=True,
default=uuid6.uuid7,
editable=False,
help_text="System-generated unique identifier (UUID).",
)
class Meta:
abstract = True
class TimeTrackedModel(Model):
"""
Base Model to track created_dt and updated_dt fields automatically.
"""
created_at = DateTimeField(
auto_now_add=True, help_text="Date and time when this record was created."
)
updated_at = DateTimeField(
auto_now=True, help_text="Date and time when this record was last updated."
)
class Meta:
abstract = TrueCustom User Model
Update users/models.py with the following code to support email-based authentication and profile tracking:
from django.contrib.auth.models import AbstractUser, UserManager as DjangoUserManager
from django.core.validators import RegexValidator
from django.db.models import (
BooleanField,
CharField,
EmailField,
ImageField,
)
from my_project.models import TimeTrackedModel, UUIDModel
class UserManager(DjangoUserManager):
"""
Custom manager that treats email as a unique field.
This subclasses the built-in UserManager for full compatibility with AbstractUser.
"""
def create_user(
self,
username: str,
email: str,
password: str | None = None,
**extra_fields,
):
if not email:
raise ValueError("The email field must be set")
# Lowercase the email and set is_active to False to require email verification
email = self.normalize_email(email).lower()
extra_fields.setdefault("is_active", False)
user = self.model(
username=username,
email=email,
**extra_fields,
)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(
self,
username: str,
email: str,
password: str | None = None,
**extra_fields,
):
extra_fields.setdefault("is_staff", True)
extra_fields.setdefault("is_superuser", True)
extra_fields.setdefault("is_active", True)
extra_fields.setdefault("is_email_verified", True)
return self.create_user(username, email, password, **extra_fields)
class User(UUIDModel, TimeTrackedModel, AbstractUser):
"""
Custom User model that extends AbstractUser to include unique email addresses
and additional profile attributes.
"""
first_name = CharField(
max_length=150,
verbose_name="first name",
help_text="(Required) User's first name.",
)
email = EmailField(
max_length=254,
unique=True,
verbose_name="email address",
help_text="A unique email address required for logging in.",
)
phone_number = CharField(
max_length=10,
validators=[
RegexValidator(
regex=r"^[1-9]\d{9}$",
message="Phone number must be exactly 10 digits and cannot start with zero.",
)
],
null=True,
blank=True,
verbose_name="phone number",
help_text="(Optional) 10-digit phone number.",
)
is_email_verified = BooleanField(
default=False,
help_text="Indicates whether the user has verified their email address.",
)
is_phone_verified = BooleanField(
default=False,
help_text="Indicates whether the user has verified their phone number.",
)
profile_picture = ImageField(
upload_to="users/avatars/",
null=True,
blank=True,
help_text="Optional profile picture for the user.",
)
objects = UserManager()
REQUIRED_FIELDS = ["email", "first_name"]
def __str__(self) -> str:
return f"{self.username} ({self.email})"Update users/admin.py
Update users/admin.py to display your custom fields and profile pictures:
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.utils.html import format_html
from users.models import User
class CustomUserAdmin(UserAdmin):
ordering = ("email",)
# Updated list_display to include a thumbnail and the username
list_display = (
"get_profile_picture",
"username",
"email",
"first_name",
"is_staff",
"is_active",
)
search_fields = ("username", "email", "first_name", "last_name")
# Added get_profile_picture to readonly_fields for the edit page
readonly_fields = ["id", "get_profile_picture", "created_at", "updated_at"]
# Included username in the fieldsets to support your dual-login strategy
fieldsets = (
(None, {"fields": ("username", "email", "password")}),
(
"Personal info",
{
"fields": (
"id",
"first_name",
"last_name",
"phone_number",
"profile_picture",
"get_profile_picture",
)
},
),
(
"Permissions",
{
"fields": (
"is_active",
"is_staff",
"is_superuser",
"groups",
"user_permissions",
)
},
),
("Verification", {"fields": ("is_email_verified", "is_phone_verified")}),
(
"Important dates",
{"fields": ("last_login", "date_joined", "created_at", "updated_at")},
),
)
# Added username to the creation form fields
add_fieldsets = (
(
None,
{
"classes": ("wide",),
"fields": (
"username",
"email",
"first_name",
"password1",
"password2",
"phone_number",
"profile_picture",
),
},
),
)
@admin.display(description="Profile Picture")
def get_profile_picture(self, obj):
if obj.profile_picture:
return format_html(
'<img src="{}" style="width: 40px; height: 40px; border-radius: 50%; object-fit: cover;" />',
obj.profile_picture.url,
)
return "No Image"
admin.site.register(User, CustomUserAdmin)New Authentication Backend
Create my_project/backends.py to allow users to log in with either their username (case-sensitive) or email (case-insensitive):
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth import get_user_model
from django.db.models import Q
User = get_user_model()
class EmailOrUsernameBackend(ModelBackend):
"""
Custom authentication backend that allows authenticating via
either username or email address.
"""
def authenticate(self, request, username=None, password=None, **kwargs):
try:
# Look for a user where the input matches either username or email
# iexact makes the check case-insensitive
user = User.objects.get(Q(username=username) | Q(email__iexact=username))
except User.DoesNotExist:
return None
# Check the password and ensure the user is allowed to authenticate
if user.check_password(password) and self.user_can_authenticate(user):
return user
return NoneUpdate settings.py
Update your settings.py to integrate the environment variables, logging, and security settings.
# settings.py (only changes to be made)
import os
from datetime import timedelta
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv(os.path.join(BASE_DIR, ".env"))
# Identify test environment
is_test = os.getenv("DJ_ENV") == "TEST"
# Super Admin ID
# ADMIN_ID = os.getenv("ADMIN_ID")
# Logging for debugging purposes
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"verbose": {
"format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}",
"style": "{",
},
"simple": {
"format": "{levelname} {asctime} {message}",
"style": "{",
},
},
"handlers": {
"console": {
"level": "DEBUG",
"class": "logging.StreamHandler",
"formatter": "simple",
},
"file": {
"level": "DEBUG",
"class": "logging.handlers.RotatingFileHandler",
"filename": os.path.join(BASE_DIR, "debug.log") if is_test else "/logs/debug.log",
"formatter": "verbose",
"maxBytes": 1024 * 1024 * 50,
"backupCount": 5,
},
"mail_admins": {
"level": "ERROR",
"class": "django.utils.log.AdminEmailHandler",
"formatter": "simple",
},
},
"loggers": {
"my_logger": {
"handlers": ["console", "file"] if is_test else ["file", "mail_admins"],
"propagate": False,
"level": "DEBUG",
},
},
}
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.getenv("SECRET_KEY")
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = is_test
ALLOWED_HOSTS = [
"prod-public-ip",
"example.com",
"www.example.com",
"api.example.com",
"www.api.example.com",
]
if is_test:
ALLOWED_HOSTS.extend(["localhost", "127.0.0.1"])
INSTALLED_APPS = [
# ...
# 3rd party
"corsheaders",
"rest_framework",
"rest_framework_simplejwt",
"rest_framework_simplejwt.token_blacklist",
"drf_spectacular",
"storages",
# project apps
"users",
# ...
]
MIDDLEWARE = [
...,
"corsheaders.middleware.CorsMiddleware", # Add CorsMiddleware before CommonMiddleware
"django.middleware.common.CommonMiddleware",
...,
]
ROOT_URLCONF = "my_project.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": os.getenv("POSTGRES_DATABASE"),
"HOST": os.getenv("POSTGRES_HOST"),
"PORT": os.getenv("POSTGRES_PORT"),
"USER": os.getenv("POSTGRES_USER"),
"PASSWORD": os.getenv("POSTGRES_PASSWORD"),
"CONN_MAX_AGE": 600,
"CONN_HEALTH_CHECKS": True,
}
}
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.db.DatabaseCache",
"LOCATION": "pirate_django_cache",
}
}
PASSWORD_RESET_TIMEOUT = 60 * 60 * 24
LANGUAGE_CODE = "en-us"
TIME_ZONE = "Asia/Kolkata"
USE_I18N = True
USE_TZ = True
STATIC_URL = "static/"
STATICFILES_DIRS = [
BASE_DIR / "static" # project-level assets
]
STATIC_ROOT = BASE_DIR / "staticfiles" # the output directory for collectstatic
# AWS S3 and CloudFront config for django-storages go here (See this link: production/django/aws-s3-for-media.html)
if DEBUG:
# LOCAL SETUP: serve media from local filesystem
STORAGES = {
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
"staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
},
}
# Ensure you have these set so local files know where to save
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"
else:
# PROD SETUP: store media in AWS S3, serve via CloudFront signed URLs
STORAGES = {
"default": {
"BACKEND": "storages.backends.s3boto3.S3Boto3Storage",
},
"staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
},
}
if not is_test:
# HTTPS handling
SECURE_SSL_REDIRECT = True
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
USE_X_FORWARDED_HOST = True
# HSTS
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
# Others
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True
# Email Configuration (User Secure Server config)
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = "smtpout.secureserver.net"
EMAIL_PORT = 465
EMAIL_USE_SSL = True
EMAIL_USE_TLS = False
EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER")
EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD")
SERVER_EMAIL = os.getenv("SERVER_EMAIL")
DEFAULT_FROM_EMAIL = SERVER_EMAIL
ADMINS = [os.getenv("ADMIN_EMAIL"), os.getenv("MANAGER_EMAIL")]
MANAGERS = [os.getenv("ADMIN_EMAIL"), os.getenv("MANAGER_EMAIL")]
# Custom User Model
AUTH_USER_MODEL = "users.User"
AUTHENTICATION_BACKENDS = [
'my_project.backends.EmailOrUsernameBackend',
# 'django.contrib.auth.backends.ModelBackend', # Default
]
# CORS (to allow all subdomains of example.com)
if is_test:
CORS_ALLOWED_ORIGINS = ["http://localhost:3000", "http://127.0.0.1:3000"]
else:
CORS_ALLOWED_ORIGINS = ["https://example.com", "https://www.example.com"]
CORS_ALLOWED_ORIGIN_REGEXES = [
r"^https://www\.[a-zA-Z0-9-]+\.example\.com$",
r"^https://[a-zA-Z0-9-]+\.example\.com$",
]
CORS_ALLOW_CREDENTIALS = True # required for cookie-based refresh
# CSRF
CSRF_COOKIE_DOMAIN = None if is_test else ".example.com"
CSRF_COOKIE_HTTPONLY = False # JS must be able to read it to send it as a header
CSRF_COOKIE_SECURE = not is_test
CSRF_COOKIE_SAMESITE = "Strict"
if is_test:
CSRF_TRUSTED_ORIGINS = ["http://localhost:3000", "http://127.0.0.1:3000"]
else:
CSRF_TRUSTED_ORIGINS = ["https://example.com", "https://*.example.com"]
# Session Cookie (Admin Only)
SESSION_COOKIE_DOMAIN = None if is_test else "api.example.com"
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SECURE = not is_test
SESSION_COOKIE_SAMESITE = "Strict"
# Other Security Settings
# Force HTTPS and tell browsers to remember it
if not is_test:
SECURE_SSL_REDIRECT = True # redirect all HTTP → HTTPS
SECURE_HSTS_SECONDS = 31536000 # 1 year
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
# Prevent clickjacking
X_FRAME_OPTIONS = "DENY"
# Prevent MIME sniffing
SECURE_CONTENT_TYPE_NOSNIFF = True
# Control referrer header leakage
SECURE_REFERRER_POLICY = "strict-origin-when-cross-origin"
# Django Rest Framework
REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",),
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework_simplejwt.authentication.JWTAuthentication"
],
"DEFAULT_RENDERER_CLASSES": ["rest_framework.renderers.JSONRenderer"],
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
"DEFAULT_FILTER_BACKENDS": ("django_filters.rest_framework.DjangoFilterBackend",),
"DEFAULT_THROTTLE_CLASSES": (
"rest_framework.throttling.AnonRateThrottle",
"rest_framework.throttling.UserRateThrottle",
),
"DEFAULT_THROTTLE_RATES": {"anon": "500/hour", "user": "100/minute"},
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 100,
}
if is_test:
REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"].append(
# To utilize auth done via admin UI
"rest_framework.authentication.SessionAuthentication"
)
REST_FRAMEWORK["DEFAULT_RENDERER_CLASSES"].append(
# To enable DRF UI in test environment
"rest_framework.renderers.BrowsableAPIRenderer"
)
# Django Rest Framework (Simple JWT)
SIMPLE_JWT = {
# Keep access tokens short-lived in production.
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=5),
"REFRESH_TOKEN_LIFETIME": timedelta(days=30),
# Issue a new refresh token on every /token/refresh/ call.
"ROTATE_REFRESH_TOKENS": True,
# Blacklist the old refresh token after rotation.
"BLACKLIST_AFTER_ROTATION": True,
"UPDATE_LAST_LOGIN": True,
"AUTH_HEADER_TYPES": ("Bearer",),
}
# Refresh Token Cookie
AUTH_COOKIE_NAME = "pirate_token"
AUTH_COOKIE_DOMAIN = None if is_test else ".example.com"
AUTH_COOKIE_SECURE = not is_test
AUTH_COOKIE_SAMESITE = "Strict"
TOKEN_REFRESH_URL = "/auth/token/refresh/"
# MFA Settings
DRF_MFA_SETTINGS = {
"TOKEN_VALID_WINDOW": 1,
"ENCRYPTION_KEY": os.getenv("DRF_MFA_ENCRYPTION_KEY"),
"ISSUER_NAME": "Example.Com",
}
# API Docs
SPECTACULAR_SETTINGS = {
"TITLE": "Example.Com API",
"DESCRIPTION": "API Documentation of Example.Com",
"VERSION": "1.0.0",
"SERVE_INCLUDE_SCHEMA": False,
"SERVE_PERMISSIONS": [
"rest_framework.permissions.IsAdminUser",
# "my_project.common.permissions.IsSuperAdmin",
],
"SWAGGER_UI_SETTINGS": {
"persistAuthorization": True,
},
}Update urls.py
Update project-level urls.py to serve static and media files from STATIC_ROOT and MEDIA_ROOT respectively and to allow Admin UI access locally:
# urls.py
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import include, path
from drf_spectacular.views import (
SpectacularAPIView,
SpectacularSwaggerView,
)
urlpatterns = [
# API Docs
path("v1/schema/", SpectacularAPIView.as_view(), name="schema"),
path(
"v1/docs/",
SpectacularSwaggerView.as_view(url_name="schema"),
name="swagger-ui",
),
# your URL patterns
]
if settings.DEBUG:
urlpatterns += [path("admin/", admin.site.urls)]
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)Update wsgi.py
Update wsgi.py to avoid server errors in production environment later:
import os
import sys
from pathlib import Path
from django.core.wsgi import get_wsgi_application
# To load all project-level env variables
SETTINGS_DIR = Path(__file__).resolve().parent
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "my_project.settings")
# Add following lines to the file, else you will run into "ModuleNotFoundError: No module named ... error
# (Web page will show Internal Server Error)
sys.path.append(str(SETTINGS_DIR))
application = get_wsgi_application()Final Steps and Verification
Run the following commands to apply your database schema and collect static files:
# Apply migrations
uv run manage.py makemigrations
uv run manage.py migrate
# Initialize system tables
uv run manage.py createcachetable
uv run manage.py collectstatic
# Create admin access
uv run manage.py createsuperuserVerify your setup by running the built-in check and starting the server:
uv run manage.py check
uv run manage.py runserver localhost:8000You can now access the Admin Dashboard at http://localhost:8000/admin to manage your users.
Connect to GitHub
Initialize git and link the project to a GitHub repo:
git init
git add .
git commit -m "Initial commit"
git branch -M main
git remote add origin <remote-URL> # Replace with your GitHub repository URL
git push -u origin main