Write your first Kubernetes charm for a Django app on Ubuntu 26.04 LTS

Imagine you have a Django app backed up by a database such as PostgreSQL and need to deploy it. In a traditional setup, this can be quite a challenge, but with Charmcraft you’ll find yourself packaging and deploying your Django app in no time.

Introduction

In this tutorial we will build a Kubernetes charm for a Django app using Charmcraft, so we can have a Django app up and running with Juju. Let’s get started!

This tutorial should take 90 minutes for you to complete.

If you’re new to the charming world, Django apps are specifically supported with a template to quickly generate a rock and a matching template to generate a charm. A rock is a special kind of OCI-compliant container image, while a charm is a software operator for cloud operations that use the Juju orchestration engine. The combined result is a Django app that can be deployed, configured, scaled, integrated, and so on, on any Kubernetes cluster.

What you’ll need

  • A local system, e.g., a laptop, with AMD64 or ARM64 architecture which has sufficient resources to launch a virtual machine with 4 CPUs, 8 GB RAM, and a 60 GB disk.

  • Familiarity with Linux.

The RAM and disk space are necessary to set up all the required software and to facilitate the creation of the rock and charm. If your local system has less than the sufficient resources, the tutorial will take longer to complete.

What you’ll do

  1. Create a Django app.

  2. Use that to create a rock with Rockcraft.

  3. Use that to create a charm with Charmcraft.

  4. Use that to test, deploy, configure, etc., your Django app on a local Kubernetes cloud with Juju.

  5. Repeat the process, mimicking a real development process.

Important

Should you get stuck or notice issues, please get in touch on Matrix or Discourse

Set things up

First, install Multipass. Launch an Ubuntu 26.04 VM named charm-dev:

multipass launch --cpus 4 --disk 60G --memory 8G --name charm-dev 26.04
multipass shell charm-dev

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

Install Rockcraft, Charmcraft, and uv

For the Ubuntu 26.04 extensions, use the candidate release of Rockcraft and the edge release of Charmcraft. Install these tools and uv, the charm’s Python dependency manager, with classic confinement, which grants them access to the whole file system:

sudo snap install rockcraft --channel latest/candidate --classic
sudo snap install charmcraft --channel latest/edge --classic
sudo snap install astral-uv --classic

Install LXD, Canonical Kubernetes, and Juju

LXD provides the build environment for the rock and charm. Make sure it is installed:

lxd --version

If LXD is not installed, install it with sudo snap install lxd.

Initialize LXD:

lxd init --auto

Canonical Kubernetes will host the Django app and PostgreSQL. Install the Kubernetes and Juju snaps:

sudo snap install k8s --channel 1.34-classic/stable --classic
sudo snap install juju --channel 3.6/stable

Bootstrap a single-node cluster and enable local storage and Gateway API:

sudo k8s bootstrap --timeout 10m
sudo k8s status --wait-ready --timeout 10m
sudo k8s enable local-storage
sudo k8s enable gateway

The --wait-ready option waits for the cluster to become ready. Local storage supplies the volumes needed by Juju, PostgreSQL, and the image registry. Gateway API supplies the ingress controller used later in the tutorial.

Configure the load balancer to use the VM’s address, so the app will be accessible from outside the VM. Enable the load balancer:

sudo k8s enable load-balancer
sudo k8s kubectl wait --for=condition=Accepted gatewayclass/ck-gateway --timeout=5m

Save the node’s internal address in an environment variable and use it to set the address range for the load balancer:

export VM_IP=$(sudo k8s kubectl get nodes \
  -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}')
sudo k8s set load-balancer.cidrs="${VM_IP}/32"
printf 'VM address: %s\n' "$VM_IP"

Keep the printed VM address for any browser checks on your host machine. Later, the gateway action will return the full deployed app URL for browser and curl checks. The VM_IP variable is local to this VM terminal; it is not available in terminals on your host machine.

Create a local image registry

Canonical Kubernetes doesn’t include a registry addon. We’ll deploy an OCI registry in the cluster to store the rocks built during this tutorial. Download the registry manifest from the paas-charm repository:

curl --fail --location --output registry.yaml \
  https://raw.githubusercontent.com/canonical/paas-charm/main/docs/reuse/tutorial/registry_26_04.yaml

