Containerize your Django Web Application with Docker
Tags: django, docker, webdev, beginners
Published: 2024-05-16 22:43:16

## Introduction
Containerization is becoming commonplace in modern software development. The practice involves the development of software applications in isolated environments that mimic a desired operating system. Containerization has enabled developers, using different operating systems, to create applications and collaborate in a consistent environment. Containerization essentially standardizes the development environment for a team of software developers collaborating on a project. 

Several containerization technologies exist in the market today, with the most popular ones being [Docker](https://www.docker.com/) and [Kubernetes](https://www.dynatrace.com/news/blog/kubernetes-vs-docker/#:~:text=Docker%20is%20a%20suite%20of,application%20code%20and%20dependencies%20inside). This article walks Django developers through containerization using Docker. The article discusses how to set up a containerized development workspace, and the best practices involved. We will create a simple Django application and containerize it.

## Prerequisites
•	A Working knowledge of web development using Django.<br>
•	A Windows 10+ computer with [Windows Subsystem for Linux (WSL)](https://learn.microsoft.com/en-us/windows/wsl/install) installed.

## Setting up Docker on Windows
You will need to install [Docker on Windows]( https://docs.docker.com/desktop/install/windows-install/) and create a [Docker hub ](https://hub.docker.com/signup) account. Follow the installation guidelines provided to set up Docker Desktop on your Windows. Ensure your system meets the specified [requirements](https://learn.microsoft.com/en-us/windows/wsl/install).

The Docker desktop app acts as a front-end where you can view your images and containers (More on this in soon). Your WSL acts as a backend, where you interact with the Docker Engine, which essentially builds your images and runs your containers. You interact with the Docker Engine by running Docker commands right from your WSL Command Line Interface (CLI).

As a best practice, it is advisable to run Docker as a non-root user using WSL. To set up a non-root user account in your WSL distro with Docker, follow [this]( https://docs.docker.com/engine/install/linux-postinstall/) guideline. If you already have a non-root user and choose not to set up the user with Docker, you will have to preface all your Docker commands with the **sudo** command.
If you have followed the installation process successfully, run `docker –version`. Your terminal should display the Docker version you just installed:
```shell
$ docker --version
docker version 26.0.0, build 2ae903e
```
Docker ships with a pre-built "Hello Word" image. You can check if Docker is installed and running properly by confirming that this image exists. To do so, run this command on WSL:
```shell
$ docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
1b930d010525: Pull complete
Digest:sha256:b8ba256769a0ac28dd126d584e0a2011cd2877f3f76e093a7ae560f2a5301c00
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly. To generate this message, Docker took the following steps:

1. The Docker client contacted the Docker daemon.
2. The Docker daemon pulled the "hello-world" image from the Docker Hub. (amd64)
3. The Docker daemon created a new container from that image which runs the executable that produces the output you are currently reading.
4. The Docker daemon streamed that output to the Docker client, which sent it to your terminal.
To try something more ambitious, you can run an Ubuntu container with: 
$ docker run -it ubuntu bash
Share images, automate workflows, and more with a free Docker ID:
https://hub.docker.com/
For more examples and ideas, visit: https://docs.docker.com/get-started/
```

Then run `docker-info`:
```shell
$ docker info
Client: 
  Debug Mode: false
Server: 
  Containers: 1
  Running: 0
  Paused: 0
  Stopped: 1
  Images: 1
  ...
```
Now we are ready to build and containerize our Django web application.

## But Before we Build…
Let’s understand core Docker terminologies. When building with Docker, you should familiarize yourself with these three fundamental terms:
* A Docker image
* A Docker container
* A Docker host

A Docker image is, basically, a list of instruction that the Docker engine follows to build your project. A Docker image can also be construed as a snapshot in time of your software project. You rebuild your image every time you update software packages in your project, or when you change some key code, as in settings.py. You write an image inside the `Dockerfile`. 

A Docker container is a running instance of a Docker Image. You build your project (an image) inside Docker, then you run it, in which case the image is now known as a container. Docker Host is the underlying Operating System that runs Docker containers. One host could run several containers.

## Now we Build...
### Create a Basic Django application
We are going to build a basic blog application where users can blog on various topics of interest. This article assumes you are already familiar with the flow of building any Django project, hence it will not go into details about building with Django.

Let us think for a moment about the structure and functionality of our project. We want our blog web application to enable a user input a topic and a brief blog about the topic. Each topic could have one or several blog entries. Thus, we will model a topic with a blog entry for each topic.

For this project, we only want to display a list of all available topics though, so we will write a view logic that queries the database to retrieve a list of all topics available. Finally, we will write a template that renders the list of topics on the user interface. We could develop a full blog website. However, for Docker demonstration purposes, we will use this minimalist application.

Let’s get started…
From your CLI:
1.	Create a project directory named code, or any name you prefer, and change into this directory.
    ```mkdir code && cd code```
2.	Install the pipenv python virtual environment package if you do not have it already.
    ```pip install pipenv```
3.	Install Django using pipenv.
    ```pipenv install django```
4.	Activate a virtual environment using the shell command.
    ```pipenv shell```
You should see the name of your directory preceding your WSL distro prompt like this:
```shell
(code) nick@DESKTOP-TOB16R4:~/ 
```
This indicates that your environment is active

### Pipfile and Pipfile.lock
Note that when you installed Django, pipenv created two files:
* Pipfile
* Pipfile.lock

The Pipfile contains a list of dependencies for your project. It serves the same purpose as a requirements.txt file. A Pipfile.lock locks down all project dependencies you installed and the order in which you installed them.

The Pipfile.lock file enforces a deterministic build; you will achieve the same result regardless of the number of times you, or anybody in your team, installs the locked software packages. Without locking down the dependencies and their order, team members may have a slightly differentiated build installations, which could result in conflicts. Remember we want a consistent development environment when working with Docker.
### A Containerized Blog App
While your virtual environment is still active, run:
```shell
django-admin startproject config .
```
Django will create a project level directory called **config** in the current working directory. Do not forget the period after the command, or Django will create an extra directory for our project. We want the project-level directory to be in the current working directory. You could name your project anything else other than config.

Next, we need to migrate our database. The action will apply the default user model and other admin settings to the database.
```shell
python manage.py migrate
```
Next we need to confirm that our project is working as expected:
```shell
python manage.py runserver
```
If you visit **http://127.0.0.1:8000** on your browser, you should see the default Django successfully installed page.
If everything is OK, return to your terminal and exit your virtual environment by typing the command `exit`.

### Build your Image
Our project is working as expected in our local setting. Now we need to host it on Docker. It should show a similar page. To containerize it, we will need to build an image of the project. As mentioned earlier, this is a list of instructions that Docker will run to set up our project. Create a file called Dockerfile in your root directory:

**Dockerfile**
```
# Pull base image
FROM python:3.10
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
# Set work directory
WORKDIR /code
# Install dependencies
COPY Pipfile Pipfile.lock /code/
RUN pip install pipenv && pipenv install --system
# Copy project
COPY . /code/
```

The Dockerfile is executed top to bottom. Thus, the first command must be the **FROM** command. This command pulls over our base image from an online repository, which is python 3.10 in this case. You could specify any Python version you want based on your project needs.

The ENV command set two basic environment variables: PYTHONDONTWRITEBYTECODE prevents Python from writing .pyc files, which we do not need. PYTHONUNBUFFERED formats Docker’s output to the console.

Next we are setting up a working directory called **code** using the WORKDIR command. This step is essential when we are executing commands within our container. If we do not specify a working directory, we would have to type in a long path. By setting a default directory as above, Docker automatically executes commands within this directory.

Next, we install project dependencies using pipenv. Remember that our project contains one dependency, Django, so far. We begin by copying over the contents of our Pipfile and Pipfile.lock into our working directory (code), that we just created. Then we install the dependencies globally within Docker by appending the –system flag at the end of the install command. We use the --system flag to instruct pipenv to install the software packages globally within Docker. This is because pipenv, by default, looks for a virtual environment to install packages, and Docker now acts as our virtual environment.

Next we copy over the rest of our local code into our Docker working directory.<br>
Now let’s build our image with a single command.
Run:
```shell
$ docker build .
```
Docker will output a lot of information on the console. Check the last part of the output to confirm whether Docker successfully built your image.

Next, we need to get this image running so we can interact with our web app from the browser. To accomplish that, we would need to create a 
`docker-compose.yml` file.

**docker-compose.yml**
```shell
version: '3.8'
services: 
  web:
    build: .
    command: python /code/manage.py runserver 0.0.0.0:8000
    volumes: - .:/code
    ports: - 8000:8000
```
Let’s explain the contents of this file:<br>
The first line specifies the Docker compose version, which is 3.8 in this case.

The services command specifies the containers to run, which is a just web service container in this case. As mentioned earlier, you can specify several containers to run.

The build command instructs Docker to look for a Dockerfile within the current working directory. Next, we start our server at port 8000, which is Django’s default port.

The volumes mount syncs Docker’s file system with our local file system, meaning we do not have to edit the files locally once we edit them inside Docker.

Finally, we expose port 8000 within Docker.

Time to run our container. Type this command:
```shell
$ docker-compose up
```
You should get the following output:
```shell
Creating network "code_default" with the default driver
Building web
Step 1/7 : FROM python:3.8 ...
Creating code_web_1 ... done
Attaching to code_web_1
web_1 | Watching for file changes with StatReloader
web_1 | Performing system checks...
web_1 |
web_1 | System check identified no issues (0 silenced).
web_1 | May 03, 2024 - 19:28:08
web_1 | Django version 3.1, using settings 'config.settings'
web_1 | Starting development server at http://0.0.0.0:8000/
web_1 | Quit the server with CONTROL-C.
```
If you visit your browser at http://127.0.0.1:8000, you should see the default Django successfully installed page as before. This means that you have successfully containerized your Django project using Docker.
Since Docker stores a running container in RAM, and a container takes a lot of space (A simple container could be over 1GB), it is advisable to stop the container when not in use. To do so, run:<br>
```shell
$ docker-compose down
```

### Building within Docker
We can start our docker container in **detached mode**. Running a container in detached mode allows you to use a single terminal without having to stop your server or use multiple terminals. To use a container in detached mode, run:
```shell
$ docker-compose up –d 
```

Building within Docker is no different from building locally. The only exception is we now have to preface any command we run with `docker-compose exec web`. 

We are going to create an app called **blog** then create the necessary model, view and template for our blog web app.<br>
Run:
```shell
$ docker-compose exec web python manage.py startapp blog
```
```python
# blog/models.py
from django.db import models

class Topic(models.Model):
    topic = models.CharField(max_length=200)
    
    def __str__(self):
        return self.topic


class Entry(models.Model):
    topic = models.ForeignKey(Topic, on_delete=models.CASCADE)
    text = models.TextField()

    def __str__(self):
        return self.text[:50]
```
Then migrate your database to apply these new models
```shell
docker-compose exec web python manage.py makemigrations blog
docker-compose exec web python manage.py migrate
```

Finally, you should register your models with the admin site at blog/admin.py so you can interact with them via them admin panel at http://127.0.0.1:8000/admin. You can try adding a few topics and entries via the admin.

Next configure URL routes for the blog app:
```python
# config/urls.py
from django.urls import path, include

urlpatterns = [
    path('admin/', include(admin.site.urls)),
    path('', include('blog.urls')),
]
```
```python
# blog/urls.py
from django.urls import path
from .views import TopicListView
urlpatterns = [
    Path('', TopicListView.as_view(), name='topics'),
]
```
Write a view to list all topics available in the database:
```python
# blog/views.py
from .models import Topic
from django.views.generic import ListView

class TopicListView(ListView):
    """Lists all topics"""
    model = Topic
    context_object_name = topic_list
    template_name = 'topics.html'
```
Render the topics list to the user interface
```html
<!--templates/topic.html-->
<!DOCTYPE html>
<html>
    <h1>Topics</h1>
<ul>
    {% for topic in topic_list %}
        <li>{{ topic }}</li>
</ul>
</html>
```
After you have updated your app files like above, try visiting the local host URL to check if it lists all the topics you have added. If you are encountering any errors, Docker provides an error log. Run:<br>
```docker logs```

## A Summary of Steps
In summary, follow these steps to containerize your Django application:
* Create a virtual environment locally and install Django
* Create a new project
* Exit the virtual environment
* Write a Dockerfile and then build the initial image
* Write a docker-compose.yml file and run the container with docker-compose up
* Rebuild your image every time you install a software package
* Restart Docker every time you make changes to your settings.py file
* Run docker logs if you encounter any errors

## Conclusion
Modern web development involves working in teams. Therefore, the development necessitates building using containers such as Docker. It is important for beginners in backend development familiarize with Docker as early as possible. This article covers the fundamentals of getting started with Docker. It discusses fundamental Docker concepts and commands. Whereas there is more to containerization and the Docker technology that requires further learning, this article is an excellent starting point for beginners. You can start here and work your way up to becoming an expert. Docker itself has a steep learning curve, and it takes more than what we have discussed here to gain proficiency. Nevertheless, you can start here and build small, containerized Django applications to familiarize yourself with the basics. Happy containerization!