To easily deploy your Django Project make sure your project follows guidelines listed in this article.
1. Requirements.txt¶
The deployment of a Django project starts with building an image with your code and its dependencies.
The requirements.txt
is located in the root of the repository and contains all packages needed for your projects without links to other files ( -r
lines).
Default python build image separately adds requirements.txt
and installs packages to utilize Docker build cache during the subsequent builds. E.g. if you change your project files, but requirements.txt
haven't changed, your deployment will go very fast.
2. Environment variables¶
The project should respect some environment variables. DATABASE_URL
for database connection string, REDIS_URL
for redis connection string. Good idea to make use of other variables: ALLOWED_HOSTS
, SECRET_KEY
to avoid hardcoding these in your code.
3. Static files¶
To serve static make sure to install the whitenoise
library and follow their installation guide.
4. Media files¶
To have media uploads you can either use S3 compatible service or use create a volume in your app's settings.
5. Secure Proxy SSL Header¶
This line has to be present in your settings.py
file to allow Django understand that it is running behind a secure proxy.
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
If this line is not present you might get infinite redirect loop or a misleading CSRF when a visitor tries to submit a form/log in to the site.
6. Management commands and initial setup¶
It is recommended to create a release.sh
script that will contain all commands needed to spin up and setup your app + all management commands to avoid the need to run shell commands for tasks like creating superuser. Then such release.sh script should be called from release.sh
process.
Example of release.sh
:
#!/bin/bash
set -e
python manage.py migrate
python manage.py makesuperuser
Example of management command makesuperuser.py
:
from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand
from django.utils.crypto import get_random_string
User = get_user_model()
class Command(BaseCommand):
def handle(self, *args, **kwargs):
email = 'admin@example.com'
new_password = get_random_string(10)
try:
u = None
if not User.objects.filter(is_superuser=True).exists():
self.stdout.write("No superusers found, creating one")
u = User.objects.create_superuser(username='admin', email=email, password=new_password)
self.stdout.write("=======================")
self.stdout.write("A superuser has been created")
self.stdout.write(f"Email: {email}")
self.stdout.write(f"Password: {new_password}")
self.stdout.write("=======================")
else:
self.stdout.write("A superuser exists in the database. Skipping.")
except Exception as e:
self.stderr.write(f"There was an error {e}")
7. Logging settings¶
In your settings file make sure you have the LOGGING
object that will produce output in stdout
. Otherwise, you won't see anything in the app logs.
You can grab this example and paste it into your settings.py
:
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {"console": {"class": "logging.StreamHandler"}},
"loggers": {"": {"handlers": ["console"], "level": "DEBUG"}},
}
8. Settings file for Django deployment¶
You can use this file as inspiration for what has to be in settings for deployment.
This settings file assumes that directory with settings.py
, wsgi.py
, asgi.py
and the root urls.py
is called project
You must have django-environ
installed.
from pathlib import Path
import environ
import os
from django.urls import reverse_lazy
env = environ.Env(
# set casting, default value
DEBUG=(bool, False)
)
BASE_DIR = Path(__file__).resolve().parent.parent
# Take environment variables from .env file
environ.Env.read_env(os.path.join(BASE_DIR, ".env"))
SECRET_KEY = env("SECRET_KEY", default="change_me")
DEBUG = env("DEBUG", default=False)
ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", default=["*"])
# Application definition
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
]
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",
]
ROOT_URLCONF = "project.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [
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",
],
},
},
]
WSGI_APPLICATION = "project.wsgi.application"
# Database
# https://docs.djangoproject.com/en/4.0/ref/settings/#databases
DATABASES = {
# read os.environ['DATABASE_URL'] and raises
# ImproperlyConfigured exception if not found
#
# The db() method is an alias for db_url().
"default": env.db(default="sqlite:///db.sqlite3"),
}
if DATABASES["default"]["ENGINE"] == "django.db.backends.postgresql":
DATABASES["default"]["ATOMIC_REQUESTS"] = True
DATABASES["default"]["CONN_MAX_AGE"] = env.int("CONN_MAX_AGE", default=60)
CI_COLLATION = "und-x-icu"
elif DATABASES["default"]["ENGINE"] == "django.db.backends.sqlite3":
CI_COLLATION = "NOCASE"
elif DATABASES["default"]["ENGINE"] == "django.db.backends.mysql":
CI_COLLATION = "utf8mb4_unicode_ci"
else:
raise NotImplementedError("Unknown database engine")
CACHES = {
# Read os.environ['CACHE_URL'] and raises
# ImproperlyConfigured exception if not found.
#
# The cache() method is an alias for cache_url().
"default": env.cache(default="dummycache://"),
}
# Password validation
# https://docs.djangoproject.com/en/4.0/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",
},
]
AUTHENTICATION_BACKENDS = [
# Needed to login by username in Django admin, regardless of `allauth`
"django.contrib.auth.backends.ModelBackend",
]
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {"console": {"class": "logging.StreamHandler"}},
"loggers": {"": {"handlers": ["console"], "level": "DEBUG"}},
}
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
# Internationalization
# https://docs.djangoproject.com/en/4.0/topics/i18n/
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
STATIC_URL = env.str("STATIC_URL", default="/static/")
STATIC_ROOT = env.str("STATIC_ROOT", default=BASE_DIR / "staticfiles")
MEDIA_ROOT = env("MEDIA_ROOT", default=BASE_DIR / "media")
MEDIA_URL = env("MEDIA_PATH", default="/media/")
9. Troubleshooting Django Deployment¶
10.1 502 Bad Gateway¶
If you have this error then Nginx was unable to connect to the backend. This can happen because your app is not listening on the designated port.
These can be the reasons and how to solve it:
- App doesn't respect the $PORT
variable. Usually not a problem for gunicorn
, but if you are using uvicorn
you need to pass the port. To do that, make a shell script run.sh
that will execute uvicorn
and have the port in parameters uvicorn project.asgi:application --host 0.0.0.0 --port $PORT
. In the Procfile specify web process like this web: sh run.sh
. Alternatively, in application settings you can specify the container port on which your app will be running and put that number in the --port
argument.
- App doesn't start at all, then please go to App logs and see why app is not starting or crashing
10.2 400 Bad Request¶
Django's ALLOWED_HOSTS
must be set according to domains you are using. If your app is available on multiple domains, you need to add them all.
To avoid hardcoding domains you can use django-environ
package. In settings.py
import environ
# ...
ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", default=["*"])
If you are looking for tutorial for a specific cloud provider checkout out one of these: