Build a rock for a Flask application

In this tutorial, we’ll create a simple Flask application and learn how to containerise it in a rock, using Rockcraft’s flask-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

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 need to install Rockcraft:

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
sudo snap disable docker
sudo snap enable docker

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”, make sure to apply one of the suggested fixes here.

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).

Note

This tutorial requires version 1.5.2 or later for Rockcraft. Check the version using rockcraft --version. If there’s an older version of Rockcraft installed, use sudo snap refresh rockcraft --channel latest/stable to get the latest stable version.

Finally, create a new directory for this tutorial and go inside it:

mkdir flask-hello-world
cd flask-hello-world

Create the Flask application

Start by creating the “Hello, world” Flask application that we’ll use for this tutorial.

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

Flask

In order to test the Flask application 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

In the same directory, copy and save the following into a text file called app.py:

import flask

app = flask.Flask(__name__)


@app.route("/")
def index():
    return "Hello, world!\n"


if __name__ == "__main__":
    app.run()

Run the Flask application using flask run -p 8000 to verify that it works.

Test the Flask application by using curl to send a request to the root endpoint. We’ll need a new terminal for this – if we’re using Multipass, run multipass shell rock-dev to get another terminal:

curl localhost:8000

The Flask application should respond with Hello, world!.

The Flask application looks good, so let’s stop it for now by pressing Ctrl + C.

Pack the Flask application into a rock

First, we’ll need a rockcraft.yaml file. Rockcraft will automate its creation and tailoring for a Flask application by using the flask-framework profile:

rockcraft init --profile flask-framework

The rockcraft.yaml file will automatically be created in the project’s working directory. Open it in a text editor and check that the name is flask-hello-world. Ensure that platforms includes the host architecture. For example, if the host uses the ARM architecture, include arm64 in platforms.

Note

For this tutorial, we’ll use the name flask-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

Note

Depending on the network, this step can take a couple of minutes to finish.

Once Rockcraft has finished packing the Flask 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 65MB in size. We will reduce its size later in this tutorial.

Note

If we changed the name or version in rockcraft.yaml 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’re building on and the packages installed at the time of packing.

Run the Flask rock with Docker

We already have the rock as an OCI archive. Now we need to load it into Docker:

sudo rockcraft.skopeo --insecure-policy \
  copy oci-archive:flask-hello-world_0.1_amd64.rock \
  docker-daemon:flask-hello-world:0.1

Check that the image was successfully loaded into Docker:

sudo docker images flask-hello-world:0.1

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

REPOSITORY          TAG       IMAGE ID       CREATED       SIZE
flask-hello-world   0.1       c256056698ba   2 weeks ago   149MB

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 finally ready to run the rock and test the containerised Flask application:

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

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

curl localhost:8000

The Flask application should again respond with Hello, world!.

View the application logs

When deploying the Flask rock, we can always get the application logs via pebble:

sudo docker exec flask-hello-world pebble logs flask

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

2024-06-21T03:41:45.077Z [flask] [2024-06-21 03:41:45 +0000] [17] [INFO] Starting gunicorn 22.0.0
2024-06-21T03:41:45.077Z [flask] [2024-06-21 03:41:45 +0000] [17] [INFO] Listening at: http://0.0.0.0:8000 (17)
2024-06-21T03:41:45.077Z [flask] [2024-06-21 03:41:45 +0000] [17] [INFO] Using worker: sync
2024-06-21T03:41:45.078Z [flask] [2024-06-21 03:41:45 +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 a Flask application! This concludes the first part of this tutorial, so we’ll stop the container and remove the respective image for now:

sudo docker stop flask-hello-world
sudo docker rmi flask-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 Flask application 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 rockcraft.yaml, 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

Note

The sed command replaces the current base in rockcraft.yaml with the bare base. The command also adds a build-base which is required when using the bare base.

So that we can compare the size after chiselling, open the rockcraft.yaml file and change the version (e.g. to 0.1-chiselled). 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 Flask rock is now approximately 30% 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:flask-hello-world_0.1-chiselled_amd64.rock \
  docker-daemon:flask-hello-world:0.1-chiselled
sudo docker images flask-hello-world:0.1-chiselled
sudo docker run --rm -d -p 8000:8000 \
  --name flask-hello-world flask-hello-world:0.1-chiselled

and then using the same curl request:

curl localhost:8000

Unsurprisingly, the Flask application should still respond with Hello, world!.

Cleanup

And that’s it. We can now stop the container and remove the corresponding image:

sudo docker stop flask-hello-world
sudo docker rmi flask-hello-world:0.1-chiselled

Update the Flask application

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

Start by opening the app.py file in a text editor and update the code to look like the following:

import datetime

import flask

app = flask.Flask(__name__)


@app.route("/")
def index():
    return "Hello, world!\n"


@app.route("/time")
def time():
    return f"{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"


if __name__ == "__main__":
    app.run()

Since we are creating a new version of the application, open the rockcraft.yaml file and change the version (e.g. to 0.2).

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 application in the image.

Pack and run the rock using similar commands as before:

rockcraft pack
sudo rockcraft.skopeo --insecure-policy \
  copy oci-archive:flask-hello-world_0.2_amd64.rock \
  docker-daemon:flask-hello-world:0.2
sudo docker images flask-hello-world:0.2
sudo docker run --rm -d -p 8000:8000 \
  --name flask-hello-world flask-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 localhost:8000/time

The updated application should respond with the current date and time (e.g. 2024-06-21 09:47:56).

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 flask-hello-world
sudo docker rmi flask-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 __pycache__
# delete all the files created during the tutorial
rm flask-hello-world_0.1_amd64.rock \
  flask-hello-world_0.1-chiselled_amd64.rock \
  flask-hello-world_0.2_amd64.rock \
  rockcraft.yaml app.py requirements.txt
If using Multipass...

If we created an instance using Multipass, we can also clean it up. Start by exiting it:

exit

And then we can proceed with its deletion:

multipass delete rock-dev
multipass purge

Troubleshooting

Application updates not taking effect?

Upon changing the Flask application 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.