Apply the downloaded registry.yaml and wait for the registry to be ready:

sudo k8s kubectl apply -f registry.yaml
sudo k8s kubectl rollout status deployment/registry \
  -n charm-tutorial-registry --timeout=5m
curl --fail --retry 30 --retry-connrefused --retry-delay 2 "http://localhost:32000/v2/"

You can access the registry at localhost:32000 from within the VM. It uses a persistent volume so its images survive pod restarts. The NodePort service also exposes port 32000 on the VM’s network interfaces.

Bootstrap Juju

Register Canonical Kubernetes as a Juju cloud, then bootstrap the development controller:

mkdir -p ~/.local/share
sudo k8s config | juju add-k8s k8s --client
juju bootstrap k8s dev-controller --config bootstrap-timeout=600

It can take a few minutes to download the controller images.

The Ubuntu 26.04 extensions are experimental. Enable them for both tools in the terminal where you’ll build the rock and charm:

export ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=1
export CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=1

Let’s create a new directory for this tutorial and enter into it:

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 charm/rock artifacts (django-hello-world). Rockcraft and Charmcraft will generate projects named django-hello-world when we pass the name explicitly during the project initialization.

Finally, install python3-venv and create a virtual environment:

sudo apt update && sudo apt install python3-venv -y
python3 -m venv .venv
source .venv/bin/activate

Create the Django app

Let’s start by creating the “Hello, world” Django app that will be used for this tutorial.

Create a new requirements file with nano requirements.txt. Then, copy the following text into it, and save:

~/django-tutorial/requirements.txt
Django==6.1.2
psycopg2-binary==2.9.13

Note

The psycopg2-binary package is needed so the Django app can connect to PostgreSQL.

Install the packages:

pip install -r requirements.txt

Create a new project using django-admin:

django-admin startproject django_hello_world

Run the Django app locally

We will test the Django app by visiting the app in a web browser.

Change into the ~/django-tutorial/django_hello_world directory. Open the settings file of the app located at ./django_hello_world/settings.py. Update the ALLOWED_HOSTS setting to allow all traffic:

~/django-tutorial/django_hello_world/django_hello_world/settings.py
ALLOWED_HOSTS = ['*']

Save and close the settings.py file.

Replace django_hello_world/urls.py with the following code to serve the initial greeting at the root URL:

~/django-tutorial/django_hello_world/django_hello_world/urls.py
"""URL configuration for the initial greeting."""

from django.contrib import admin
from django.http import HttpRequest, HttpResponse
from django.urls import path


def index(request: HttpRequest) -> HttpResponse:
    """Return the initial greeting.

    Args:
        request: The incoming request.

    Returns:
        The greeting response.
    """
    return HttpResponse("Hello world!", content_type="text/plain")


urlpatterns = [
    path("", index),
    path("admin/", admin.site.urls),
]

Now, run the Django app to verify that it works:

./manage.py runserver 0.0.0.0:8000

Note

Specifying 0.0.0.0:8000 allows for traffic outside of the Multipass VM.

When you run the command for the first time, you’ll see a warning about migrations. We can ignore this warning for now, as we aren’t currently performing any database operations. We’ll set up the database later.

Verify the app

On your host machine, open a browser tab and visit http://<VM address>:8000, replacing <VM address> with the address printed during setup. This is the same address saved in VM_IP inside the VM; that environment variable is not set on your host machine.

The Django app should respond in the browser with Hello world!.

Close the app

The Django app looks good, so we can stop it for now from the original terminal of the Multipass VM using 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-framework profile, Rockcraft automates the creation of rockcraft.yaml and tailors the file for a Django app. Change back into the ~/django-tutorial directory and initialize the rock:

cd ..
rockcraft init --name django-hello-world --profile django-framework --base ubuntu@26.04

The rockcraft.yaml file will automatically be created using the name provided via --name, keeping the generated metadata aligned with the django-hello-world charm even though the workspace folder is called django-tutorial.

The Ubuntu 26.04 profile uses a bare runtime base and Ubuntu 26.04 as its build environment. Keep these generated settings.

Let’s verify that the project file is compatible with your host machine. Check the architecture of your system:

dpkg --print-architecture

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
# See https://documentation.ubuntu.com/rockcraft/1/reference/extensions/django-framework
# For questions or help, visit https://matrix.to/#/#12-factor-charms:ubuntu.com

