Django like all modern application development frameworks requires that you eventually manage tasks to support the core operation of a project. This can range from efficiently setting up a Django application to run in the real world, deploying a Django application to a cloud provider, to managing an application's static resources (e.g. CSS, JavaScript, image files).

In addition, other routine application management tasks can include: establishing a logging strategy to enforce problem detection; setting up email delivery for application users and/or administrators; as well as debugging tasks to inspect the outcome of complex operations. In this chapter, you'll learn about these and other common topics associated with Django application management.

Django settings.py for the real world

The settings.py is the central configuration for all Django projects. In previous chapters you already worked with a series of variables in this file to configure things like Django applications, databases, templates and middleware, among other things.

Although the settings.py file uses reasonable default values for practically all variables, when a Django application transitions into the real world, you need to take into account a series of adjustments, to efficiently run the Django application, offer end users a streamlined experience and keep potentially rogue attackers in check.

Switch DEBUG to False

One of the first things that's necessary to launch a Django application into the real world is to change the DEBUG variable to False in a project's settings.py file. I've briefly mentioned in previous chapters how Django's behavior changes when switching DEBUG=False to DEBUG=True in settings.py. All these behavioral changes associated with the DEBUG variable are intended to enhance project security and performance. Table 5-1 illustrates the differences between having a project run with DEBUG=False and DEBUG=True.

Table 5-1. Django behavior differences between DEBUG=True and DEBUG=False in settings.py

Functionality DEBUG=True behavior DEBUG=False behavior
Error handling and notification Displays full stack of errors on request pages for quick analysis Displays default 'vanilla' or custom error pages without any stack details to limit security threats or embarrassments. Emails project administrators of errors.(See the 'Define administrators for ADMINS and MANAGERS' section in this section for more details on email notifications)
Static resources Set up by default on a project's /static/ URL for simplicity. Disables automatic set up to enhance performance and avoid security vulnerabilities. This requires dealing with static resources in a separate workflow, using one of various techniques:
  • Configuring the the same web server that serves the Django application to serve static resources.
  • Serving static resources on a separate web server, for performance reasons.
  • Serving static resources on a cloud service (e.g. AWS S3 and/or CDN [Content Delivery Network]).
  • Using a turn-key Python module like WhiteNoise to deal with static resources as part of the Django application itself.

The upcoming section Set up static web page resources -- Images, CSS, JavaScript -- has more details

Host/site qualifier Requests for all hosts/sites are accepted for processing It's necessary to qualify for which hosts/sites a project can handle requests. If a site/host is not qualified, all requests are denied. (See the 'Define ALLOWED_HOSTS' sub-section in this section for more details)

As you can see in table 5-1, the changes enforced by changing DEBUG=True to DEBUG=False in settings.py are intended for publicly accessible applications (i.e. production environments). You may not like the hassle of adapting to these changes, but they are enforced to maintain a heightened level of security and maintain high performance on all Django projects that run in the real world.

Define ALLOWED_HOSTS

By default, the ALLOWED_HOSTS variable in settings.py is empty. The purpose of ALLOWED_HOSTS is to validate a request's HTTP Host header. Validation is done to prevent rogue users from sending fake HTTP Host headers that can potentially poison caches and password reset emails with links to malicious hosts. Since this issue can only present itself under an uncontrolled user environment (i.e. public/production servers), this validation is only done when DEBUG=False.

If you switch to DEBUG=False and ALLOWED_HOSTS is left empty, Django refuses to serve requests and instead responds with HTTP 400 bad request pages, since it can't validate incoming HTTP Host headers. Listing 5-1 illustrates a sample definition of ALLOWED_HOSTS.

Listing 5-1 Django ALLOWED_HOSTS definition

ALLOWED_HOSTS = [
    '.coffeehouse.com',
    '.bestcoffeehouse.com',
]

As you can see in listing 5-1, the ALLOWED_HOSTS value is a list of strings. In this case it defines two host domains, that allow bestcoffeehouse.com to act as an alias of coffeehouse.com. The leading .(dot) for each domain indicates a sub-domain is also an allowed host domain (e.g. static.coffeehouse.com or shop.coffeehouse.com is valid for .coffeehouse.com).

