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.
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
See Multipass installation instructions, switch to Windows in the drop down.
See Multipass installation instructions, switch to macOS in the drop down.
Then we can create the VM with the following command:
multipass launch --disk 10G --name rock-dev 24.04
Finally, once the VM is up, open a shell into it:
multipass shell rock-dev
LXD will be required for building the rock. Make sure it is installed and initialised:
sudo snap install lxd
lxd init --auto
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-hello-world
cd django-hello-world
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>=5.2.1
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
Change into the django_hello_world
directory and run the Django app
using python manage.py runserver
to verify that it works.
Test the Django app by using curl
to send a request to the root
endpoint. We’ll need a new terminal for this – run
multipass shell rock-dev
to get another terminal:
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. Visit http://localhost:8000
using a
browser to see the fully rendered page.
The Django app looks good, so let’s stop it for now 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. Change
back into the ~/django-hello-world
directory and initialize the rock:
cd ..
rockcraft init --profile django-framework
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:
name: django-hello-world
# see https://documentation.ubuntu.com/rockcraft/en/1.6.0/explanation/bases/
# for more information about bases and using 'bare' bases for chiselled rocks
base: [email protected] # the base environment for this Django app
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.
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
Warning
There is a known connectivity issue with LXD and Docker. If we see a
networking issue such as “A network related operation failed in a context
of no network access” or Client.Timeout
, allow egress network traffic
to flow from the LXD managed bridge using:
iptables -I DOCKER-USER -i <network_bridge> -j ACCEPT
ip6tables -I DOCKER-USER -i <network_bridge> -j ACCEPT
iptables -I DOCKER-USER -o <network_bridge> -m conntrack \
--ctstate RELATED,ESTABLISHED -j ACCEPT
ip6tables -I DOCKER-USER -o <network_bridge> -m conntrack \
--ctstate RELATED,ESTABLISHED -j ACCEPT
Run lxc network list
to show the existing LXD managed bridges.
Depending on the network, this step can take a couple of minutes to finish.
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:
sudo 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:
sudo docker images django-hello-world:0.1
The output should list the Django container image, along with its tag, ID and size:
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:
sudo 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:
sudo 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:
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:
sudo docker stop django-hello-world
sudo docker rmi django-hello-world:0.1
Chisel the rock¶
This is an optional but recommended step, especially if we’re looking to deploy the rock into a production environment. With Chisel we can produce lean and production-ready rocks by getting rid of all the contents that are not needed for the Django app to run. This results in a much smaller rock with a reduced attack surface.
Note
It is recommended to run chiselled images in production. For development, we may prefer non-chiselled images as they will include additional development tooling (such as for debugging).
The first step towards chiselling the rock is to ensure we are using a
bare
base.
In the project file, change the base
to bare
and add
build-base: ubuntu@22.04
:
sed -i \
"s/base: .*/base: bare\nbuild-base: [email protected]/g" \
rockcraft.yaml
So that we can compare the size after chiselling, open the project
file and change the version
(e.g. to 0.1-chiselled
). The top of
the rockcraft.yaml
file should look similar to the following:
name: django-hello-world
# see https://documentation.ubuntu.com/rockcraft/en/1.6.0/explanation/bases/
# for more information about bases and using 'bare' bases for chiselled rocks
base: bare
build-base: [email protected]
version: '0.1-chiselled'
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:
Pack the rock with the new bare
base:
rockcraft pack
As before, verify that the new rock was created:
ls *.rock -l --block-size=MB
We’ll verify that the new Django rock is now approximately 15% smaller
in size! And that’s just because of the simple change of base
.
And the functionality is still the same. As before, we can confirm this by running the rock with Docker:
sudo rockcraft.skopeo --insecure-policy \
copy oci-archive:django-hello-world_0.1-chiselled_$(dpkg --print-architecture).rock \
docker-daemon:django-hello-world:0.1-chiselled
sudo docker images django-hello-world:0.1-chiselled
sudo docker run --rm -d -p 8000:8000 \
--name django-hello-world django-hello-world:0.1-chiselled
and then using the same curl
request:
curl --fail localhost:8000
The Django app should still respond with
The install worked successfully! Congratulations!
.
Cleanup¶
And that’s it. We can now stop the container and remove the corresponding image:
sudo docker stop django-hello-world
sudo docker rmi django-hello-world:0.1-chiselled
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.
cd django_hello_world
django-admin startapp time_app
Open the file time_app/views.py
and replace its contents with the following:
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:
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:
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:
name: django-hello-world
# see https://documentation.ubuntu.com/rockcraft/en/1.6.0/explanation/bases/
# for more information about bases and using 'bare' bases for chiselled rocks
base: bare
build-base: [email protected]
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
rockcraft pack
will create a new image with the updated code even if we
don’t change the version. It is recommended 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
sudo rockcraft.skopeo --insecure-policy copy \
oci-archive:django-hello-world_0.2_$(dpkg --print-architecture).rock \
docker-daemon:django-hello-world:0.2
sudo docker images django-hello-world:0.2
sudo 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 (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:
sudo docker stop django-hello-world
sudo 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
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
.