name: django-hello-world
# see https://documentation.ubuntu.com/rockcraft/1/explanation/bases/
# for more information about bases and using 'bare' bases for chiselled rocks
base: bare # as an alternative, an ubuntu base can be used
build-base: ubuntu@26.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 application # 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. Edit the platforms key in rockcraft.yaml if required.

Django apps require a database. Django will use a SQLite database by default. This won’t work on Kubernetes because the database would disappear every time the pod is restarted – e.g., to perform an upgrade – and this database wouldn’t be shared by all containers as the app is scaled. We’ll use Juju later to deploy a database.

We’ll need to update the settings.py file to prepare for integrating the app with a database. Open the file and update the imports to include json, os and secrets. The top of the settings.py file should look similar to the following snippet:

~/django-tutorial/django_hello_world/django_hello_world/settings.py
"""
Django settings for django_hello_world project.

Generated by 'django-admin startproject' using Django 6.1.2.

For more information on this file, see
https://docs.djangoproject.com/en/6.1/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/6.1/ref/settings/
"""

from pathlib import Path

import json
import os
import secrets

We need to change some settings to be production ready. Near the top of the settings.py file, change the SECRET_KEY, DEBUG and ALLOWED_HOSTS variables to:

~/django-tutorial/django_hello_world/django_hello_world/settings.py
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', secrets.token_hex(32))

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.environ.get('DJANGO_DEBUG', 'false') == 'true'

ALLOWED_HOSTS = json.loads(os.environ.get('DJANGO_ALLOWED_HOSTS', '[]'))

We will also use PostgreSQL as the database for our Django app. In settings.py, go further down to the Database section and change the DATABASES variable to:

~/django-tutorial/django_hello_world/django_hello_world/settings.py
DATABASES = {
   'default': {
           'ENGINE': 'django.db.backends.postgresql',
           'NAME': os.environ.get('POSTGRESQL_DB_NAME'),
           'USER': os.environ.get('POSTGRESQL_DB_USERNAME'),
           'PASSWORD': os.environ.get('POSTGRESQL_DB_PASSWORD'),
           'HOST': os.environ.get('POSTGRESQL_DB_HOSTNAME'),
           'PORT': os.environ.get('POSTGRESQL_DB_PORT'),
    }
}

Save and close the settings.py file. The app will no longer run locally due to these changes, and we can’t test the app until we’ve deployed it and connected it to the PostgreSQL database. The charm runs python3 manage.py migrate before starting the app to apply its database migrations.

Now let’s pack the rock:

rockcraft pack

Depending on your system and network, this step can take several minutes to finish.

Once Rockcraft has finished packing the Django rock, the terminal will respond with something similar to Packed django-hello-world_0.1_<architecture>.rock. The file name reflects your system’s architecture. After the initial pack, subsequent rock packings are faster.

The rock needs to be copied to the local OCI registry. This registry acts as a temporary Docker Hub, storing OCI archives so they can be downloaded and deployed in the Kubernetes cluster. Copy the rock:

rockcraft.skopeo copy \
  --insecure-policy \
  --dest-tls-verify=false \
  oci-archive:django-hello-world_0.1_$(dpkg --print-architecture).rock \
  docker://localhost:32000/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.

  • --dest-tls-verify=false: disables the need for HTTPS and verify certificates while interacting with the local OCI registry.

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

  • docker: specifies the name of the image in the local OCI registry.

Create the charm

From the ~/django-tutorial directory, create a new directory for the charm and change inside it:

mkdir charm
cd charm

Similar to the rock, we’ll take advantage of a pre-defined extension in Charmcraft with the --profile flag that caters initial charm files for specific web app frameworks. Using the django-framework profile, Charmcraft automates the creation of the files needed for our charm, including a charmcraft.yaml project file, pyproject.toml and source code for the charm. The source code contains the logic required to operate the Django app.

Initialize a charm named django-hello-world:

charmcraft init --profile django-framework --name django-hello-world --base ubuntu@26.04

The files will automatically be created in your working directory.

The Ubuntu 26.04 profile uses the uv plugin and pyproject.toml for the charm dependencies. These dependencies are separate from those of your web app. Use uv, installed during setup, to create the lockfile before packing:

uv lock

The charm needs to know which port the app listens on. paas-config.yaml contains settings that the charm uses to run your app. Charmcraft packages this file with the charm, and the charm reads it when configuring the workload. Unlike options set with juju config, these settings are part of the packaged charm; changing them requires repacking and refreshing it.

Create paas-config.yaml beside charmcraft.yaml with the following content to set the app’s runtime port:

~/django-tutorial/charm/paas-config.yaml
port: 8000

The port key replaces the Juju app-port option.

We will need to integrate our Django app to the PostgreSQL database, which means we must declare a requirement in the charm project file. Edit the project file by adding the following section to the end:

~/django-tutorial/charm/charmcraft.yaml
requires:
  postgresql:
    interface: postgresql_client
    optional: false

Tip

Want to learn more about all the configurations in the django-framework profile? Run charmcraft expand-extensions from the ~/django-tutorial/charm/ directory.

Now let’s pack the charm:

charmcraft pack

Depending on your system and network, this step can take several minutes to finish.

Once Charmcraft has finished packing the charm, the terminal will respond with something similar to Packed django-hello-world_<architecture>.charm. The file name reflects your system’s architecture. After the initial pack, subsequent charm packings are faster.

Deploy the Django app

So far, we’ve packed our Django app into a rock and used that rock to create our corresponding charm. Now we have all the materials necessary to deploy the Django app with Juju.

A Juju model is needed to handle Kubernetes resources while deploying the Django app. The Juju model holds the app along with any supporting components. In this tutorial, our model will hold the Django app, the PostgreSQL database, and ingress.

Let’s create a new model:

juju add-model django-hello-world

Constrain the Juju model to your architecture:

juju set-model-constraints -m django-hello-world \
  arch=$(dpkg --print-architecture)

Now let’s use the OCI image we previously uploaded to deploy the Django app. Deploy using Juju by specifying the OCI image name with the --resource option:

juju deploy \
  ./django-hello-world_$(dpkg --print-architecture).charm \
  django-hello-world --resource \
  app-image=localhost:32000/django-hello-world:0.1

Now let’s deploy PostgreSQL 16:

juju deploy postgresql-k8s --trust --channel 16/stable

Integrate PostgreSQL with the Django app:

juju integrate django-hello-world postgresql-k8s

It will take a few minutes to deploy the Django app. You can run juju status to monitor its progress:

juju status --relations --watch 2s

The --relations flag will list the currently enabled integrations. It can take a couple of minutes for the apps to finish the deployment. During this time, the Django app may enter a blocked state as it waits to become integrated with the PostgreSQL database. Due to the optional: false key in the endpoint definition, the Django app will not start until the database is ready.

Once the status of the App has gone to active, you can stop watching using Ctrl + C. To monitor your deployment, keep a juju status session active in a second terminal.

The Django app should now be running. We can see the status of the deployment using juju status which should be similar to the following output:

ubuntu@charm-dev:~/django-tutorial/charm$
juju status
Model               Controller      Cloud/Region        Version  SLA          Timestamp
django-hello-world  dev-controller  k8s  3.6.2    unsupported  16:47:01+10:00

App                 Version  Status  Scale  Charm               Channel    Rev  Address         Exposed  Message
django-hello-world           active      1  django-hello-world               3  10.152.183.126  no
postgresql-k8s      16       active      1  postgresql-k8s      16/stable  957  10.152.183.197  no

Unit                   Workload  Agent  Address      Ports  Message
django-hello-world/0*  active    idle   10.1.157.80
postgresql-k8s/0*      active    idle   10.1.157.78         Primary

Enable debug mode temporarily to help diagnose the development deployment. Set the configuration:

juju config django-hello-world django-debug=true

Note

The django-debug configuration key sets the DJANGO_DEBUG environment variable that we previously updated in settings.py.

Turning on debug mode shouldn’t be done in production. We will do this in the tutorial for now and later disable debug mode.

Let’s expose the app using ingress so that we can access it. Deploy the gateway-api-integrator charm and integrate it with the Django app:

juju deploy gateway-api-integrator --channel=1/edge --trust
juju integrate gateway-api-integrator django-hello-world

Configure the Gateway API integrator to use Canonical Kubernetes’ ck-gateway class and serve HTTP on its load-balancer IP address:

juju config gateway-api-integrator \
  gateway-class=ck-gateway enforce-https=false