If you want to accept a single and fully qualified domain (FQDN) you would define ALLOWED_HOSTS=['www.coffeehouse.com'], which would only accept requests with an HTTP Host www.coffeehouse.com. In a similar fashion, if you want to bypass this security feature and accept any HTTP host value in a request -- which I don't recommend, see below -- you can define ALLOWED_HOSTS=['*'], where '*' represents a wild-card to accept any HTTP host value.

Caution Although setting ALLOWED_HOSTS=['*'] offers a quick solution, doing so raises the possibility of the rare, but possible, security vulnerability: HTTP host header attack[1].

Be careful with the SECRET_KEY value

The SECRET_KEY value in settings.py is another security related variable like ALLOWED_HOSTS. However, unlike ALLOWED_HOSTS, SECRET_KEY is assigned a default value and a very long value at that (e.g. 'django-insecure-3*w@re!%88p%w%+-3^g_z=pna5zot51cfjt4t^!6=u7sp7qo1!').

The purpose of the SECRET_KEY value is to digitally sign certain data structures that are sensitive to tampering. Specifically, Django by default uses the SECRET_KEY on sensitive data structures like session identifiers, cookies and password reset tokens. But you can rely on the SECRET_KEY value to cryptographically protect any sensitive data structure in a Django project[2].

The one thing the default data structures signed with the SECRET_KEY have in common, is they're sent to users on the wider Internet and are then sent back to the application to trigger actions on behalf of users. It's in this scenario we enter into a trust issue. Can the data sent back to the application be trusted ? What if a malicious user attempts to simulate another user's cookie or session data to hijack his access ? This is what digitally signed data prevents.

Before Django sends any of these sensitive data structures to users on the Internet, it signs them with a project's SECRET_KEY. When the data structures come back to fulfill an action, Django re-checks these sensitive data structures against the SECRET_KEY again. If there was any tampering on the data structures, the signature check fails and Django halts the process.

The only remote possibility a rogue user has to successfully pull an attack of this kind is if the SECRET_KEY is compromised -- since an attacker can potentially create an altered data structure that matches a project's SECRET_KEY. Therefore you should be careful about exposing your project's SECRET_KEY. If you suspect for any reason a project's SECRET_KEY has been compromised you should replace it immediately -- only a few ephemeral data structures (i.e. sessions, cookies) become invalid with this change, until users re-login again and the new SECRET_KEY is used to re-generate these data structures.

Define administrators for ADMINS and MANAGERS

Once a Django project is made accessible to end users, you'll want to define a set of users to notify them when certain events happen in a Django project (e.g. security issues or other critical factors). To this end, Django defines two sets of administrative groups defined in settings.py: ADMINS and MANAGERS.

The purpose of having two administrative groups in settings.py is mostly due to historical reasons in Django, where by default, users assigned to ADMINS received certain kinds of notifications and users assigned to MANAGERS received other types of notifications. However, it's often a common practice to alias the value of one group to the other (e.g. MANAGERS = ADMINS). In addition to the default events tied to each administrative group -- which I'll describe shortly -- it's also good practice to leverage these administrative groups to trigger notifications for custom events (e.g. notify administrators of user sign ups or business logic edge cases) vs. hard-coding administrative users/emails elsewhere in a project's codebase.

By default, both the ADMINS and MANAGERS variables are empty. The values assigned to both variables need to be a list of tuples, where each tuple is composed of a first value representing the name of a person and the second part of the tuple the person's email. Listing 5-2 shows a sample definition of ADMINS and MANAGERS.

Listing 5-2. Django ADMINS and MANAGERS definition

ADMINS = [('Webmaster','webmaster@coffeehouse.com'),('Administrator','admin@coffeehouse.com')]
MANAGERS = ADMINS

As you can see is listing 5-2, the ADMINS variable is assigned two tuples with different administrators. Next, you can see the MANAGERS variable is aliased with the ADMINS value. You can of course define different values for MANAGERS using the same syntax as ADMINS, but in this case, I just gave both variables the same values for simplicity.

