In this article I will show you how to start a new Django project from scratch and set it up to work in Docker for local development, make it ready for production and why it is beneficial to dockerize your Django project.

Source code

The source code for this tutorial is available on GitHub: https://github.com/appliku/django-docker-tutorial

What is Docker and Docker Compose?

Docker makes it easier to create, deploy, and run applications by using containers.

Containers are like lightweight virtual machines that can run anywhere, and they keep the application isolated from the underlying system. Docker makes it easy to package an application with all its dependencies into a container, and then deploy it to any environment without worrying about compatibility issues.

Docker Compose is a tool that helps manage multiple containers as part of a single application. It allows developers to define their application environment in a single file, specifying which services (containers) should be run, and how they should communicate with each other. Docker Compose then takes care of starting and stopping these containers as a single unit, making it easy to develop and test complex applications.

TLDR: Docker is a way to package and run applications in a portable and isolated manner, while Docker Compose is a tool for managing multiple containers as a single application. Together, they make it easier for developers to build, ship, and run applications in any environment.

Why dockerize your Django application?

Dockerizing your Django application means packaging your application and its dependencies into a container using Docker. This container can then be easily shared, run, and deployed across different platforms without worrying about compatibility issues.

Benefits of Dockerizing your Django application

Firstly, it makes your application more portable and scalable, meaning you can run your application on any platform, whether it's your local machine or a cloud server, without worrying about installation or compatibility issues. This also makes it easier to test your application on different environments.

Secondly, Dockerizing your Django application helps to isolate your application from the host machine and other applications. This ensures that your application runs consistently and avoids any conflicts with other applications or services that may be running on the same machine.

Thirdly, Dockerizing your Django application can help you streamline your development workflow by allowing you to quickly spin up and tear down development environments. This means you can develop, test, and debug your application more efficiently, without having to worry about setting up your development environment each time.

Tutorial requirements

In order to proceed with this tutorial make sure you have installed python 3 (3.10 or 3.11 should be fine) and Docker Desktop.

Create a new Python virtual environment

Open the terminal and run these commands:

mkdir myproject
cd myproject
python3 -m venv env
source env/bin/activate
pip install -U pip  # update pip to the latest version
pip install django
pip install django-environ

These commands are used to set up a virtual environment for a Django project. Here's what each command does:

mkdir myproject: This creates a new directory called myproject in the current working directory.

cd myproject: This changes the working directory to the myproject directory.

python3 -m venv env: This creates a new virtual environment called env for the Python interpreter python3. A virtual environment is an isolated Python environment that allows you to install packages and dependencies specific to a particular project.

source env/bin/activate: This activates the virtual environment env. When the virtual environment is activated, the shell prompt will change to include the name of the virtual environment.

pip install -U pip: This updates the package manager pip to the latest version.

pip install django: This installs the Django web framework, which is a high-level Python web framework that allows developers to quickly build web applications.

pip install django-environ: This installs the django-environ package, which is a library that helps manage environment variables for Django projects. It provides an easy way to configure Django applications using environment variables, which can be useful when deploying your application to different environments, such as development, staging, and production.

Create a Django Project

django-admin startproject project .  # note the trailing '.' character

Here is how is the project layout looks like.

.
./project
./project/asgi.py
./project/__init__.py
./project/settings.py
./project/urls.py
./project/wsgi.py
./manage.py

Please note: We won't be creating any additional apps within this tutorial. Our goal here is to make a django project work within the Docker environment.

Django Settings for Docker

We want our apps to be scalable, work under a lot of traffic and handle growth.

In order to do that we need to build our app so it allows scaling.

In this case it is important that our app follows rules of The 12-factor app: https://12factor.net

