The Simplest Django + Docker Example

Tim White
10 min readMay 17, 2021

The Simplest Docker + Django Example

Docker makes deploying whole application stacks a predictable process, which can be very helpful in getting your application developed and deployed reliably. A full Docker implementation can be quite complex, but you can learn a lot from a very simple example. This article seeks to walk you through the simplest working example so that you can start your learning journey.

This example uses Django, but would work similarly for other Python stacks, including FastAPI or Flask. Since this is the simplest example, we won’t be using a database.

All the code we will be walking through in this article can be found in the companion GitHub Repository.

Docker Installation

In order to follow along with this example, you will need Docker installed on your local machine. The open-source version of Docker that runs locally is all you need. If you want to follow along with pushing images to the Docker Registry (which is the Docker image equivalent of GitHub), then you will need to create a docker.com account.

Once you have Docker installed, you should be able to open a terminal (or cmd) window, and type docker and see helpful output.

One nice thing about Docker is that once you have it installed, you don’t need to install Python or Django locally, as we’ll be running everything inside Docker.

Docker Terminology

There are some handy terms that you should get familiar with when working with Docker:

How the parts of the Docker ecosystem are related

Dockerfile

The Dockerfile is the secret sauce to Docker — a simple set of instructions for starting with a base operating system and installing everything you need to run your application. Each instruction that you add to your Dockerfile creates a layer, and each layer is saved independently. When you build your Dockerfile, you end up with a Docker image, which you can visualize as a zip file with your operating system, supporting packages, and application inside it.

Docker image

The Docker image is a portable compressed file that contains the base operating system that you started with as well as everything you specified in your Dockerfile. Imagine that you started with a fresh operating system install on your server, installed only the exact things on it you needed to run your application (including your application code), and then zipped up the whole server filesystem. You can then send this image to someone else to run as a Docker container on their system. Another way of thinking about it is that you have taken a “snapshot” of your server and stored as a Docker image.

Docker container

Containers are Docker images that have been run. Since you can pass arguments to your Docker image when you run it, each invocation is saved separately by the Docker runtime as a container. You can look at the logs of running or stopped containers, and you can remove them once they have run.

Docker runtime

The Docker ‘runtime’ or “container runtime” is what enables images to run as containers. It adapts the “host” operating system to be able to run the “guest” images as containers. When you install Docker Engine, this is the most important thing you are installing.

Docker container registry

This is a central repository to store and distribute Docker images in after they have been built. It works a bit like GitHub and PyPi, but for Docker images. You can push your images to the Docker.com container registry or set up your own.

Docker Compose

Docker compose is a way of running one or more Docker images. You can specify all the options for the all the containers in a single docker-compose.yml file, including environment variables and port mappings. You can also call out dependencies between containers, and map file locations inside your running container to your local file system. Docker compose comes with most Docker installations.

Exploring the Simplest Docker + Django Project

Let’s take a look at the layout of our project:

Simplest Docker + Django Project Layout

The simplest_django folder contains the Django project. We have a single template which is pointed to in urls.py, a Django settings.py file, and the standard wsgi.py that Django creates.

Here is a snippet of the settings.py file that shows how we use os.environ.get to retrieve an Environment Variable called MAGIC_MESSAGE and bind it to a Django setting. We’ll show this value on our home page.

import osMAGIC_MESSAGE = os.environ.get("MAGIC_MESSAGE")

Here is the urls.py, so you can see what we expect to happen when you access this application:

# Django URLs

from django.urls import path
from django.views.generic import TemplateView

from django.conf import settings

urlpatterns = [
path(
'',
TemplateView.as_view(template_name=”home.html”,
extra_context={“magic”: settings.MAGIC_MESSAGE})
),
]

We are using Django’s TemplateView to specify that we should render thehome.html template at the root of the application, and we’re passing in the value of the MAGIC_MESSAGE setting to that template. (MAGIC_MESSAGE is a setting that we created, not a default part of Django).

The home.html template is also simple:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Home Page</title>
<link rel="stylesheet" href="/statics/style_sheet.css">
</head>
<body>
<h1>Home Page of Docker App</h1>
<p class="magic">{{ magic|default_if_none:"No Magic. :(" }}</p></body>
</html>

We just show “Home Page of Docker App” in a heading, and then show the value of the magic setting that we passed in, or “No Magic” if it was not set.

The requirements.txt file just has Django<4.0 in it, since that is what we need to run this application.

So that’s the whole of the Django part of our code, let’s move on to the Docker setup!

Docker setup

The most important part of the Docker setup is the Dockerfile:

FROM python:3.9WORKDIR /usr/src/app
COPY requirements.txt ./
RUN pip install -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]

We start with an official Docker image called python. After the colon, we specify the 3.9 tag of that image. This is an image that starts with Debian Linux, and then installs Python 3.9 in it. This is all taken care of by the official image maintainers, so we don’t have to do any of that ourselves. Don’t be put off if you aren’t familiar with Debian, one of the advantages of Docker is that you don’t need to do system administration in the container, especially one like this that already has everything you need to run Python.

Next, the WORKDIR Docker command switches to the /usr/src/app directory, which is a recommended location to place your application code.

We then use the COPY Docker command to copy our requirements.txt file from our current local filesystem into the image (into the /usr/src/app/ directory, since that’s what we set as our WORKDIR).

The RUN command runs pip to install the Python packages in our requirements.txt file (which is just Django).

Then we COPY all of the contents of our current local filesystem directory into the image. This will bring in our simplest_django folder, as well as manage.py (and everything else in the same directory as the Dockerfile).

The EXPOSE command tells the Docker runtime that we will allow network connections into this image on port 8000.

Finally, the CMD command tells the Docker runtime what command to execute when it starts up the container. We are specifying python manage.py runserver 0.0.0.0:8000 to run Django on port 8000. We specify each part of that command line as a separate argument to make it easier for this command to be parsed by the runtime and container as we intended.

This Dockerfile now has the full set of instructions to:

1) Start with a fresh install of Debian Linux and install Python 3.9, pip, and build tools needed to compile Python packages (built into the python image).

2) Install our application’s package dependencies with pip.

3) Copy our application code into the image.

4) Tell the Docker runtime we want to allow network connections on port 8000.

5) Run Django on port 8000 when the container starts.

You might be wondering why we COPY the requirements.txt and our application code in two separate lines. This is because each line creates a separate layer in the image. So, when we build the image, it caches each layer. It only rebuilds layers where we either changed the command in the Dockerfile or changed the local files that command is operating on.

Importantly, it also re-builds all the layers that appear in the Dockerfile after the last layer that changed. By separating the pip install from the app install, we can prevent re-installing Django into our image over and over again when we update our app code, since that layer will be cached, and our app files are copied in later in the Dockerfile.

Building the Docker image

Now that we have the Dockerfile, we need to build it to create the image and give it a tag we can refer to later. You need to have your Docker running locally, and then you can issue this command to build it:

docker build --tag docker-simple .

Note that there is a period at the end of the command — that is part of the command.

This will download the python:3.9 image, run all the commands in the Dockerfile, and create a new image tagged docker-simple that we can run.

Note that since we are copying our application code into the image during the build, the only way to get changes to our application code into the image is to run `build` again. There are other ways to use Docker for local development, where you map your local disk into the file system of the container as it is running, rather than copying the code at build time. However, for production deployments, it is typical to COPY all the code into the image like we are doing here.

Building the Docker Image Layer Cake from the Bottom Up

Running the Docker Image as a Container

There are two main ways to run our image as a container. Either directly from the command line or using Docker compose.

First, the command line version:

docker run -p 8000:8000 -e MAGIC_MESSAGE="Docker command line" \ docker-simple

The output you will see after you run this should be:

Watching for file changes with StatReloader

Which is Django’s default output. It will sit there running until you hit ctrl-c to stop it.

The docker run command tells the Docker runtime to fire up our image that we tagged docker-simple and map our local network port 8000 into the container’s port 8000. It also specifies an environment variable called MAGIC_MESSAGE with the value of Docker command line. This is read by our Django application and displayed on our default web page.

Try browsing to http://localhost:8000, and you should see:

Our Simple Django App Running in Docker!

Try hitting ctrl-c, and running the container with different values for MAGIC_MESSAGE!

Using Docker compose

We can run also use Docker compose to run our app. Included in our GitHub repo is a docker-compose.yml file that is set up to build and run our app:

version: "3.6"services:
django:
build: .
environment:
- MAGIC_MESSAGE=Docker Compose
- SECRET_KEY=23408728djlkjd37nmckdllsorhh377889s900&
ports:
- "8000:8000"

The version in this case is the version of the Docker compose file format, and not related to the version of Python or Docker we are using.

services is the list of services we would like to control with this Docker compose file. In this case, we just have our django service, but in a more full-featured setup, we might also have a web server (e.g. Nginx), and a database (e.g. Postgres), which can all run as interconnected Docker containers with Docker compose.

build: . tells Docker compose that we want to build and run the Dockerfile in the same directory as the docker-compose.yml file.

environment is where we pass in our Environment Variables, in this case our MAGIC_MESSAGE, and a value for Django’s SECRET_KEY.

ports is how we map the local filesystem port to the port inside the container.

You’ll notice that these commands are the same as the options of the run command.

You can run this Docker compose file like this:

docker compose up

This will start up the container specified in our docker-compose.yml file.

You can hit ctrl-c to stop the server.

You can start up your container with the -d option to have it run in the background:

docker compose up -d

And then you use:

docker compose stop

to stop it.

Try stopping and starting a few times and changing the MAGIC_MESSAGE in the docker-compose.yml file.

Pushing your image to the Docker Trusted Registry

As a final, optional step, you can push the image that you built to your account in the Docker Trusted Registry:

docker login
docker push <your account name>/docker-simple:latest

You only need to login one time, and then it should remember you. <your account name> is your login name for the https://docker.io website.

This will push your image up to the Docker Trusted Registry. Once you’ve done this, it will be available for other folks to pull:

docker pull <your account name>/docker-demo:latest

This will pull that image down onto their computer so they can run it.

You can also specify the image from the registry in your docker-compose.yml file, rather than having it build and run your local directory:

version: "3.6"services:
django:
image: <your account name>/docker-simple:latest
environment:
- MAGIC_MESSAGE=Docker Compose
- SECRET_KEY=23408728djlkjd37nmckdllsorhh377889s900
ports:
- "8000:8000"

It will then download that image locally if it doesn’t already have it. It will also check to see if a newer version of that tag of that image is available before starting up.

Hopefully walking through the simplest example of Django + Docker has helped you understand how Docker can enable running Django in an isolated container without even having to have Python installed on your local machine, and how you can use Environment Variables to pass information into your `container` when it runs, even if your application code is baked into the image.

You can imagine how powerful this can be using other official Docker images such as those for Nginx and Postgres with Docker compose to run multiple servers in concert.

Enjoy learning about Docker!

Was this content helpful to you? Consider buying me a coffee! :)

--

--