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: