Build a rock for a Flask application

In this tutorial you will 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 22.04 installation. If you don’t have one available, you can create one using Multipass:

How to create an Ubuntu 22.04 VM with Multipass

Is Multipass already installed and active? Check by running

sudo snap services multipass

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

sudo snap install multipass

Then you can create the VM with the following command:

multipass launch --disk 10G --name rock-dev 22.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, you’ll need to install Rockcraft:

sudo snap install rockcraft --classic

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

sudo snap install docker

Warning

There is a known connectivity issue with LXD and Docker. If you see a networking issue such as “A network related operation failed in a context of no network access”, make sure you apply one of the fixes suggested here.

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

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 will be used 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 to verify that it works:

flask run -p 8000 &

Note

The & at the end of the command runs the Flask application in the background. You can continue to use your terminal as normal and will see all the output from the Flask application in your terminal. To stop the Flask application, you can use the kill command shown below.

Test the Flask application by using curl to send a request to the root endpoint:

curl localhost:8000

The Flask application should respond with Hello, world!.

The Flask application looks good, so you can stop for now:

kill $!

Pack the Flask application into a rock

First, you’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 your working directory. Open it in a text editor and customise the name, summary and description. Ensure that platforms includes the architecture of your host. For example, if your host uses the ARM architecture, include arm64 in platforms.

Note

For this tutorial, we’ll use the name “flask-hello-world” and build the rock on an amd64 platform.

Pack the rock:

rockcraft pack

Note

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

Once Rockcraft has finished packing the Flask rock, you’ll find a new file in your 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 you changed the name or version in rockcraft.yaml or are not on an amd64 platform, the name of the .rock file will be different for you.

Run the Flask rock with Docker

You already have the rock as an OCI archive. Now you’ll need to import it into a format that Docker recognises:

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 imported into Docker:

sudo docker images flask-hello-world:0.1

The output should list your 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 you’re finally ready to run the rock and test your 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 your Flask rock, you can always get the application logs via pebble:

sudo docker exec flask-hello-world pebble logs flask

As a result, Pebble will give you the logs for the flask service running inside the container. You should 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

You can also choose to follow the logs by using the -f option with the pebble logs command above.

Important

To get the Flask application logs, the container must be running. This is also valid for the remaining sections of this tutorial.

Cleanup

Now you have a fully functional rock for you Flask application! This concludes the first part of this tutorial, so you can 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 you’re looking to deploy your rock into a production environment. With Chisel you can produce lean and production-ready rocks by getting rid of all the contents that are not needed for your Flask application to run. This results in a much smaller rock with a reduced attack surface.

The first step towards chiselling your rock is to ensure you 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

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

You’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, you can confirm this by running the rock with Docker

sudo rockcraft.skopeo --insecure-policy copy oci-archive:flask-hello-world_0.1_amd64.rock docker-daemon:flask-hello-world:0.1
sudo docker images flask-hello-world:0.1
sudo docker run --rm -d -p 8000:8000 --name flask-hello-world flask-hello-world:0.1

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. You can now stop the container and remove the corresponding image:

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

Update Flask application

As a final step, let’s say you want to update your application. For example, you 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 you are creating a new version of your application, open the rockfile.yaml file and change the version (e.g. to 0.2).

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

Cleanup

You 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 your environment

You’ve reached the end of this tutorial.

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

# exit and delete the virtual environment
deactivate
rm -rf .venv
# delete all the files created during the tutorial
rm flask-hello-world_0.1_amd64.rock flask-hello-world_0.2_amd64.rock rockcraft.yaml app.py requirements.txt
If using Multipass...

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

exit

And then you can proceed with its deletion:

multipass delete rock-dev
multipass purge

Troubleshooting

Application updates not taking effect?

Upon changing your 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.