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:
__init__.py
base.py
dev.py
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 ofdjangobin-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 ofdjango.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 thesecrets
variable as a dictionary.If you try to access a configuration which doesn't exist in the
secrets
directory, you will get aKeyError
exception. Sadly, this is not very helpful. To make debugging easier we have definedget_secret_setting()
function. This method returns the value of the setting it is called with or anImproperlyConfigured
exception if the setting is not found in thesecrets
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 toos.path.dirname()
. This will make sure that theBASE_DIR
setting points to the correct base directory.In line 23, we have added
'django.middleware.common.BrokenLinkEmailsMiddleware'
to theMIDDLEWARE
list. This will email theMANAGERS
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:
Unlike
dev.py
, inprod.py
we are loading our sensitive configurations viaget_secret_setting()
function defined in thebase.py
file.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 fromexample.com
orwww.example.com
then you would need to setALLOWED_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.
In lines 15-19, we define configurations to connect to PostgreSQL database.
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.
Load Comments