# Settings for Multiple Environments in Django

Last updated on July 27, 2020

## Multiple Settings File #

So far, a single settings.py file has served us well. Now we are moving to the production environment, as a result, some of the settings in settings.py file needs to be changed. Most notably, we will change DEBUG to False and ALLOWED_HOSTS to the production server IP or domain.

To efficiently run our Django project in different environments, we will split our single settings.py into multiple files, each one representing settings for a particular environment.

Let's start by renaming settings.py to old.settings.py and creating a directory named settings in the Django configurations directory (djangobin/django_project/django_project). Inside the settings directory create following four files:

1. __init__.py
2. base.py
3. dev.py
4. prod.py.

The __init__.py file tells Python that the settings directory is a package. The base.py contains the settings common to development and production environment. The dev.py and prod.py contains the settings specific to the development and production respectively.

At this point, Django configurations directory should look like this:

  1 2 3 4 5 6 7 8 9 10 11 12 13 django_project/ ├── celery.py ├── __init__.py ├── old.settings.py ├── settings │   ├── __init__.py │   ├── base.py │   ├── dev.py │   └── prod.py ├── urls.py └── wsgi.py 1 directory, 9 files 

The complete code of base.py is as follows (changes are highlighted):

djangobin/django_project/django_project/settings/base.py

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 """ Django settings for django_project project. Generated by 'django-admin startproject' using Django 1.11. For more information on this file, see https://docs.djangoproject.com/en/1.11/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/1.11/ref/settings/ """ import os, json from django.core.exceptions import ImproperlyConfigured with open(os.path.abspath("djangobin-secrets.json")) as f: secrets = json.loads(f.read()) def get_secret_setting(setting, secrets=secrets): try: return secrets[setting] except KeyError: raise ImproperlyConfigured("Set the {} setting".format(setting)) # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ # Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'django.contrib.humanize', 'django.contrib.flatpages', 'django.contrib.sites', 'django.contrib.sitemaps', 'djangobin', ] 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', 'django.middleware.common.BrokenLinkEmailsMiddleware', ] ROOT_URLCONF = 'django_project.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [ os.path.join(BASE_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', 'djangobin.context_processors.recent_snippets', ], }, }, ] WSGI_APPLICATION = 'django_project.wsgi.application' # Password validation # https://docs.djangoproject.com/en/1.11/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', }, ] # Internationalization # https://docs.djangoproject.com/en/1.11/topics/i18n/ LANGUAGE_CODE = 'en-us' TIME_ZONE = 'UTC' USE_I18N = True USE_L10N = True USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.11/howto/static-files/ STATIC_URL = '/static/' STATIC_ROOT = os.path.join(BASE_DIR, 'staticfies') MEDIA_ROOT = os.path.join(BASE_DIR, 'media') MEDIA_URL = '/media/' LOGIN_REDIRECT_URL = 'djangobin:index' LOGIN_URL = 'djangobin:login' SITE_ID = 1 

Here are few things to notice:

• For security reasons, you must never hardcode sensitive configurations like SECRET_KEY, database credentials, or API keys in your code. Furthermore, these configurations are also subject to change across deploys. If you put these configurations in the code then you would have to constantly update the code everytime you move to a new environment.

In our case, we have stored all our sensitive configurations in a JSON file named djangobin-secrets.json, which resides in project root directory (djangobin/django_project). The contents of djangobin-secrets.json looks like this:

djangobin/django_project/djangobin-secrets.json

  1 2 3 4 5 6 7 8 9 10 11 12 13 { "SECRET_KEY": "rj3vhyKiDRNmth75sxJemvOuTy1Hy0ogeKgS9JP8Gp7dGDctfSMUuOt5QbSpsS9xAlvBMTXW3Z6VTODvvFcV3TmtrZUbGkHBcs8I", "DATABASE_NAME": "djangobin", "DATABASE_USER": "postgres", "DATABASE_PASSWORD": "pass", "DATABASE_HOST": "127.0.0.1", "DATABASE_PORT": "5432", "EMAIL_HOST_USER": "apikey", "EMAIL_HOST": "smtp.sendgrid.net", "EMAIL_HOST_PASSWORD": "IK.qQecgqph1Sa9TkOOljo8pA.5Xrj1oyJKuOGBbHnWFmdDe32G8XXojH45W1loxIsktqY3Nc", "EMAIL_PORT": "587", "EMAIL_USE_TLS": "True" } 

This file contains the SECRET_KEY, database credentials and email credentials. If you are using a version control system (which you should) add this file to .gitignore.

To generate the secret key we the get_random_string() function of django.utils.crypto module:

 1 2 3 4 5 6 >>> >>> from django.utils.crypto import get_random_string >>> >>> get_random_string(80) 'ZEjwrGbwF9fjFAfTXTvz7LxXnCGUPOJyfPszrk2WHtbrbH3mgBeag2NWUueGYYiA7fTw36F50T2R3F5L' >>> 

In production, we will use PostgreSQL as our database and SendGrid to send emails.

If you already know how to install and configure PostgreSQL, go ahead and populate the database credentials, otherwise, wait until the next lesson where we will look at how to install and configure PostgreSQL.

To obtain the email credentials sign up for a free account on SendGrid. As of this writing, SendGrid free plan allows you to send 100 email daily.

Back to base.py file.

In lines 16-17, we read the contents of djangobin-secrets.json file and store it in the secrets variable as a dictionary.

If you try to access a configuration which doesn't exist in the secrets directory, you will get a KeyError exception. Sadly, this is not very helpful. To make debugging easier we have defined get_secret_setting() function. This method returns the value of the setting it is called with or an ImproperlyConfigured exception if the setting is not found in the secrets directory.

• Our settings file is now one level deep inside the Django configurations directory. In other words, the BASE_DIR setting no longer points to project root directory (djangobin/django_project/) instead it points to the Django configurations directory (djangobin/django_project/django_project). This effectively breaks the path to templates, static files and media files. To account for this, in line 27, we have we added an additional call to os.path.dirname(). This will make sure that the BASE_DIR setting points to the correct base directory.

• In line 23, we have added 'django.middleware.common.BrokenLinkEmailsMiddleware' to the MIDDLEWARE list. This will email the MANAGERS whenever an HTTP 404 error occurs.

The code for dev.py is as follows:

djangobin/django_project/django_project/settings/dev.py

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 from .base import * # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = '_5=#=+cl&lp@&ayps6ia0viff)^v$_wvutyyxca!xu0w6d2z3$' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True ALLOWED_HOSTS = [] DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), } } EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' SERVER_EMAIL = 'infooveriq@gmail.com' DEFAULT_FROM_EMAIL = SERVER_EMAIL ADMINS = ( ('OverIQ', 'admin@overiq.com'), ) MANAGERS = ( ('OverIQ', 'manager@overiq.com'), ) 

Nothing fancy here, the dev.py just contains development specific settings. To import the common settings from base.py we use from .base import * statement (line 1). Note that we have deliberately hardcoded some of the sensitive configurations because it makes the development process easy. However, this is not the case with prod.py file.

djangobin/django_project/django_project/settings/prod.py

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 import os from .base import * # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = get_secret_setting('SECRET_KEY') # SECURITY WARNING: don't run with debug turned on in production! DEBUG = False ALLOWED_HOSTS = ["*"] DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', 'NAME': get_secret_setting('DATABASE_NAME'), 'USER': get_secret_setting('DATABASE_USER'), 'PASSWORD': get_secret_setting('DATABASE_PASSWORD'), 'HOST': get_secret_setting('DATABASE_HOST'), 'PORT': get_secret_setting('DATABASE_PORT'), } } STATIC_ROOT = 'static' EMAIL_HOST_USER = get_secret_setting('EMAIL_HOST_USER') EMAIL_HOST = get_secret_setting('EMAIL_HOST') EMAIL_HOST_PASSWORD = get_secret_setting('EMAIL_HOST_PASSWORD') EMAIL_PORT = get_secret_setting('EMAIL_PORT') EMAIL_USE_TLS = True EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' DEFAULT_FROM_EMAIL = 'support@overiq.com' SERVER_EMAIL = 'no-reply@overiq.com' ADMINS = ( ('OverIQ', 'infooveriq@gmail.com'), ) MANAGERS = ( ('OverIQ', 'manager@overiq.com'), ) 

