Django with Docker
How to containerize a Django application with Docker
Deploying applications is never ease regardless of the provisioning one may take. Containers solve many of the problems of application deployment. In this post, I want to describe how to containerize a minimal django application with docker. I will assume that docker is already installed on the machine. If not, checkout the official docker documentation. The code for this notebook can be found at this repository.
This post is basically edited from the testdriven.io; Dockerizing Django with Postgres, Gunicorn, and Nginx post. You should check this article out if you want more details on what is happening.
Assuming that docker is already installed on the host machine, I can check the version of docker and docker-compose by typing in the terminal.
docker --version
docker-compose --version
The application won't do something really great as my goal here is to understand how to make these components work together. Thus, the application I will be looking at has two main components in terms of infrastructure. Namely,
- It uses Django to support HTTP requests/responses
- It uses MySQL for persistence
Creating a simple Django project is fairly easy. Checkout how to do so here. Let's create a hello_world_django
project. I will have the project files in the app
directory. So
mkdir app && cd app
django-admin startproject hello_world_django .
The above creates the app
directory and within that directory it creates the hello_world_django
.
├── hello_world_django
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── manage.py
Let's create a requirements.txt
file in the app
directory with the following contents
Django==3.0.7
In order to containerize the hello_world_django
project, we need to have a Dockerfile
. A Dockerfile
specifies overall how our application is to be built. So in the app
directory, create a Dockerfile
with the following contents
# pull official base image
FROM python:3.8.3-alpine
# set work directory
WORKDIR /usr/src/app
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt
# copy project
COPY . .
The Dockerfile
above, starts with an Alpine-based Docker image for Python 3.8.3. It then sets a working directory along with two environment variables:
-
PYTHONDONTWRITEBYTECODE
: Prevents Python from writingpyc
files to disc (equivalent topython -B
option) -
PYTHONUNBUFFERED
: Prevents Python from bufferingstdout
andstderr
(equivalent topython -u
option)
Finally, it updates pip
, copies over the requirements.txt
file, installed the dependencies, and copied over the Django project itself. Although we can use docker build
to build our image, I will use docker-compose
to do so. In the source directory, create a file called docker-compose.yml
with the following contents
version: '3.8'
services:
web:
build: ./app
command: python manage.py runserver 0.0.0.0:8000
volumes:
- ./app/:/usr/src/app/
ports:
- 8000:8000
env_file:
- ./.env.dev
We also need one more file, namely the .env.dev
file that contains the following
DEBUG=1
SECRET_KEY=foo
DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 0.0.0.0 [::1]
The file should also be placed at the root directory where the docker-compose.yml
is located. We also need to update the settings.py
file so that we can retrieve these from the environment under which the application is running.
SECRET_KEY = os.environ.get("SECRET_KEY")
DEBUG = int(os.environ.get("DEBUG", default=0))
# 'DJANGO_ALLOWED_HOSTS' should be a single string of hosts with a space between each.
# For example: 'DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 [::1]'
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS").split(" ")
Let's now build the image and check if everything works as described above. We can do so
docker-compose build
Start the container by using
docker-compose up -d
We can view the application at http://0.0.0.0:8000/. This should display django's default landing page. So far so good. Let's now try to integrate MySQL into the mix.
Adding MySQL into the mix, we just need to add a new service into docker-compose.yml
. This is shown below
version: '3.7'
services:
web:
build: ./app
command: python manage.py runserver 0.0.0.0:8000
volumes:
- ./app/:/usr/src/app/
ports:
- 8000:8000
env_file:
- ./.env.dev
db:
image: mysql:5.7
container_name: mysql_my_django_app
ports:
- '3306:3306'
environment:
MYSQL_DATABASE: 'django_app_demo'
MYSQL_PASSWORD: 'password'
MYSQL_ROOT_PASSWORD: 'password'
volumes:
mysql_data:
To persist the data beyond the life of the container we configured a volume. This config
will bind mysql_data
to the "/var/lib/mysql/data/"
directory in the container.
Note that since the default database in django is sqlite3
, we need to update the DATABASES
entry in the settings.py
file according to
DATABASES = {
'default': {
'ENGINE': os.environ.get("SQL_ENGINE", "django.db.backends.sqlite3"), #'django.db.backends.mysql',
'NAME': os.environ.get("SQL_DATABASE", BASE_DIR / "db.sqlite3"), #'django_app_demo',
'USER': os.environ.get("SQL_USER", "user"), #'root',
'PASSWORD': os.environ.get("SQL_PASSWORD", "password"), #'password',
'HOST': os.environ.get("SQL_HOST", "localhost"), #'db',
'PORT': os.environ.get("SQL_PORT", "3306"), #3306,
}
}
Similarly, we update the .env.dev
file now looking like
DEBUG=1
SECRET_KEY=foo
DJANGO_ALLOWED_HOSTS=localhost 127.0.0.1 0.0.0.0 [::1]
SQL_ENGINE=django.db.backends.mysql
SQL_DATABASE=django_app_demo
SQL_USER=root
SQL_PASSWORD=password
SQL_HOST=db
SQL_PORT=3306
We also need to to install mysqlclient
otherwise we get a django exception django.core.exceptions.ImproperlyConfigured: Error loading MySQLdb module.
We add this in the requirements file. So the requirements.txt
is now as follows
Django==3.0.7
mysqlclient==2.0.3
We now have two containers. Let's build the new image and spin the two containers
docker-compose up -d --build
Once again we can access the application at http://0.0.0.0:8000/.
In order to be able to persist data, we need to create the database tables. Django uses the notion of migrations in order to build and monitor the database tables. Let's instruct Django to run any migrations. Typically, we don't want to do that every time we fire up the container, so I just use a manual approach
docker-compose exec django_app python manage.py migrate
In this post, I described how to containerize a minimal Django-based web application. Specifically, I used the following docker commands