Let's list the key points here:

  1. Codebase – One codebase tracked in revision control, many deploys
  2. Dependencies – Explicitly declare and isolate dependencies
  3. Config – Store config in the environment
  4. Backing services – Treat backing services as attached resources
  5. Build, release, run – Strictly separate build and run stages
  6. Processes – Execute the app as one or more stateless processes
  7. Port binding – Export services via port binding
  8. Concurrency – Scale out via the process model
  9. Disposability – Maximize robustness with fast startup and graceful shutdown
  10. Dev/prod parity – Keep development, staging, and production as similar as possible
  11. Logs – Treat logs as event streams
  12. Admin processes – Run admin/management tasks as one-off processes

I strongly recommend to read all pages on that site.

Right now we will address the third point by making settings of our app configured through environment variables.

Open the project/settings.py and start editing it.

from pathlib import Path
import environ
import os

BASE_DIR = Path(__file__).resolve(strict=True).parent.parent
env = environ.Env()
DEBUG = env.bool("DJANGO_DEBUG", False)
# Allowed Hosts Definition
if DEBUG:
    # If Debug is True, allow all.
    ALLOWED_HOSTS = ['*']
else:
    ALLOWED_HOSTS = env.list('DJANGO_ALLOWED_HOSTS', default=['example.com'])
SECRET_KEY = env('DJANGO_SECRET_KEY')
# Databases
DATABASES = {
    "default": env.db("DATABASE_URL")
}
DATABASES["default"]["ATOMIC_REQUESTS"] = True
DATABASES["default"]["CONN_MAX_AGE"] = env.int("CONN_MAX_AGE", default=60)
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler'
        },
    },
    'loggers': {
        '': {  # 'catch all' loggers by referencing it with the empty string
            'handlers': ['console'],
            'level': 'DEBUG',
        },
    },
}

These are variables that we need to define in the settings as they are the most crucial for how Django project works.

For the sake of simplicity you can past it at the end of the file, after all the existing lines, and you can clean up lines duplicated variables (that came from default django) later.

I want us to focus on things we define here and why.

Using environment variables will allow us to modify the behaviour of Django project without touching the codebase.