The prod.py is almost similar to dev.py but it defines settings specific to the production environment.

Here are few things to notice:

1. Unlike dev.py, in prod.py we are loading our sensitive configurations via get_secret_setting() function defined in the base.py file.

2. In line 10, we set ALLOWED_HOSTS to [*]. ALLOWED_HOSTS setting is a security feature which is used to validate whether the request is coming from allowed domains or not. It specifies a list of a string representing host/domain names this Django project can serve. By default, it is set to an empty list. Once you move to production you required to set it otherwise you will get 500 Internal Server Error.

If you own example.com and want to allow requests from example.com or www.example.com then you would need to set ALLOWED_HOSTS to:

ALLOWED_HOSTS =  ['example.com', 'www.example.com']


If you want to allow requests from example.com and all its subdomains then use the period as a subdomain wildcard. For example:

ALLOWED_HOSTS =  ['.example.com']


To allow Django to accept requests from any domain set ALLOWED_HOSTS to "*".

ALLOWED_HOSTS =  ['*']


However, in a real deploy, you should limit this setting only to the host/domain you want to allow.

3. In lines 15-19, we define configurations to connect to PostgreSQL database.

4. In lines 25-28, we define settings required to send emails via SendGrid.

## Running Project #

We have refactored our code quite a lot. Now let's take a look at how we can interact with our Django project with this new setup.

In the terminal execute ./manage.py file and you will get the error as follows:

 1 2 3 $./manage.py ... Note that only Django core commands are listed as settings are not properly configured (error: Requested setting INSTALLED_APPS, but settings are not configured. You must either define the environment variable DJANGO_SETTINGS_MODULE or call settings.configure() before accessing settings.).  You will get the same error (just different wording) if you try to run the runserver or shell command.  1 2 3 $ ./manage.py runserver ... django.core.exceptions.ImproperlyConfigured: Requested setting DEBUG, but settings are not configured. You must either define the environment variable DJANGO_SETTINGS_MODULE or call settings.configure() before accessing settings. 

The problem is that the Django doesn't know where our settings file is located. We can specify the location of settings file from the command line using the --setting option.

 1 2 3 4 5 6 7 8 $./manage.py runserver --settings=django_project.settings.dev Performing system checks... System check identified no issues (0 silenced). June 04, 2018 - 06:37:16 Django version 1.11, using settings 'django_project.settings.dev' Starting development server at http://127.0.0.1:8000/ Quit the server with CONTROL-C.  Note that you will need to specify --settings option everytime you execute the manage.py script. For example: $ ./manage.py shell --settings=django_project.settings.dev

$./manage.py makemigrations --settings=django_project.settings.dev  Specifying the --settings option everytime you execute ./manage.py file can quickly become tedious. Alternatively, you can set the DJANGO_SETTINGS_MODULE environment variable to the desired settings file as follows: $ export DJANGO_SETTINGS_MODULE=django_project.settings.dev


You can now execute the ./manage.py as usual without specifying the path to the settings file.