By default, events that trigger notifications to both groups are limited and happen under certain circumstances.

By default, ADMINS are sent email notifications of errors associated with the django.request or django.security packages, if and only if DEBUG=False in settings.py and Django's AdminEmailHandler logging handler is enabled -- which it's by default -- in the LOGGING variable also in settings.py. This is a pretty narrow criteria, as it's intended to notify only the most serious errors -- for requests and security -- and only for production environments, in which DEBUG=False and default logging is set up. For no other events or conditions are the ADMINS notified by email.

By default, MANAGERS are sent email notifications of broken links (i.e. HTTP 404 page requests), if and only if DEBUG=False and the Django middleware django.middleware.common.BrokenLinkEmailsMiddleware is enabled. Because HTTP 404 page requests aren't a serious problem, by default BrokenLinkEmailsMiddleware is disabled. This is an even narrower criteria than for ADMINS, because irrespective of a project being in development (DEBUG=True) or production (DEBUG=False) the BrokenLinkEmailsMiddleware class needs to be added to MIDDLEWARE variable in settings.py for MANAGERS to get notifications. For no other events or conditions are the MANAGERS notified by email.

Now that you know the purpose of ADMINS and MANAGERS, add users and emails as you see fit to your project. Remember you can always leverage the values in ADMINS and MANAGERS for other custom logic in a Django project (e.g. notify administrators of user sign ups or business logic edge cases).

Modify LOGGING to stop email notifications to ADMINS

By default, users in ADMINS start receiving error emails as soon as you switch to DEBUG=False, due to Django's default LOGGING configuration. This is unlike MANAGERS which will never receive email unless you add the BrokenLinkEmailsMiddleware to MIDDLEWARE_CLASSES.

To stop email notifications to ADMINS even when DEBUG=False you can modify Django's LOGGING settings and disable the AdminEmailHandler handler. Details on disabling Django logging handlers are described in the logging section in this chapter. You can also leave ADMINS undefined so no emails are sent out, but that leaves your project with no ADMINS definition that may be useful for other purposes.

Use dynamic absolute paths

There are some Django variables in settings.py that rely on directory locations, such is the case for STATIC_ROOT which defines a consolidation directory for a project's static files or the DIRS list of the TEMPLATES variable which defines the location of a project's templates, among other variables.

The problem with variables that rely on directory locations is that if you run the project on different servers or share it with other users, it can be difficult to keep track or reserve the same directories across a series of environments. To solve this issue you can define variables to dynamically determine the absolute paths of a project. Listing 5-3 illustrates a Django project directory structure, deployed to the /www/ system directory.

Listing 5-3. Django project structure deployed to /www/

+-/www/+
       |           
       +--STORE--+
                 |
                 +---manage.py
                 |
                 +---coffeestatic--+
                 |                 |
                 |                 +-(Consolidated static resources) 
                 |
                 +---coffeehouse--+
                                  |
                                  +-__init__.py
                                  +-settings.py
                                  +-urls.py
                                  +-wsgi.py
                                  |
                                  +---templates---+
                                                  +-app_base_template.html
                                                  +-app_header_template.html
                                                  +-app_footer_template.html

Typically a Django settings.py file would define the values for STATIC_ROOT and DIRS in TEMPLATES as illustrated in listing 5-4.

Listing 5-4. Django settings.py with absolute path values

# Other configuration variables omitted for brevity
STATIC_ROOT = '/www/STORE/coffeestatic/'

# Other configuration variables omitted for brevity
TEMPLATES = [
		{
		'BACKEND': 'django.template.backends.django.DjangoTemplates',
		'DIRS': ['/www/STORE/coffeehouse/templates/',],
		}
	    ]

The issue with the setup in listing 5-4 is it will require editing if you deploy the Django application to a server where the /www/ directory isn't available (e.g. due to restrictions or a Windows OS where directories start with a leading letter C:/).

An easier approach illustrated in listing 5-5 is to define variables to dynamically determine the absolute paths of a project.