We’ll leave external-hostname unset, and we won’t add a certificates relation for this isolated development VM. Use a domain and TLS in production

Monitor juju status until everything has a status of active. In the VM terminal, ask the gateway for the published endpoints:

juju run gateway-api-integrator/leader get-proxied-endpoints

The proxied-endpoints result contains a JSON object, for example:

proxied-endpoints: '{"gateway-api-integrator": {"url": "http://10.115.29.100"}, "django-hello-world": {"url": "http://10.115.29.100/django-hello-world-django-hello-world"}}'

Copy the complete url for your application, not the gateway-api-integrator entry. The example address will differ on your VM. The app URL already includes its routing prefix; the gateway strips this prefix before forwarding requests to the app.

In the VM terminal, save that URL for the following commands, replacing <app-url> with the value returned by the action:

export APP_URL='<app-url>'

Use the same complete URL directly in a browser on your host machine. No DNS entry or custom Host header is needed. APP_URL exists only in the VM terminal where you exported it; repeat the export with the same returned URL if you open another VM terminal. If the gateway address changes, run the action again and update APP_URL.

Open the returned Django app URL in a browser on your host machine, or run curl "$APP_URL" in the VM terminal. The Django app should respond with Hello world!.

The development cycle

So far, we have worked though the entire cycle, from creating an app to deploying it. But now – as in every real-world case – we will go through the experience of iterating to develop the app, and deploy each iteration.

Add a “Hello, world” app

In this iteration, we’ll add a greeting app that returns a Hello, world! greeting.

The initial greeting is defined in the project’s URL configuration. We’ll move it into a separate Django app and update its greeting. We will need to go back out to the ~/django-tutorial directory where the rock is and enter into the ./django_hello_world directory where the Django app is. Let’s add a new Django app:

django-admin startapp greeting

Open the greeting/views.py file and replace the content with:

~/django-tutorial/django_hello_world/greeting/views.py
from django.http import HttpResponse


def index(request):
    return HttpResponse("Hello, world!", content_type="text/plain")

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

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

from . import views

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

Open the django_hello_world/urls.py file and edit the imports for django.urls and the value of urlpatterns like in the following example:

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

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

Update the rock

Since we’re changing the app we should update the version of the rock. Go back to the ~/django-tutorial directory where the rock is and change the version in rockcraft.yaml to 0.2. The top of the rockcraft.yaml file should look similar to the following:

~/django-tutorial/rockcraft.yaml
# See https://documentation.ubuntu.com/rockcraft/1/reference/extensions/django-framework
# For questions or help, visit https://matrix.to/#/#12-factor-charms:ubuntu.com

name: django-hello-world
# see https://documentation.ubuntu.com/rockcraft/1/explanation/bases/
# for more information about bases and using 'bare' bases for chiselled rocks
base: bare # as an alternative, an ubuntu base can be used
build-base: ubuntu@26.04 # build-base is required when the base is bare
version: "0.2" # just for humans. Semantic versioning is recommended
summary: A summary of your Django application # 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:

Now let’s pack and upload the new version of the rock using similar commands as before:

rockcraft pack
rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \
  oci-archive:django-hello-world_0.2_$(dpkg --print-architecture).rock \
  docker://localhost:32000/django-hello-world:0.2

Redeploy the charm

We’ll redeploy the new version with juju refresh.

From the Django project root, ~/django-tutorial, run:

cd charm
juju refresh django-hello-world \
    --path=./django-hello-world_$(dpkg --print-architecture).charm \
    --resource app-image=localhost:32000/django-hello-world:0.2

Now that we’ve checked the greeting app, disable debug mode:

juju config django-hello-world django-debug=false

Use juju status --watch 2s again to wait until the App is active again. Open the same app URL in a web browser, or inside the Multipass VM run:

curl "$APP_URL"

Either way, the Django app should respond with Hello, world!.

Provide a configuration

To demonstrate how to provide a configuration to the Django app, we will make the greeting configurable. The app will read this configuration from the GREETING_VAR environment variable. Return to the rock directory ~/django-tutorial. From there, open the ./django_hello_world/greeting/views.py file and replace the content with:

~/django-tutorial/django_hello_world/greeting/views.py
import os

from django.http import HttpResponse