$./manage.py runserver  The DJANGO_SETTINGS_MODULE variable will remain in existence until the shell session is active. Unfortunately, If you start a new shell you will have to set DJANGO_SETTINGS_MODULE again. A much better approach would be to modify the virtualenv's activate script and set DJANGO_SETTINGS_MODULE environment variable when activating the virtualenv and unset it when deactivating the virtualenv. Open activate script and modify it as follows: djangobin/env/bin/activate   1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 # This file must be used with "source bin/activate" *from bash* # you cannot run it directly deactivate () { unset -f pydoc >/dev/null 2>&1 # reset old environment variables # ! [ -z${VAR+_} ] returns true if VAR is declared at all if ! [ -z "${_OLD_VIRTUAL_PATH+_}" ] ; then PATH="$_OLD_VIRTUAL_PATH" export PATH unset _OLD_VIRTUAL_PATH fi if ! [ -z "${_OLD_VIRTUAL_PYTHONHOME+_}" ] ; then PYTHONHOME="$_OLD_VIRTUAL_PYTHONHOME" export PYTHONHOME unset _OLD_VIRTUAL_PYTHONHOME fi # This should detect bash and zsh, which have a hash command that must # be called to get it to forget past commands. Without forgetting # past commands the $PATH changes we made may not be respected if [ -n "${BASH-}" ] || [ -n "${ZSH_VERSION-}" ] ; then hash -r 2>/dev/null fi if ! [ -z "${_OLD_VIRTUAL_PS1+_}" ] ; then PS1="$_OLD_VIRTUAL_PS1" export PS1 unset _OLD_VIRTUAL_PS1 fi unset VIRTUAL_ENV if [ ! "${1-}" = "nondestructive" ] ; then # Self destruct! unset -f deactivate fi unset DJANGO_SETTINGS_MODULE } # unset irrelevant variables deactivate nondestructive VIRTUAL_ENV="/home/pp/django-1.11/djangobin/env" export VIRTUAL_ENV _OLD_VIRTUAL_PATH="$PATH" PATH="$VIRTUAL_ENV/bin:$PATH" export PATH # unset PYTHONHOME if set if ! [ -z "${PYTHONHOME+_}" ] ; then _OLD_VIRTUAL_PYTHONHOME="$PYTHONHOME" unset PYTHONHOME fi if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT-}" ] ; then _OLD_VIRTUAL_PS1="$PS1" if [ "x" != x ] ; then PS1="$PS1" else PS1="(basename \"$VIRTUAL_ENV\")$PS1" fi export PS1 fi # Make sure to unalias pydoc if it's already there alias pydoc 2>/dev/null >/dev/null && unalias pydoc pydoc () { python -m pydoc "$@" } # This should detect bash and zsh, which have a hash command that must # be called to get it to forget past commands. Without forgetting # past commands the$PATH changes we made may not be respected if [ -n "${BASH-}" ] || [ -n "${ZSH_VERSION-}" ] ; then hash -r 2>/dev/null fi export DJANGO_SETTINGS_MODULE=django_project.settings.dev 

Start a new shell, activate the virtual environment and check the existence of DJANGO_SETTINGS_MODULE environment variable using the echo command:

 1 2 $echo$DJANGO_SETTINGS_MODULE django_project.settings.dev 

As expected, DJANGO_SETTINGS_MODULE environment variable is available.

Now you can run ./manage.py file without setting any environment variable or specifying the --settings option

Start the Django development server to make sure everything is working as expected.

$./manage.py runserver  The output should look like this:  1 2 3 4 5 6 7 Performing system checks... System check identified no issues (0 silenced). May 17, 2018 - 14:51:37 Django version 1.11, using settings 'django_project.settings.dev' Starting development server at http://127.0.0.1:8000/ Quit the server with CONTROL-C.  The DJANGO_SETTINGS_MODULE variable will be automatically removed upon deactivating the virtual environment. ## Creating Requirements file # A requirements file is a simple text file containing the project dependencies. We use the requirements file to install the project dependencies. To create the requirements file execute the following command: $ pip freeze > requirements.txt


Our DjangoBin project is now complete. In the next lesson, we will learn how to deploy it to the DigitalOcean server.