Listing 5-5. Django settings.py with dynamically determined absolute path

from pathlib import Path

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

# Other configuration variables omitted for brevity
STATIC_ROOT = BASE_DIR / 'coffeestatic'

# Other configuration variables omitted for brevity
TEMPLATES = [
		{
		'BACKEND': 'django.template.backends.django.DjangoTemplates',
		'DIRS': [ PROJECT_DIR / 'templates' ],
		}
	    ]

The variables defined at the top of listing 5-5 rely on the Python pathlib module to dynamically determine the absolute system path relative to the settings.py file. The PROJECT_DIR=Path(__file__).resolve().parent statement gets translated into the /www/STORE/coffeehouse/ value, which is the absolute system directory of files like settings.py. And to access the parent of /www/STORE/coffeehouse/ you simply make an additional call to the parent method and define the BASE_DIR variable so it gets translated into the /www/STORE/ value.

The remaining statements in listing 5-5 use pathlib's string concatenation functionality to use the PROJECT_DIR and BASE_DIR values and set the absolute paths in the STATIC_ROOT and TEMPLATE_DIRS variables. In this manner you don't need to hard code the absolute paths for any Django configuration variable, the variables automatically adjust to any absolute directory irrespective of the application deployment directory.

Use multiple environments or configuration files for Django

In every Django project you'll eventually come to the realization that you have to split settings.py into multiple environments or files. This will be either because the values in settings.py need to change between development and production servers, there are multiple people working on the same project with different requirements (e.g. Windows and Linux) or you need to keep sensitive settings.py information (e.g. passwords) in a local file that's not shared with others.

In Django there is no best or standard way to split settings.py into multiple environments or files. In fact, there are many techniques and libraries to make a Django project run with a split settings.py file. Next, I'll present the three most popular options I've used in my projects. Depending on your needs you may feel more comfortable using one option over another or inclusively mixing two or all three of these techniques to achieve an end solution.

Option 1) Multiple environments in the same settings.py file with a control variable

The settings.py file is treated as an ordinary Python file, so there's no limitation to using Python libraries or conditionals to obtain certain behaviors. This means you can easily introduce a control variable based on a fixed value (e.g. server host name) to conditionally set up certain variable values.

For example, changing the DATABASES variable -- because passwords and the database name change between development and production -- changing the EMAIL_BACKEND variable -- since you don't need to send actual emails in development as you do in production -- or changing the CACHES variable -- since you don't need a cache to speed up performance in development as you need in production.

Listing 5-6 illustrates the setup of a control variable called DJANGO_HOST based on Python's socket module, the variable is then used to load different sets of Django variables based on a server's host name.

Listing 5-6 Django settings.py with control variable with host name to load different sets of variables.

from pathlib import Path

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

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.admindocs',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    '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',
]

ROOT_URLCONF = 'coffeehouse.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [ PROJECT_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.contrib.messages.context_processors.messages',
            ],
        },
    },
]

# Import socket to read host name
import socket

# If the host name starts with 'live', DJANGO_HOST = "production"
if socket.gethostname().startswith('live'):
    DJANGO_HOST = "production"
# Else if host name starts with 'test', set DJANGO_HOST = "test"
elif socket.gethostname().startswith('test'): 
    DJANGO_HOST = "testing"
else:
# If host doesn't match, assume it's a development server, set DJANGO_HOST = "development"
    DJANGO_HOST = "development"

# Define general behavior variables for DJANGO_HOST and all others
if DJANGO_HOST == "production":
    DEBUG = False
    STATIC_URL = 'https://static.coffeehouse.com/'
else:
    DEBUG = True
    STATIC_URL = 'static/'

# Define DATABASES variable for DJANGO_HOST and all others
if DJANGO_HOST == "production":
   # Use mysql for live host
   DATABASES = {
    'default': {
        'NAME': 'housecoffee',
        'ENGINE': 'django.db.backends.mysql',
        'USER': 'coffee',
        'PASSWORD': 'secretpass'
    }
  }
else: 
   # Use sqlite for non live host
   DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
	'NAME': BASE_DIR / 'coffee.sqlite3',
    }
  }

# Define EMAIL_BACKEND variable for DJANGO_HOST
if DJANGO_HOST == "production":
    # Output to SMTP server on DJANGO_HOST production
    EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
elif DJANGO_HOST == "testing":
    # Nullify output on DJANGO_HOST test
    EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend'
else: 
    # Output to console for all others
    EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

# Define CACHES variable for DJANGO_HOST production and all other hosts 
if DJANGO_HOST == "production":
   # Set cache
   CACHES = {
        'default': {
            'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
            'LOCATION': '127.0.0.1:11211',
            'TIMEOUT':'1800',
            }
        }
   CACHE_MIDDLEWARE_SECONDS = 1800
else: 
   # No cache for all other hosts
   pass

The first line in listing 5-6 imports the Python socket module to gain access to the host name. Next, a series of conditionals are declared using socket.gethostname() to determine the value of the control variable DJANGO_HOST. If the host name starts with the letters live the DJANGO_HOST variable is set to "production", if the host name starts with test then DJANGO_HOST is set to "testing" and if the host name starts with neither of the previous options then DJANGO_HOST is set to "development".

In this scenario, the string method startswith is used to determine how to set the control variable based on the host name. However, you can just as easily use any other Python library or even criteria (e.g. IP address) to set the control variable. In addition, since the control variable is based on a string, you can introduce as many configuration variations as needed. In this case we use three different variations to set settings.py variables -- "production","testing" and "development" -- but you could easily define five or a dozen variations if you require such an amount of different set ups.

Option 2) Multiple environment files using configparser

Another variation to split settings.py is to rely on Python's built-in configparser module. configparser allows Django to read configuration variables from files that use a data structure similar to the one used in Microsoft Windows INI files. Listing 5-7 illustrates a sample configparser file.

Listing 5-7. Python configparser sample file production.cfg.

[general]
DEBUG: false
STATIC_URL: https://static.coffeehouse.com/
[databases]
NAME: housecoffee
ENGINE: django.db.backends.mysql 
USER: coffee
PASSWORD: secretpass 
[security]
SECRET_KEY: django-insecure-3*w@re!%%88p%%w%%+-3^g_z=pna5zot51cfjt4t^!6=u7sp7qo1!

As you can see in Listing 5-7, the format for a configparser file is structured in various sections declared between brackets (e.g. [general], [databases]) and below each section are the different keys and values. The variables in listing 5-7 are used for a production environment placed in a file named production.cfg. I chose the .cfg extension for this file, but you can use the .config or .ini extensions if you like, the extension is irrelevant to Python, the only thing that matters is the data format in the file itself.

Similar to the contents in production.cfg, you can create other files with different variables for other environments (e.g.testing.cfg, development.cfg). Once you have the configparser file or files, then you can import them into a Django settings.py. Listing 5-8 shows a sample settings.py that uses values from a configparser file.

Listing 5-8. Django settings.py with configparser import.

from pathlib import Path

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

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.admindocs',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    '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',
]

ROOT_URLCONF = 'coffeehouse.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [ PROJECT_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.contrib.messages.context_processors.messages',
            ],
        },
    },
]

# Access configparser to load variable values
import configparser
config = configparser.SafeConfigParser(allow_no_value=True)

# Import socket to read host name
import socket

# If the host name starts with 'live', load configparser from "production.cfg"
if socket.gethostname().startswith('live'):
    config.read(PROJECT_DIR / 'production.cfg')
# Else if host name starts with 'test', load configparser from "testing.cfg"
elif socket.gethostname().startswith('test'):
    config.read(PROJECT_DIR / 'testing.cfg')
else:
# If host doesn't match, assume it's a development server, load configparser from "development.cfg"
    config.read(PROJECT_DIR / 'development.cfg')

DEBUG = config.get('general', 'DEBUG')
STATIC_URL = config.get('general', 'STATIC_URL')
DATABASES = {
    'default': {
        'NAME': config.get('databases', 'NAME'),
        'ENGINE': config.get('databases', 'ENGINE'),
        'USER': config.get('databases', 'USER'),
        'PASSWORD': config.get('databases', 'PASSWORD')
    }
  }

