Welcome back. Here we are at part 4 of the series on how we dockerise every Django project at Webinative.
Here's a quick recap.
Part 1: Created a Django project with a core app and custom User model.
Part 2: Added some must-have third-party Django apps and python packages to our project.
Part 3: Abstracted deployment-specific configurations into environment variables.
We now have a Django application running inside a python virtual environment. This article will focus on containerisation using Docker and Docker-Compose.
A container is just like any other software/application running on your computer. However, the key difference from a regular application is how a containerised application packages all code, dependencies, runtime, system libraries and settings into a standalone unit.
A container image is a static file with executable code that can create a container. In other words, container images become containers when they run on the Docker Engine.
Docker can build images automatically by reading the instructions from a Dockerfile
. A Dockerfile
is a text document that contains all the commands a user could call on the command line to assemble an image.
Imagine yourself assembling a PC.
Your PC is the container — it has everything it needs to run within a self-contained unit.
The local store is the Docker Engine.
The job-sheet at the local store is the container image.
Your parts-list is the Dockerfile — the instructions to build your container image.
Let's start by defining the Dockerfile
for our Django application. See the final version below. We will go through the sections one by one and understand them.
FROM python:3.10-slim-bullseye
ENV PYTHONBUFFERED 1
ARG USERNAME=webinative
# replace with your actual UID and GID if not the default 1000
ARG USER_UID=1000
ARG USER_GID=${USER_UID}
# create user
RUN groupadd --gid $USER_GID ${USERNAME} \
&& useradd --uid ${USER_UID} --gid ${USER_GID} -m ${USERNAME} \
# create a folder for vscode editor stuff
&& mkdir -p /home/${USERNAME}/.vscode-server \
&& chown ${USER_UID}:${USER_GID} /home/${USERNAME}/.vscode-server \
# create a folder for project code
&& mkdir -p /home/${USERNAME}/code \
&& chown ${USER_UID}:${USER_GID} /home/${USERNAME}/code \
# add sudo support
&& apt-get update && apt-get install -y sudo \
&& echo ${USERNAME} ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/${USERNAME} \
&& chmod 0440 /etc/sudoers.d/${USERNAME}
# install git and psycopg2 dependencies
RUN apt-get install -y git gcc libpq-dev
USER ${USERNAME}
WORKDIR /home/${USERNAME}/code
ADD pip-requirements /home/${USERNAME}/code/
# install python packages locally for user "webinative"
RUN pip install -r pip-requirements
# remove psycopg2 dependencies
RUN sudo apt-get remove -y gcc libpq-dev
# not switching back to "root" user
So, what does this file do?
We start with a base python image. How to decide which image/version to use?
Start with your production environment — where you would deploy the application for your end users. In our case, we will use the most recent Ubuntu LTS 22.04 Jammy Jellyfish version.
Then figure out the default python version that's bundled in the server OS. In our case, it is Python v3.10.2 (See Jammy Jellyfish realease notes).
Finally, pick an official python image from the Docker Hub.
Ubuntu 22.04 LTS is based on Debian Bookworm. But as of this writing, there are NO python images for Bookworm. So, we will use the version based on the previous Debian release — Bullseye. In addition, we will use the slimmer version to keep the container image size minimal.
FROM python:3.10-slim-bullseye
Our host OS and the container share our project's code. You'll edit the code on your code editor (running on the host OS) while the application runs within the container.
Any code generated within your container should be readable/writable on your host OS and vice-versa. If not, you will encounter permission issues (when creating apps, migrations, translation files, handling file uploads and much more).
To ensure this, we will,
webinative
" within the container with UID and GID the same as our host OS user account./home/webinative/code
" folder and ensure ownership.Find your UID and GID by running the following command in the terminal,
id
# outputs something like
# uid=1000(magesh) gid=1000(magesh) groups=...
In the example above, my UID is 1000, and my GID is 1000.
We create a new Linux user named webinative
with the same UID and GID obtained from the previous step.
ARG USERNAME=webinative
# replace with your actual UID and GID if not the default 1000
ARG USER_UID=1000
ARG USER_GID=${USER_UID}
# create user
RUN groupadd --gid $USER_GID ${USERNAME} \
&& useradd --uid ${USER_UID} --gid ${USER_GID} -m ${USERNAME} \
# create a folder for vscode editor stuff
&& mkdir -p /home/${USERNAME}/.vscode-server \
&& chown ${USER_UID}:${USER_GID} /home/${USERNAME}/.vscode-server \
# create a folder for project code
&& mkdir -p /home/${USERNAME}/code \
&& chown ${USER_UID}:${USER_GID} /home/${USERNAME}/code \
# add sudo support
&& apt-get update && apt-get install -y sudo \
&& echo ${USERNAME} ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/${USERNAME} \
&& chmod 0440 /etc/sudoers.d/${USERNAME}
Next, we will configure the following instructions for the container,
webinative
./home/webinative/code/
.pip-requirements
file into our working directory and install project dependencies.# install git and psycopg2 dependencies
RUN apt-get install -y git gcc libpq-dev
USER ${USERNAME}
WORKDIR /home/${USERNAME}/code
ADD pip-requirements /home/${USERNAME}/code/
# install python packages locally for user "webinative"
RUN pip install -r pip-requirements
# remove psycopg2 dependencies
RUN sudo apt-get remove -y gcc libpq-dev
We have the instructions to build our container image in the Dockerfile
. However, we'll have to specify more configurations when running the container, such as,
We could specify these configurations in our command-line while running the container. However, the command could quickly become too big to type manually and difficult to maintain.
To simplify the runtime command, we will use Docker Compose.
Compose is a tool for defining and running multi-container Docker applications. With Compose, you use a YAML file to configure your application's services. Then, with a single command, you create and start all the services from your configuration.
Here's our docker-compose.yml
file,
version: '3'
services:
dockerise-django:
build: .
user: webinative
command: python -u manage.py runserver 0:8000
container_name: dockerise-django
env_file:
- ./dockerise-django.env
- ./.env
volumes:
- .:/home/webinative/code
- dockerise-django-vscode:/home/webinative/.vscode-server
ports:
- "8000:8000"
restart: on-failure
volumes:
dockerise-django-vscode:
Let's go through the options one by one.
services
Every component in our application that can be scaled/replaced independently is a service. Simply put, every service is a container. Other examples of services are a Postgres database, a RabbitMQ server, an SMTP client, etc.
In our case, the Django application is a service. We name it dockerise-django
build
Specifies where to look for the Dockerfile
when building the container image.
Our target Dockerfile
is in the current folder. Hence, the single dot.
user
Specify the user
that will run the container's command
. Otherwise, the container's root
user runs the command.
We'll use the webinative
user account to run the Django development server.
command
The command that the container executes. The container runs as long as the command. Upon successful completion or on error, the container stops.
Our Django container runs the development server command,
python -u manage.py runserver 0:8000
The additional argument -u
forces the stdout and stderr streams to be unbuffered so that you view the application logs in real-time during development.
container_name
Specify a custom container name instead of a default generated name. We use dockerise-django
.
env_file
File(s) containing environment variables for the container.
Let's take a look at our .env
file.
DJANGO_ALLOWED_CIDR_NETS=192.168.0.0/24
DJANGO_ALLOWED_HOSTS=localhost
DJANGO_DEBUG=True
DJANGO_INTERNAL_IPS=127.0.0.1
DJANGO_TIME_ZONE=UTC
DJANGO_SECRET_KEY="django-insecure-q-*lrcfa-ll41wh@9=l+f=96%!9%vpm8h)jdw)gpw7)i41c94k"
Of these ENV vars, only the DJANGO_ALLOWED_CIDR_NETS
value changes from developer to developer depending on their location and local network settings. All other values remain the same (for the development environment).
Let's move the other ENV vars into a new file named dockerise-django.env
. Git will track this new file and make it available to all team members.
# contents of dockerise-django.env
DJANGO_ALLOWED_HOSTS=localhost
DJANGO_DEBUG=True
DJANGO_INTERNAL_IPS=127.0.0.1
DJANGO_TIME_ZONE=UTC
DJANGO_SECRET_KEY="django-insecure-q-*lrcfa-ll41wh@9=l+f=96%!9%vpm8h)jdw)gpw7)i41c94k"
# contents of .env
DJANGO_ALLOWED_CIDR_NETS=192.168.0.0/24
We'll include both env
files in our container.
env_file:
- ./dockerise-django.env
- ./.env
volumes
Specify mapping between a host folder and a container folder.
version: '3'
services:
dockerise-django:
...
volumes:
- .:/home/webinative/code
- dockerise-django-vscode:/home/webinative/.vscode-server
volumes:
dockerise-django-vscode:
We share our code (current folder) with the /home/webinative/code
folder within the container.
We will use Visual Studio Code with Dev containers extension. We provision a named volume dockerise-django-vscode
to store all the vscode-server
stuff. With this config, all binaries, extensions and cache that VS Code needs are NOT lost when containers are re-provisioned (destroyed and run again).
We also define the named volume as a top-level declaration at the end of the YAML file.
ports
Specify the mapping between a host port and a container port.
ports:
- "8000:8000"
Our Django development server runs within the container on port 8000. We map that port to our host machine's 8000 port for our browser to send and receive traffic.
Note: Ensure no other applications run on your host machine's 8000 port.
restart
Our container stops every time our application raises an unhandled exception. During development, errors are unavoidable, and we do not want to start the container manually every time. We can configure our container to restart automatically upon encountering unhandled exceptions.
We now have the instructions ready to build our container image. Let's test them.
In your terminal, run,
docker-compose build
The command usually takes about a minute to complete the first time.
Next, verify the built docker-image using the command,
docker image ls | grep dockerise-django
You should see it listed as shown below.
Next, we'll run the container from the built image.
In your terminal, run,
docker-compose up
Open your web browser and visit http://localhost:8000/
. You should see a screen as shown below.
Notice that we no longer see the debug toolbar in our browser. We'll have to reconfigure the debug-toolbar to work with our docker setup.
Replace the INTERNAL_IPS
setting in the settings.py
file, as shown below.
if DEBUG:
ALLOWED_CIDR_NETS_ENV = os.getenv("DJANGO_ALLOWED_CIDR_NETS")
if ALLOWED_CIDR_NETS_ENV:
ALLOWED_CIDR_NETS = ALLOWED_CIDR_NETS_ENV.split(",")
import socket
hostname, _, ips = socket.gethostbyname_ex(socket.gethostname())
INTERNAL_IPS = [ip[: ip.rfind(".")] + ".1" for ip in ips] + [
"127.0.0.1",
"10.0.2.2",
]
The new configuration makes the DJANGO_INTERNAL_IPS
environment variable obsolete. Delete it from the dockerise-django.env
file.
In your terminal, press Ctrl + C to stop the running container. Recreate the container with the updated env file using the command,
docker-compose up
Refresh your browser, and you should see the debug-toolbar appearing again.
Congratulations
Your django application is now running inside a container.
Some useful commands that might come in handy,
# run the containers in background mode
docker-compose up -d
# stop the containers
docker-compose stop
# start the containers
docker-compose start
# destroy the containers
docker-compose down
We have now containerised our Django application. We won't require the python virtual environment we created in the first part of this series anymore.
You can safely remove it using the following commands,
deactivate
rmvirtualenv dockerise_django
Let's remove the 404 error page by adding a new view, template and URL config.
<!-- contents of core/templates/core/home.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Home | Dockerised Django App</title>
</head>
<body>
<h1>Dockerised Django App</h1>
<p>Author: Magesh Ravi</p>
<p>Organisation: Webinative Technologies</p>
</body>
</html>
# contents of core/views.py
from django.shortcuts import render
def home(request):
return render(request, "core/home.html")
# contents of core/urls.py
from django.urls import path
from .views import home
app_name = "core"
urlpatterns = [path("", home, name="home")]
# update dockerise_django/urls.py
urlpatterns = [
path("", include("core.urls", namespace="core")), # add this line
path("admin/", admin.site.urls),
path("__debug__/", include("debug_toolbar.urls")),
]
Visit http://localhost:8000/
in your browser, and you should see a page as shown below.
You might notice that VS Code's Intellisense (code-completion) has stopped working with python files (views.py
and urls.py
). That's because we have moved the project's python runtime from the virtual environment to the container, and our code editor does not know about this change.
We must connect VS Code to the python binaries inside the container for the code-completion to work again.
Dev Containers is a VS Code extension that does exactly this. Install it from the extensions view.
Once installed, connect VS Code to your container by pressing Ctrl + Shift + P
and search for "Dev Containers: Attach to Running Container..." and hit Enter.
You should then see a list of running containers to choose. Pick dockerise-django
.
You should see a new VS Code window open up.
Go to File > Open Folder, browse /home/webinative/code/
and click OK.
You should see the complete folder contents in your sidebar.
Next, install the Python extension inside the container.
Once you have installed the python extension, you should see code completions working as expected in python files.
To summarise, in this post, we have successfully
Dockerfile
.docker-compose.yaml
.All changes described in this post have been committed to the GitHub branch part_04/containerise
of this repository.
Like this article? Have a comment to share or a question to raise? Join our Discord server.