These are the most important ones:

  • DEBUG – it should always be set to False outside of development environment. In local development environment you will add it to .env file(we'll discuss it later).
  • ALLOWED_HOSTS – if DEBUG is turned on, meaning it is local dev environment – Django will not check for which domain is used to open the app. If it is off, accessing the app through domain that is not listed here will result in 400 Bad Request.
  • DATABASE_URL must be set to something like postgres://somethin@passwrd:db:5432/db_name and it will be converted into a python dictionary as required per Django databases settings

Other stuff: SECURE_PROXY_HEADER will help Django understand if it is behind secure proxy or not. We don't need it right now for the local dev environment, but better add this right away.

The LOGGING variable defines configuration for logging which makes sure all logs are outputed to stdout, which is the number 11 in the 12 factor app list.

Dockerfile for Django

In the root of the project create a file Dockerfile (without any extension).

FROM python:3.11.2-bullseye
ENV PIP_NO_CACHE_DIR off
ENV PIP_DISABLE_PIP_VERSION_CHECK on
ENV PYTHONUNBUFFERED 1
ENV PYTHONDONTWRITEBYTECODE 1
ENV COLUMNS 80
RUN apt-get update \
 && apt-get install -y --force-yes \
 nano python3-pip gettext chrpath libssl-dev libxft-dev \
 libfreetype6 libfreetype6-dev  libfontconfig1 libfontconfig1-dev\
  && rm -rf /var/lib/apt/lists/*
WORKDIR /code/
COPY requirements.txt /code/
RUN pip install -r requirements.txt
COPY . /code/

Here's what each instruction does:

FROM python:3.11.2-bullseye

This instruction specifies the base image for this Dockerfile, which is the official Python 3.11.2 image for Debian Bullseye.

ENV PIP_NO_CACHE_DIR off
ENV PIP_DISABLE_PIP_VERSION_CHECK on
ENV PYTHONUNBUFFERED 1
ENV PYTHONDONTWRITEBYTECODE 1
ENV COLUMNS 80

These instructions set environment variables that affect how the image is built and how the Python application runs:

PIP_NO_CACHE_DIR is set to "off" to ensure that pip doesn't use any cached files. PIP_DISABLE_PIP_VERSION_CHECK is set to "on" to disable pip version checking. PYTHONUNBUFFERED is set to "1" to disable buffering for standard input and output. PYTHONDONTWRITEBYTECODE is set to "1" to disable writing bytecode files. COLUMNS is set to "80" to ensure that the output of various commands is properly formatted for an 80-column terminal. This variable being set is required by some python libraries. If it is not set they make your project fail with an error.

RUN apt-get update \
 && apt-get install -y --force-yes \
 nano python3-pip gettext chrpath libssl-dev libxft-dev \
 libfreetype6 libfreetype6-dev  libfontconfig1 libfontconfig1-dev\
  && rm -rf /var/lib/apt/lists/*

This instruction installs some Debian dependencies that are required to run python apps.

WORKDIR /code/

This instruction sets the working directory for the image to /code/.

COPY requirements.txt /code/
RUN pip install -r requirements.txt
COPY . /code/

These instructions copy the requirements.txt file to the /code/ directory, install the required Python packages using pip, and then copy the rest of the application code to the /code/ directory. It is done in two separate steps to make builds faster if you haven't changed the requirements file.

Speaking of which, create a requirements.txt file in the root of the project and add two lines in it:

Django==4.2
django-environ==0.10.0
psycopg2-binary==2.9.6

docker-compose.yml for Django

Now let's create the docker-compose.yml file in the root of our project.

version: '3.3'
services:
  db:
    image: postgres
    environment:
      - POSTGRES_USER=tutorial
      - POSTGRES_PASSWORD=tutorial
      - POSTGRES_DB=tutorial
    ports:
      - "127.0.0.1:5432:5432"
  web:
    build: .
    restart: always
    command: python manage.py runserver 0.0.0.0:8000
    env_file:
      - .env
    ports:
      - "127.0.0.1:8000:8000"
    volumes:
      - .:/code
    links:
      - db
    depends_on:
      - db

This is a docker-compose.yml file that defines two services: db and web.

The db service uses the postgres Docker image and sets up a PostgreSQL database. It also specifies some environment variables for the database such as the username, password, and database name. It maps the container's port 5432 to the host's port 5432, so that the database can be accessed from the host machine.

The web service builds an image using the Dockerfile in the current directory (.). It specifies a command to start the Django web server and maps the container's port 8000 to the host's port 8000. It also specifies an environment file (.env) to load environment variables, and it mounts the current directory (.) as a volume inside the container so that code changes are reflected without needing to rebuild the image.

The links option creates a link from the web service to the db service, which allows the web service to access the db service by its hostname (e.g. db). The depends_on option specifies that the web service should not start until the db service is running.

This docker-compose.yml file sets up a Django web server that depends on a PostgreSQL database. Both services are defined within the same network so they can communicate with each other, and can be started with a single command using Docker Compose.

Set environment variables for Docker Compose

Last important part is to provide environment variables file .env:

DJANGO_DEBUG=1
DATABASE_URL=postgres://tutorial:tutorial@db/tutorial
DJANGO_SECRET_KEY=SuperSecretTellNoOne

Here we set variables we talked about, that will enabled the debug mode, set database credentials and provide a very very secret key.

Run Django in Docker Compose locally

In the terminal, in the root of the project (<some_directory>/myproject) run this command:

docker compose up -d

This will start pulling images for PostgreSQL, Python base image and start building our image and then start the project in the background.

The output looks something like this:

docker compose up -d

To view the logs you can run docker compose logs or docker compose logs --tail=100 -f to view last 100 log lines and keep following logs(press CTRL+C to stop logs).

To apply migrations you can run docker compose run python manage.py migrate.

Open browser at http://127.0.0.1:8000/

Django Project running in Docker Compose

Congrats. Our Django is up and running locally in Docker Compose!