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:
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:
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.
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:
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!