def index(request):
    return HttpResponse(os.environ.get("GREETING_VAR", "Hello, world!"), content_type="text/plain")

Update the rock again

Increment the version in rockcraft.yaml to 0.3 such that the top of the rockcraft.yaml file looks similar to the following:

~/django-tutorial/rockcraft.yaml
# See https://documentation.ubuntu.com/rockcraft/1/reference/extensions/django-framework
# For questions or help, visit https://matrix.to/#/#12-factor-charms:ubuntu.com

name: django-hello-world
# see https://documentation.ubuntu.com/rockcraft/1/explanation/bases/
# for more information about bases and using 'bare' bases for chiselled rocks
base: bare # as an alternative, an ubuntu base can be used
build-base: ubuntu@26.04 # build-base is required when the base is bare
version: "0.3" # just for humans. Semantic versioning is recommended
summary: A summary of your Django application # 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:

Let’s pack and upload the new version of the rock:

rockcraft pack
rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false \
  oci-archive:django-hello-world_0.3_$(dpkg --print-architecture).rock \
  docker://localhost:32000/django-hello-world:0.3

Update the charm

Change back into the charm directory.

The django-framework Charmcraft extension supports adding configurations in charmcraft.yaml which will be passed as environment variables to the Django app. Add the following to the end of the charmcraft.yaml file:

~/django-tutorial/charm/charmcraft.yaml
config:
  options:
    greeting:
      description: |
        The greeting to be returned by the Django application.
      default: "Hello, world!"
      type: string

Replace paas-config.yaml with the following content to map the new greeting option to GREETING_VAR, matching the app code:

~/django-tutorial/charm/paas-config.yaml
port: 8000
config:
  options:
    greeting:
      env-var: GREETING_VAR

The option is declared in charmcraft.yaml. By default, greeting would be exposed as DJANGO_GREETING. Setting env-var overrides that name with GREETING_VAR, without adding a framework prefix or changing the option’s value. Changing the packaged mapping requires repacking the charm; changing the greeting value through Juju does not require rebuilding the rock. The paas-config.yaml reference describes the other packaged settings.

We can now pack a new version of the charm, and then deploy it once more with juju refresh:

charmcraft pack
juju refresh django-hello-world \
  --path=./django-hello-world_$(dpkg --print-architecture).charm \
  --resource app-image=localhost:32000/django-hello-world:0.3

Monitor juju status until the app goes back to active again. Open the same app URL in a web browser or send a request to the root endpoint using:

curl "$APP_URL"

Either action should result in the Django app responding with Hello, world! again.

Now let’s change the greeting:

juju config django-hello-world greeting='Hi!'

Wait for the app to restart, and then open the same app URL or use:

curl "$APP_URL"

The web app should now respond with Hi!.

Tear things down

If you’d like to quickly tear things down, start by exiting the Multipass VM:

exit

And then you can proceed with its deletion:

multipass delete charm-dev
multipass purge

If you’d like to manually reset your working environment, you can run the following in the project directory ~/django-tutorial/charm for the tutorial:

charmcraft clean
# Back out to main directory for cleanup
cd ..
rockcraft clean
# exit and delete the virtual environment
deactivate
rm -rf .venv
# delete all the files created during the tutorial
rm -rf charm __pycache__ django_hello_world
rm django-hello-world_0.1_$(dpkg --print-architecture).rock \
  django-hello-world_0.2_$(dpkg --print-architecture).rock \
  django-hello-world_0.3_$(dpkg --print-architecture).rock \
  rockcraft.yaml requirements.txt
# Remove the juju model
juju destroy-model django-hello-world --destroy-storage --no-prompt --force

You can also clean up your Multipass instance by exiting and deleting it using the same commands as above.

Conclusion and next steps

You’ve reached the end of this tutorial. You made it through the entire development process, including:

  • Creating a Django app

  • Deploying the app locally

  • Packaging the app using Rockcraft

  • Building the app with Ops code using Charmcraft

  • Deploying the app using Juju

  • Integrating the app with PostgreSQL to be production ready

  • Exposing the app using an ingress

  • Adding an initial app and configuring the app

Please provide feedback about your experience with this tutorial by completing the following Google form: 26.04 tutorial feedback

By the end of this tutorial you built a charm and evolved it in a number of typical ways. But there is a lot more to explore: