Build a rock for a FastAPI app

In this tutorial, we’ll create a simple FastAPI app and learn how to containerise it in a rock with Rockcraft’s fastapi-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 install Rockcraft with classic confinement, which grants it access to the whole file system:

sudo snap install rockcraft --classic --channel latest/edge

This tutorial requires the latest/edge channel of Rockcraft as the framework is currently experimental.

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 fastapi-hello-world
cd fastapi-hello-world

Create the FastAPI app

Let’s start by creating the “Hello, world” FastAPI app that we’ll use throughout this tutorial.

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

~/fastapi-hello-world/requirements.txt
fastapi[standard]>=0.109.1

It’s fastest to test the FastAPI app locally, before we pack it into a rock, so let’s install python3-venv and create a virtual environment we can work in:

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, put the following code into a new file, app.py:

~/fastapi-hello-world/app.py
from fastapi import FastAPI

app = FastAPI()


@app.get("/")
async def root():
    return {"message": "Hello World"}

Run the FastAPI app using fastapi dev app.py --port 8000 to verify that it works.

Test the FastAPI 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 FastAPI app should respond with {"message":"Hello World"}.

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

Pack the FastAPI app into a rock

Now let’s create a container image for our FastAPI 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 FastAPI profile, Rockcraft automates the creation of rockcraft.yaml and tailors the file for a FastAPI app. From the ~/fastapi-hello-world directory, initialize the rock:

rockcraft init --profile fastapi-framework

The project file will automatically be created in the project’s 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:

~/fastapi-hello-world/rockcraft.yaml
name: fastapi-hello-world
# see https://documentation.ubuntu.com/rockcraft/en/latest/explanation/bases/
# for more information about bases and using 'bare' bases for chiselled rocks
base: [email protected] # the base environment for this FastAPI app
version: '0.1' # just for humans. Semantic versioning is recommended
summary: A summary of your FastAPI app # 79 char long summary
description: |
    This is fastapi project'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 fastapi-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 fastapi-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_ENABLE_EXPERIMENTAL_EXTENSIONS=true 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. Since FastAPI is an experimental extension, ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS must be enabled.

Once Rockcraft has finished packing the FastAPI 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 FastAPI rock with Docker

We already have the rock as an OCI archive. Now we 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:fastapi-hello-world_0.1_$(dpkg --print-architecture).rock \
  docker-daemon:fastapi-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 FastAPI 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 fastapi-hello-world:0.1

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

REPOSITORY            TAG       IMAGE ID       CREATED       SIZEfastapi-hello-world   0.1       30c7e5aed202   2 weeks ago   193MB

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 FastAPI app:

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

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

curl --fail localhost:8000

The FastAPI app should again respond with {"message":"Hello World"}.

View the app logs

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

sudo docker exec fastapi-hello-world pebble logs fastapi

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

2024-10-01T06:32:50.180Z [fastapi] INFO:     Started server process [12]2024-10-01T06:32:50.181Z [fastapi] INFO:     Waiting for application startup.2024-10-01T06:32:50.181Z [fastapi] INFO:     Application startup complete.2024-10-01T06:32:50.182Z [fastapi] INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)2024-10-01T06:32:58.214Z [fastapi] INFO:     172.17.0.1:55232 - "GET / HTTP/1.1" 200 OK

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 FastAPI app! This concludes the first part of this tutorial, so we’ll stop the container and remove the respective image for now:

sudo docker stop fastapi-hello-world
sudo docker rmi fastapi-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 FastAPI 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@24.04:

sed -i \
  "s/base: .*/base: bare\nbuild-base: [email protected]/g" \
  rockcraft.yaml

Note

The sed command replaces the current base in the project file 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 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:

~/fastapi-hello-world/rockcraft.yaml
name: fastapi-hello-world
# see https://documentation.ubuntu.com/rockcraft/en/latest/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 FastAPI app # 79 char long summary
description: |
    This is fastapi project'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_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack

As before, verify that the new rock was created:

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

We’ll verify that the new FastAPI rock is now approximately 35% 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:fastapi-hello-world_0.1-chiselled_$(dpkg --print-architecture).rock \
  docker-daemon:fastapi-hello-world:0.1-chiselled
sudo docker images fastapi-hello-world:0.1-chiselled
sudo docker run --rm -d -p 8000:8000 \
  --name fastapi-hello-world fastapi-hello-world:0.1-chiselled

and then using the same curl request:

curl --fail localhost:8000

The FastAPI app should still respond with {"message":"Hello World"}.

Cleanup

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

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

Update the FastAPI 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.

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

~/fastapi-hello-world/app.py
import datetime

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
async def root():
    return {"message": "Hello World"}


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

Since we are creating a new version of the app, 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:

~/fastapi-hello-world/rockcraft.yaml
name: fastapi-hello-world
# see https://documentation.ubuntu.com/rockcraft/en/latest/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 FastAPI app # 79 char long summary
description: |
    This is fastapi project'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_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack
sudo rockcraft.skopeo --insecure-policy \
  copy oci-archive:fastapi-hello-world_0.2_$(dpkg --print-architecture).rock \
  docker-daemon:fastapi-hello-world:0.2
sudo docker images fastapi-hello-world:0.2
sudo docker run --rm -d -p 8000:8000 \
  --name fastapi-hello-world fastapi-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. {"value":"2024-10-01 06:53:54\n"}).

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 fastapi-hello-world
sudo docker rmi fastapi-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 fastapi-hello-world_0.1_$(dpkg --print-architecture).rock \
  fastapi-hello-world_0.1-chiselled_$(dpkg --print-architecture).rock \
  fastapi-hello-world_0.2_$(dpkg --print-architecture).rock \
  rockcraft.yaml app.py 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 FastAPI 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.