Build a rock for a Django app

In this tutorial, we’ll create a simple Django app and learn how to containerise it in a rock, using Rockcraft’s django-framework extension.

It should take 25 minutes for you to complete.

Setup

We recommend starting from a clean Ubuntu installation. If we don’t have one available, we can create one using Multipass:

Is Multipass already installed and active? Check by running

snap services multipass

If we see the multipass service but it isn’t “active”, then we’ll need to run sudo snap start multipass. On the other hand, if we get an error saying snap "multipass" not found, then we must install Multipass:

sudo snap install multipass

Then we can create the VM with the following command:

multipass launch --memory 2G --disk 10G --name rock-dev 26.04

Finally, once the VM is up, open a shell into it:

multipass shell rock-dev

Unless stated otherwise, we will work entirely within the VM from now on.


LXD will be required for building the rock. Make sure it is installed and initialised:

sudo snap install lxd
lxd init --auto

There is a known connectivity issue with LXD and Docker. To avoid this issue, enable IPv4 forwarding before installing Docker:

echo "net.ipv4.conf.all.forwarding=1" | sudo tee -a /etc/sysctl.d/99-forwarding.conf
sudo systemctl restart systemd-sysctl

Warning

In a production environment, make sure to use a hardened firewall configuration.

In order to create the rock, we’ll install Rockcraft with classic confinement, which grants it access to the whole file system:

sudo snap install rockcraft --classic

We’ll use Docker to run the rock. We can install it as a snap:

sudo snap install docker

By default, Docker is only accessible with root privileges (sudo). We want to be able to use Docker commands as a regular user:

sudo addgroup --system docker
sudo adduser $USER docker
newgrp docker

Restart Docker:

sudo snap disable docker
sudo snap enable docker

Note that we’ll also need a text editor. We can either install one of our choice or simply use one of the already existing editors in the Ubuntu environment (like vi).

Finally, create an empty project directory:

mkdir django-tutorial
cd django-tutorial

We’ll name the outer workspace directory ~/django-tutorial/ so we can distinguish it from the Django project (django_hello_world) and the rock artifact (django-hello-world). Rockcraft will generate a project file named django-hello-world when we pass the name explicitly during project initialization.

Create the Django app

Start by creating the “Hello, world” Django app that will be used for this tutorial.

Create a requirements.txt file, copy the following text into it and then save it:

~/django-tutorial/requirements.txt
Django>=5.2.2

In order to test the Django app locally (before packing it into a rock), install python3-venv and create a virtual environment:

sudo apt-get update && sudo apt-get install python3-venv -y
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

Create a new project using django-admin:

django-admin startproject django_hello_world

Enter the ~/django-tutorial/django_hello_world/ directory, then run the Django app to verify that it works:

python manage.py runserver

Test the Django app by using curl to send a request to the root endpoint. We’ll need a new shell of the VM for this – in a separate terminal, run multipass shell rock-dev again:

curl --fail localhost:8000

The Django app should respond with The install worked successfully! Congratulations!.

Note

The response from the Django app includes HTML and CSS which makes it difficult to read on a terminal. To see the fully-rendered page in a web browser, set ALLOWED_HOSTS = ['*'] in ~/django-tutorial/django_hello_world/django_hello_world/settings.py. Then return to ~/django-tutorial/django_hello_world/ and run the Django app using:

python manage.py runserver 0.0.0.0:8000

Determine the IP address of your VM. Outside of the VM, run:

multipass info rock-dev | grep IP

Visit http://<Multipass private IP>:8000 in a web browser to see the fully-rendered page, replacing <Multipass private IP> with your VM’s private IP address.

The Django app looks good, so let’s close the terminal instance we used for testing and stop the app in the original terminal instance by pressing Ctrl + C.

Pack the Django app into a rock

Now let’s create a container image for our Django app. We’ll use a rock, which is an OCI-compliant container image based on Ubuntu.

First, we’ll need a rockcraft.yaml project file. We’ll take advantage of a pre-defined extension in Rockcraft with the --profile flag that caters initial rock files for specific web app frameworks. Using the Django profile, Rockcraft automates the creation of rockcraft.yaml and tailors the file for a Django app. Return to the ~/django-tutorial/ directory and initialize the rock:

cd ..
rockcraft init --profile django-framework --name django-hello-world

The project file will automatically be created in the working directory as rockcraft.yaml.

Check out the contents of rockcraft.yaml:

cat rockcraft.yaml

The top of the file should look similar to the following snippet:

~/django-tutorial/rockcraft.yaml
name: django-hello-world
# see https://documentation.ubuntu.com/rockcraft/en/1.6.0/explanation/bases/
# for more information about bases and bare bases
base: bare # as an alternative, an ubuntu base can be used
build-base: ubuntu@24.04 # build-base is required when the base is bare
version: '0.1' # just for humans. Semantic versioning is recommended
summary: A summary of your Django app # 79 char long summary
description: |
    This is django-hello-world's description. You have a paragraph or two to tell the
    most important story about it. Keep it under 100 words though,
    we live in tweetspace and your description wants to look good in the
    container registries out there.
# the platforms this rock should be built on and run on.
# you can check your architecture with `dpkg --print-architecture`
platforms:
    amd64:
    # arm64:
    # ppc64el:
    # s390x:

Verify that the name is django-hello-world.

The platforms key must match the architecture of your host. Check the architecture of your system:

dpkg --print-architecture

Edit the platforms key in rockcraft.yaml if required.

Using a bare base provides the smallest possible foundation for the image, which helps reduce its complexity and size.

Note

For this tutorial, we’ll use the name django-hello-world and assume we’re running on the amd64 platform. Check the architecture of the system using dpkg --print-architecture.

The name, version and platform all influence the name of the generated .rock file.

Pack the rock:

rockcraft pack

Once Rockcraft has finished packing the Django rock, we’ll find a new file in the project’s working directory (an OCI archive) with the .rock extension:

ls *.rock -l --block-size=MB

The created rock is about 75MB in size. We will reduce its size later in this tutorial.

Note

If we changed the name or version in the project file or are not on an amd64 platform, the name of the .rock file will be different.

The size of the rock may vary depending on factors like the architecture we are building on and the packages installed at the time of packing.

Run the Django rock with Docker

We already have the rock as an OCI archive. Now we’ll need to load it into Docker. Docker requires rocks to be imported into the daemon since they can’t be run directly like an executable.

Copy the rock:

rockcraft.skopeo copy \
  --insecure-policy \
  oci-archive:django-hello-world_0.1_$(dpkg --print-architecture).rock \
  docker-daemon:django-hello-world:0.1

This command contains the following pieces:

  • --insecure-policy: adopts a permissive policy that removes the need for a dedicated policy file.

  • oci-archive: specifies the rock we created for our Django app.

  • docker-daemon: specifies the name of the image in the Docker registry.

Check that the image was successfully loaded into Docker:

docker images django-hello-world:0.1

The output should list the Django container image, along with its tag, ID and size:

ubuntu@rock-dev:~/django-hello-world$
sudo docker images django-hello-world:0.1
REPOSITORY          TAG       IMAGE ID       CREATED       SIZE
django-hello-world  0.1       5cd019b51db9   6 days ago   184MB

Note

The size of the image reported by Docker is the uncompressed size which is larger than the size of the compressed .rock file.

Now we’re ready to run the rock and test the containerised Django app:

docker run --rm -d -p 8000:8000 \
  --name django-hello-world django-hello-world:0.1

Use the same curl command as before to send a request to the Django app’s root endpoint which is running inside the container:

curl --fail localhost:8000

The Django app should again respond with The install worked successfully! Congratulations!.

View the app logs

When deploying the Django rock, we can always get the app logs via Pebble:

docker exec django-hello-world pebble logs django

As a result, Pebble will give us the logs for the django service running inside the container. We should expect to see something similar to this:

ubuntu@rock-dev:~/django-hello-world$
sudo docker exec django-hello-world pebble logs django
2024-08-20T06:34:36.114Z [django] [2024-08-20 06:34:36 +0000] [17] [INFO] Starting gunicorn 23.0.0
2024-08-20T06:34:36.115Z [django] [2024-08-20 06:34:36 +0000] [17] [INFO] Listening at: http://0.0.0.0:8000 (17)
2024-08-20T06:34:36.115Z [django] [2024-08-20 06:34:36 +0000] [17] [INFO] Using worker: sync
2024-08-20T06:34:36.116Z [django] [2024-08-20 06:34:36 +0000] [18] [INFO] Booting worker with pid: 18

We can also choose to follow the logs by using the -f option with the pebble logs command above. To stop following the logs, press Ctrl + C.

Cleanup

Now we have a fully functional rock for our Django app! This concludes the first part of this tutorial, so we can stop the container and remove the respective image for now:

docker stop django-hello-world
docker rmi django-hello-world:0.1

Update the Django app

As a final step, let’s update our app. For example, we want to add a new /time/ endpoint which returns the current time in UTC.

cd django_hello_world
django-admin startapp time_app

Open the file time_app/views.py and replace its contents with the following:

~/django-tutorial/django_hello_world/time_app/views.py
import datetime

from django.http import HttpResponse


def index(request):
    return HttpResponse(f"{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")

Create the file time_app/urls.py with the following contents:

~/django-tutorial/django_hello_world/time_app/urls.py
from django.urls import path

from . import views

urlpatterns = [
    path("", views.index, name="index"),
]

Open the file django_hello_world/urls.py and replace its contents with the following:

~/django-tutorial/django_hello_world/django_hello_world/urls.py
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path("time/", include("time_app.urls")),
    path("admin/", admin.site.urls),
]

Since we are creating a new version of the app, go back to the tutorial root directory using cd .. and open the project file and change the version (e.g. to 0.2). The top of the rockcraft.yaml file should look similar to the following:

~/django-tutorial/rockcraft.yaml
name: django-hello-world
# see https://documentation.ubuntu.com/rockcraft/en/1.6.0/explanation/bases/
# for more information about bases and bare bases
base: bare
build-base: ubuntu@24.04
version: '0.2'
summary: A summary of your Django app # 79 char long summary
description: |
    This is django-hello-world's description. You have a paragraph or two to tell the
    most important story about it. Keep it under 100 words though,
    we live in tweetspace and your description wants to look good in the
    container registries out there.
# the platforms this rock should be built on and run on.
# you can check your architecture with `dpkg --print-architecture`
platforms:
    amd64:
    # arm64:
    # ppc64el:
    # s390x:

Note

If we repack the rock without changing the version, the new rock will have the same name and overwrite the last one we built. It’s a good practice to change the version whenever we make changes to the app in the image.

Pack and run the rock using similar commands as before:

rockcraft pack
rockcraft.skopeo --insecure-policy copy \
  oci-archive:django-hello-world_0.2_$(dpkg --print-architecture).rock \
  docker-daemon:django-hello-world:0.2
docker images django-hello-world:0.2
docker run --rm -d -p 8000:8000 \
  --name django-hello-world django-hello-world:0.2

Note

Note that the resulting .rock file will now be named differently, as its new version will be part of the filename.

Finally, use curl to send a request to the /time/ endpoint:

curl --fail localhost:8000/time/

The updated app should respond with the current date and time in UTC (e.g. 2024-08-20 07:28:19).

Note

If you are getting a 404 for the /time/ endpoint, check the Troubleshooting steps below.

Cleanup

We can now stop the container and remove the corresponding image:

docker stop django-hello-world
docker rmi django-hello-world:0.2

Reset the environment

We’ve reached the end of this tutorial.

If we’d like to reset the working environment, we can simply run the following:

# exit and delete the virtual environment
deactivate
rm -rf .venv django_hello_world __pycache__
# delete all the files created during the tutorial
rm django-hello-world_0.1_$(dpkg --print-architecture).rock \
  django-hello-world_0.1-chiselled_$(dpkg --print-architecture).rock \
  django-hello-world_0.2_$(dpkg --print-architecture).rock \
  rockcraft.yaml requirements.txt

We can also clean the Multipass instance up. Start by exiting it:

exit

And then we can proceed with its deletion:

multipass delete rock-dev
multipass purge

Next steps

Congratulations! You’ve reached the end of this tutorial. You created a Django app, packaged it into a rock, and practiced some typical development skills such as viewing logs and updating the app.

But there is a lot more to explore:

If you are wondering…

Visit…

“What’s next?”

Write your first Kubernetes charm for a Django app in Charmcraft

“How do I…?”

Manage a 12-Factor app rock

“How do I get in touch?”

Matrix channel

“What is…?”

django-framework extension

What is a Rock?

“Why…?”, “So what?”

12-Factor app principles and support in Charmcraft and Rockcraft


Troubleshooting

App updates not taking effect?

Upon changing your Django app and re-packing the rock, if you believe your changes are not taking effect (e.g. the /time/ endpoint is returning a 404), try running rockcraft clean and pack the rock again with rockcraft pack.