Skip to main content

How to Organize Django Settings for Local Development and Production

·914 words·5 mins
Author
Alessandro Ferrini

We all know how important it is to organize our code, but often we end up having that one project with 500+ lines in the settings file. In this article, I will explain how I manage settings in Django. My goal is to find a simple way to organize settings without relying on additional packages.

Why Efficient Django Settings Management is Important
#

A well-organized settings structure is crucial because it simplifies the management of configurations across different environments. My current stack is pretty solid, so I often copy-paste settings from older projects. However, I usually need to change them significantly during development, such as adding or removing packages or changing domain-specific configurations. Therefore, having a well-organized settings structure can be beneficial for maintaining clarity and ease of use.

The default settings.py you get from django-admin startproject is great, but it becomes limited as the project grows and can spiral out of control. Even though it might be fine for smaller projects, I will explain how to organize your settings effectively.

Creating a modular structure
#

To start, create a folder named settings within your Django project. This folder will house all your settings files, keeping them organized and modular.

my_django_project/
├── manage.py
├── myapp/
├── settings/
│   ├── __init__.py
│   ├── base.py
│   ├── local.py
│   └── production.py
└── ...

base.py will contain all the base settings that are common across all environments, such as basic installed apps, middlewares, and templates. The local.py and production.py files will contain settings specific to local development and production environments, respectively.

Settings example
#

Here’s an example of base.py from one of my latest projects.

The function value_from_env is taken from this article: Django ELB Health Checks

import mimetypes
from pathlib import Path

from dotenv import load_dotenv

from config.env import value_from_env

load_dotenv()
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve(strict=True).parent.parent.parent
APPS_DIR = BASE_DIR / "interfaces"

mimetypes.add_type("application/javascript", ".js", True)

# General
DEBUG = value_from_env("DEBUG", False)
TIME_ZONE = "UTC"
LANGUAGE_CODE = "en-us"
SITE_ID = 1
USE_I18N = True
USE_TZ = True
LOCALE_PATHS = [str(APPS_DIR / "locale")]

# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": BASE_DIR / "db.sqlite3",
    }
}
# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

# Urls
ROOT_URLCONF = "interfaces.urls"
WSGI_APPLICATION = "interfaces.wsgi.application"

# Application definition
DJANGO_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "django.contrib.sites",
]
THIRD_PARTY_APPS = [
    "allauth",
    "allauth.account",
    "crispy_forms",
    "crispy_tailwind",
    "django_tables2",
]
LOCAL_APPS = ["data", "interfaces", "interfaces.actions"]

INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
# Authentication
AUTHENTICATION_BACKENDS = [
    "django.contrib.auth.backends.ModelBackend",
    "allauth.account.auth_backends.AuthenticationBackend",
]

AUTH_USER_MODEL = "data.User"
LOGIN_REDIRECT_URL = "admin:index"
LOGIN_URL = "account_login"
ACCOUNT_LOGOUT_REDIRECT_URL = LOGIN_URL

ACCOUNT_FORMS = {
      "signup": "interfaces.account.SignUp",
}

# Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
    },
    {
        "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
    },
    {
        "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
    },
    {
        "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
    },
]

# Middleware
MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "whitenoise.middleware.WhiteNoiseMiddleware",
    "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",
    "allauth.account.middleware.AccountMiddleware",
]

# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/
STATIC_ROOT = str(APPS_DIR / "static")
STATIC_URL = "/static/"

# Media
# https://docs.djangoproject.com/en/dev/ref/settings/#media-root
MEDIA_ROOT = str(APPS_DIR / "media")
# https://docs.djangoproject.com/en/dev/ref/settings/#media-url
MEDIA_URL = "/media/"

# Templates
TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [str(APPS_DIR / "templates")],
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.debug",
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.template.context_processors.i18n",
                "django.template.context_processors.media",
                "django.template.context_processors.static",
                "django.template.context_processors.tz",
                "django.contrib.messages.context_processors.messages",
            ],
        },
    },
]

# Crispy Forms
CRISPY_TEMPLATE_PACK = "tailwind"
CRISPY_ALLOWED_TEMPLATE_PACKS = "tailwind"

# Logging
LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
        },
    },
    "root": {
        "handlers": ["console"],
        "level": "WARNING",
    },
    "loggers": {
        "django": {
            "handlers": ["console"],
            "level": value_from_env("DJANGO_LOG_LEVEL", "INFO"),
            "propagate": False,
        },
    },
}

# Allauth
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_USERNAME_REQUIRED = False
ACCOUNT_USER_MODEL_USERNAME_FIELD = None
ACCOUNT_SIGNUP_PASSWORD_ENTER_TWICE = True
ACCOUNT_SESSION_REMEMBER = True
ACCOUNT_AUTHENTICATION_METHOD = "email"
ACCOUNT_UNIQUE_EMAIL = True
ACCOUNT_EMAIL_VERIFICATION = False

You might notice a few things when reading this file:

  • DEBUG is set to False by default.
  • SECRET_KEY is missing.

This is intentional. Our new structure is designed to be modular, with minimal code duplication while allowing us to catch misconfigurations and errors as early as possible. We can define or override the variables we’re interested in within the other settings files. For example, here’s a snippet from my local.py:

DEBUG = True
SECRET_KEY = value_from_env("SECRET_KEY", default="!!!SET DJANGO_SECRET_KEY!!!")

As you can see, in this case, DEBUG is always set to True, and the SECRET_KEY is set by default to !!!SET DJANGO_SECRET_KEY!!!. If you run manage.py check --deploy, this SECRET_KEY will trigger the error security.W009. For more details, refer to the Django Deployment Checklist and the Django System Check Framework.

In your production.py you will have something like this

SECRET_KEY = value_from_env("SECRET_KEY")

The DEBUG is set to False, unless we change the env variable. And also this time the SECRET_KEY is required.

Switching Between Settings Using Environment Variables
#

To switch between settings, you can use the DJANGO_SETTINGS_MODULE environment variable. Set this variable to point to the appropriate settings file based on the environment.

# For local development
export DJANGO_SETTINGS_MODULE='myproject.settings.local'

# For production
export DJANGO_SETTINGS_MODULE='myproject.settings.production'

Final tips
#

  • I use dotenv to load variables from the environment. Alternatively, you can use django-environ. The key takeaway is to avoid hardcoding sensitive information. Always store configuration in the environment and keep it away from version control.
  • Keep your settings as modular as possible. Organize settings into separate files to avoid clutter. This approach makes it easier to document and share settings between similar projects. For example, use sentry.py for your Sentry configuration.
  • Document your settings clearly. This will help you and others understand the configuration and make necessary changes efficiently.