Build a rock for a FastAPI application¶
In this tutorial, we’ll create a simple FastAPI application 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
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 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
).
Before we go any further, for this tutorial we’ll need the most recent version
of Rockcraft on the edge channel. Run sudo snap refresh rockcraft --channel
latest/edge
to switch to it.
Finally, create a new directory for this tutorial and go inside it:
mkdir fastapi-hello-world
cd fastapi-hello-world
Create the FastAPI application¶
Let’s start by creating the “Hello, world” FastAPI application that we’ll use throughout this tutorial.
Create a requirements.txt
file, copy the following text into it, and then
save it:
fastapi[standard]
It’s fastest to test the FastAPI application 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
:
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
return {"message": "Hello World"}
Run the FastAPI application using fastapi dev app.py --port 8000
to verify
that it works.
Test the FastAPI 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 FastAPI application should respond with {"message":"Hello World"}
.
The application looks good, so let’s stop it for now by pressing Ctrl + C.
Pack the FastAPI application into a rock¶
First, we’ll need a rockcraft.yaml
file. Rockcraft will automate its
creation and tailoring for a FastAPI application by using the
fastapi-framework
profile:
rockcraft init --profile fastapi-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
fastapi-hello-world
. Ensure that platforms
includes the architecture of
the host. For example, if the host uses the ARM
architecture, include arm64
in platforms
.
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
Note
Depending on the network, this step can take a couple of minutes to finish.
ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS
is required whilst the FastAPI
extension is experimental.
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 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 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:
sudo rockcraft.skopeo --insecure-policy \
copy oci-archive:fastapi-hello-world_0.1_amd64.rock \
docker-daemon:fastapi-hello-world:0.1
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 SIZE
fastapi-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 application:
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
application’s root endpoint which is running inside the container:
curl localhost:8000
The FastAPI application should again respond with {"message":"Hello World"}
.
View the application logs¶
When deploying the FastAPI rock, we can always get the application 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 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 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 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@24.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_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_amd64.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 localhost:8000
Unsurprisingly, the FastAPI application 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 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
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 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_ENABLE_EXPERIMENTAL_EXTENSIONS=true rockcraft pack
sudo rockcraft.skopeo --insecure-policy \
copy oci-archive:fastapi-hello-world_0.2_amd64.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 localhost:8000/time
The updated application 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_amd64.rock \
fastapi-hello-world_0.1-chiselled_amd64.rock \
fastapi-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 your FastAPI 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
.