Dockerizing a Python Django Web Application
This article will cover building a markdown editor application written in Django and running it in the much talked about and discussed Docker. Docker takes all the great aspects of a traditional virtual machine, e.g. a self-contained system isolated from your development machine and removes many of the drawbacks such as system resource drain, setup time, and maintenance.
When building web applications, you have probably reached a point where you want to run your application in a fashion that is closer to your production environment. Docker allows you to set up your application runtime in such a way that it runs in exactly the same manner as it will in production, on the same operating system, with the same environment variables, and any other configuration and setup you require.
By the end of the article you’ll be able to:
- Understand what Docker is and how it is used,
- Build a simple Python Django application, and
- Create a simple
Dockerfileto build a container running a Django web application server.
- Setup a Continuous Integration and Delivery (CI/CD) pipelines to build and test the Docker image automatically
What is Docker, Anyway?
Docker’s homepage describes Docker as follows:
“Docker is an open platform for building, shipping and running distributed applications. It gives programmers, development teams, and operations engineers the common toolbox they need to take advantage of the distributed and networked nature of modern applications.”
Put simply, Docker gives you the ability to run your applications within a controlled environment, known as a container, built according to the instructions you define. A container leverages your machine’s resources much like a traditional virtual machine (VM). However, containers differ greatly from traditional virtual machines in terms of system resources. Traditional virtual machines operate using Hypervisors, which manage the virtualization of the underlying hardware to the VM. This means they are large in terms of system requirements.
- Docker doesn’t require the often time-consuming process of installing an entire OS to a virtual machine such as VirtualBox or VMWare.
- You create a container with a few commands and then execute your applications on it via the Dockerfile.
- Docker manages the majority of the operating system virtualization for you, so you can get on with writing applications and shipping them as you require in the container you have built.
- Dockerfiles can be shared for others to build containers and extend the instructions within them by basing their container image on top of an existing one.
- The containers are also highly portable and will run in the same manner regardless of the host OS they are executed on. Portability is a massive plus side of Docker.
Before you begin this tutorial, ensure the following is installed to your system:
Setting Up a Django web application
Let’s jump directly to the application that we’ll dockerize. We’ll start from the Martor project, which implements a live markdown editor for Django:
- Go to the django-martor-editor repository. And Fork it.
- Clone the repository to your local machine.
Let’s take a look at the project structure, I’ve omitted some files and folders we won’t be visiting today:
├── requirements.txt # < Python module list
└── martor_demo # < Django Project root
├── app # < App code
│ ├── admin.py
│ ├── apps.py
│ ├── forms.py
│ ├── migrations
│ ├── models.py
│ ├── templates
│ ├── urls.py
│ └── views.py
├── manage.py # < Django management tool
└── martor_demo # < Django main settings
You can read more about the structure of Django on the official website. You control the application for development purposes using the
Before we can run it though, we’ll need to download and all the dependencies.
First, create a Python virtual environment:
$ python -m venv venv
$ echo venv/ >> .gitignore
$ source venv/bin/activate
Next, add some of the Python modules we’ll need:
- Gunicorn: gunicorn is an HTTP server. We’ll use it to serve the application inside the Docker container.
- Martor: Martor is Markdown plugin for Django
$ echo martor >> requirements.txt
$ echo gunicorn >> requirements.txt
Install all the modules using:
$ pip install -r requirements.txt
Push the change to GitHub:
$ git add .gitignore requirements.txt
$ git commit -m "added martor and gunicorn"
$ git push origin master
And start the development server, you can visit your application at http://127.0.0.1:8000:
$ cd martor_demo
$ python manage.py runserver
If you check the output of the previous command, you’ll see this message:
You have 18 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.
Django prints this warning because it has detected that the database has not been initialized.
To initialize a local test database and get rid of the message run:
$ python manage.py makemigrations
$ python manage.py migrate
Testing in Django
In this section, let’s add some tests to the application. Tests are our first line of defense against bugs.
Django uses the standard Unittest library, so we can get on writing tests right away.
Create a file called
from django.test import TestCase
from app.models import Post
post = Post(title="My Title", description="Blurb", wiki="Post Body")
self.assertEqual(post.title, "My Title")
self.assertEqual(post.wiki, "Post Body")
The code is illustrative of a normal unit test:
- Import the
Postmodel from the application.
- Create a
postobject with some initial values.
- Check that the values match expectations.
To run the test case:
$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
Ran 1 test in 0.001s
Destroying test database for alias 'default'...
Another tests that Django supplies are the deployment checklists. These are scripts that check for potentially dangerous security settings.
To run the checklist:
$ python manage.py check --deploy
You’ll likely see some warnings. For demo-ing, we can live with the warnings. Once you go to production, you might want to take a closer look at the messages and what they mean.
Static vs Dynamic Files
The distinction between static and dynamic is important once we release to production. Dynamic files have code that must be evaluated on each request, so they are expensive to run. Static files don’t need any execution, they don’t need a lot of resources to be served and can be cached with proxies and CDNs.
To configure the static file location:
- Edit the file
- Locate the
MEDIA_ROOTvariables and replace the lines with these:
. . .
STATIC_ROOT = os.path.join(BASE_DIR, "static")
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
Django collects all static files in one directory:
$ python manage.py collectstatic
The final test we’ll do is a style check. Python has strict forms that can be validated using Flake8, a static analysis tool.
Install and run the tool to check there are no style errors in the project:
$ pip install flake8
$ flake8 . --max-line-length=127
Before proceeding, push all modifications to GitHub:
$ git add martor_demo/settings.py app/testPosts.py
$ git add static
$ git commit -m "add unit test and static files"
$ git push origin master
Setting up a CI/CD pipeline in Semaphore takes only a few minutes, once it’s in place it, Semaphore will run the tests for you on every update and, if there are no bugs, build the Docker image automatically.
- Visit Semaphore and sign up for a free account using the Sign up with GitHubbutton.
- Use the + (plus sign) button next to Projects to find your GitHub repository:
- Click on Choose next to your repository:
- Click on Continue to workflow setup.
- Select the single job templante and click on Customize it first
This will open the Workflow Builder:
The main elements of the builder are:
- Pipeline: a pipeline is made of blocks that are executed from left to right. Pipelines usually have a specific goal such as building and testing code.
- Block: blocks group jobs that can be executed in parallel. Jobs in a block usually have similar commands and configurations. Once all job in a block complete, the next block begins.
- Job: jobs define the commands that do the work. They inherit their configuration from the parent block.
- Promotions: We can define multiple pipelines and connect them with promotions to get complex multi-stage workflows.
The first block has to download the Python modules and build the project:
- Click on the first block and set its name to “Build”
- On the job commands block type the following:
sem-version python 3.9
pip install --cache-dir .pip_cache -r requirements.txt
- Click on Run the Workflow.
- Set the branch to master.
- Click on Start.
We have three commands in Semaphore’s built-in toolbox:
- sem-version activates a specific version of one of the supported languages. In the case of Python, it also setups a virtual environment.
- checkout uses git to clone correct code revision.
- cache stores and restores files in the project-wide cache. Cache can figure out which files and directories it needs to keep. We can use it to avoid having to download Python packages each time.
The initial CI pipeline will start immediately, a few seconds later it should complete without error:
Add a second block to run the tests:
- Click on Edit Workflow.
- Click on + Add Block.
- Set the name of the block to “Test”.
- Open the Prologue section, type the following commands. The prologue is executed before each job in the block:
sem-version python 3.9
pip install --cache-dir .pip_cache -r requirements.txtcd martor_demo
python manage.py makemigrations
python manage.py migrate
python manage.py test
- Add a second job called “Checklist” and add the following commands:
python manage.py check --deploy
- This is a good place to add some style checking. Add a third job called “Style check” with the following commands. We’re using flake8 to check the style of the code:
pip install flake8
flake8 martor_demo/ --max-line-length=127
Originally published at https://semaphoreci.com on May 12, 2022.