SECRET_KEY = config.get('security', 'SECRET_KEY')
Note Configuration in listing 5-8 assumes host name starts with the name live in order to load configparser production.cfg in listing 5-7. Adjust conditionals at the start of listing 5-8 to match host name and load appropriate configparser file.

As you can see in Listing 5-8, configparser is loaded into Django via django.utils.six.moves, which is a utility to allow cross-imports between Python 2 and Python 3. In Python 2 the configparser package is actually named ConfigParser, but this utility allows us to use the same import statement using either Python 2 and Python 3. After the import, we use the SafeConfigParser class with the argument allow_no_value=True to allow processing of empty values in configparser keys.

Then we rely on the same prior technique using Python's socket module to gain access to the host name and determine which configparser file to load. The configparser file is loaded using the read method of the SafeConfigParser instance. At this juncture all configparser variables are loaded and ready for access. The remainder of listing 5-8 shows a series of standard Django settings.py variables that are assigned their value using the get method of the SafeConfigParser instance, where the first argument is the configparser section and the second argument is the key variable.

So there you have another option on how to split the variables in settings.py into multiple environments. Like I mentioned at the start, there's no best or standard way of doing this. Some people like configparser better because it splits values into separate files and avoids the many conditionals of option 1, but other people can hate configparser because of the need to deal with the special syntax and separate files. Choose whatever feels best for your project.

Option 3) Multiple settings.py files with different names for each environment

Finally, another option to split Django variables into multiple environments is to create multiple settings.py files with different names. By default, Django looks for configuration variables in the settings.py file in a project's base directory.

However, it's possible to tell Django to load a configuration file with a different name. Django uses the operating system(OS) variable DJANGO_SETTINGS_MODULE for this purpose. By default, Django sets this OS variable to <project_name>.settings in the manage.py file located in the base directory of any Django project. And since the manage.py file is used to bootstrap Django applications, the DJANGO_SETTINGS_MODULE value in this file guarantees configuration variables are always loaded from the settings.py file inside the <project_name> sub-directory.

So let's suppose you create different settings.py files for a Django application -- placed in the same directory as settings.py -- named production.py, testing.py and development.py. You have two options to load these different files.

One option is to change the DJANGO_SETTINGS_MODULE definition in a project's manage.py file to the file with the desired configuration (e.g. os.environ.setdefault("DJANGO_SETTINGS_MODULE", "coffeehouse.production") to load the production.py configuration file). However, hard coding this value is inflexible because you would need to constantly change the value in manage.py based on the desired configuration. Here you could use a control variable in manage.py to dynamically determine the DJANGO_SETTINGS_MODULE value based on a host name -- similar to the process described in the previous option 1 for settings.py.

Another possibility to set DJANGO_SETTINGS_MODULE without altering manage.py is to define DJANGO_SETTINGS_MODULE at the OS level so it overrides the definition in manage.py. Listing 5-9 illustrates how to set the DJANGO_SETTINGS_MODULE variable on a Linux/Unix OS so that application variables in the testing_settings.py file are used instead of the settings.py file.

Listing 5-9. Override DJANGO_SETTINGS_MODULE to load application variables from a file called testing.py and not the default settings.py

$ export DJANGO_SETTINGS_MODULE=coffeehouse.testing_settings
$ python manage.py runserver
System check identified no issues (0 silenced).
Django version 4.0.1, using settings 'coffeehouse.testing_settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

In listing 5-9 we use the standard Linux/Unix syntax export variable_name=variable_value to set an environment variable. Once this is done, notice the Django application that uses the development server displays the start-up message "using settings 'coffeehouse.testing_settings'".

If you plan to override the DJANGO_SETTINGS_MODULE at the OS level to load different Django application variables, be aware that by default OS variables aren't permanent or inherited. This means you may need to define the DJANGO_SETTINGS_MODULE for every shell from which you start Django and also define it as a local variable for run-time environments (e.g. Nginx, Apache).