Building containers from scratch with container tools project
As container runtime and image standards have evolved and standards developed by projects like the Open Container Initiative (OCI), there has been an increase in the number of solutions and/or use case specific tools being developed both in FOSS and commercial tools.
An example of this is the crun
runtime that we used in the Centos 8 Stream Kubernetes tutorial from earlier this year. This runtime is a memory optimised lightweight OCI compliant runtime and does not have the full lifecycle tooling that other runtimes like Docker.
This is where the container tools project comes in, this provides a number of tools that provide coverage across the build, local run and inspection of OCI compliant container images. In fact, leveraging these tools is becoming the de-facto approach for new projects.
In this article, we will build a container from scratch using buildah
, running this container on Linux using podman
and skopeo
to inspect the container we have built.
The examples below were tested on a Centos VirtualMachine running Centos 8 Stream and the tools can be installed using yum.
sudo yum install -y podman buildah skopeo
First, we are going to stop the docker service to ensure that we are using the tool we have installed.
sudo systemctl stop docker
The next step is to start the container build, lets’s build the container from an empty context using from scratch
as the base for the container context.
Then the container context needs to be mounted to the host’s filesystem to allow components to be installed and configured.
You will need to be root
to mount the container to the host. I escalate my privileges using sudo -s
so I can assign the container ID and mount path to variables.
In a more secure implementation, you may want to manage your privileges per command.
newcontainer=$(buildah from scratch)
scratchmnt=$(buildah mount $newcontainer)
Now there is an empty container context we can access and start to add some software too. As we want to optimise the size of the container as well as leverage the Centos package manager we are going to:
- Manage the dependencies we install by setting
install_weak_deps=false
- Not install any docs using the option
tsflags=nodocs
- Limit the language files we install to one using option
override_install_langs=en_US.utf8
This is done using the --setopt
parameter in the yum commands as packages are installed.
This can be taken a step further by separating the installation of bash
and coreutils
from the python packages we are going to install.
By taking these 2 actions in our container build we were able to bring the size of the container down from 642 MB to 449 MB by minimising the number of dependency hops that the yum
package manager needs to follow.
yum install --installroot $scratchmnt bash coreutils --releasever 8 --setopt=install_weak_deps=false --setopt=tsflags=nodocs --setopt=override_install_langs=en_US.utf8 -y
yum install --installroot $scratchmnt python3 python3-pip --releasever 8 --setopt=install_weak_deps=false --setopt=tsflags=nodocs --setopt=override_install_langs=en_US.utf8 -y
This can be taken a step further by cleaning up the cache files from the yum
packages that have just been installed. This has shrunk the container from 449 MB to 332 MB.
yum clean --installroot $scratchmnt all
You can verify this using du
on the mounted filesystem just like you would any other path mounted to your host.
du -hs $scratchmnt
As long as you continue to use the additional yum
options that were used above. You can keep adding to the python container as you need to in an optimised manner.
Alternatively, you can also run commands as you would using the RUN
statement in a Dockerfile.
buildah run $newcontainer pip3 install flask
Now that we have flask
installed we need to define the entry point for our container when it is executed.
Flask is a lightweight web framework that has a huge community following and a massive plugin library making it a great choice for web application use cases.
As we have the container locally we can now develop locally within the container context using chroot
to set $scratchmnt
as our new /
directory.
chroot $scratchmnt
Now our container context is the root and we can run flask
using flask run
this will raise an error as we have no application for the framework to run, so run the command to exit
to get back to the server context.
Next is to create a flask
application. This can be done anywhere on the server so let’s go to the user home directory on the Linux machine cd ~
. From here create a directory called app
and a file called app.py
with the content using your favourite editor.
The content for app.py
is the following. This is the Flask projects Hello World web application.
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return 'Web App with Python Flask!'
Now that this is created we can simply copy this to the root of the container filesystem mounted to the host using the cp
command.
cp -r ~/app $scratchmnt
Now you can test the Flask application by using chroot
again to bind to the container, using the same command as above.
Now the console is bound to the container context again you can test your Flask web application.
FLASK_APP=./app/app.py flask run
This will generate the following output.
```bash-4.4# FLASK_APP=./app/app.py /usr/local/bin/flask run
- Serving Flask app ‘./app/app.py’ (lazy loading)
- Environment: production WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
- Debug mode: off
- Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) ```
This is where we start to see what is powerful about buildah
as not only have all of the dependencies been tested but all of the components for the entry point script have been identified and tested in the container as we are developing it locally.
Now let’s put everything together and get the container built so we can run it in podman
.
Let’s create another file in the user home directory called entrypoint.sh
and put the following content in it with your favourite editor.
#! /bin/bash
export FLASK_APP=/app/app.py
/usr/local/bin/flask run
Now the entry point script is created it needs to be made executable and then placed within the container context. This could be done using cp
as we did to move the web app directory, but instead, we can use buildah
to perform the same action as a COPY
statement in a Dockerfile.
chmod775 ./entrypoint.sh
buildah copy $newcontainer ~/entrypoint.sh /
To validate that the entry point is behaving as expected we can use buildah run
again. As this executes a command in real-time rather than at build time like the RUN
statement in a Dockerfile this can be used to quickly test commands within the container context.
buildah run $newcontainer /entrypoint.sh
This should output the same as was seen above in the earlier execution of the Flask server. Use CTRL+C
to exit the server running.
Now that components are behaving as expected the container context needs to be told how to start, this is done by setting parameters in the configuration of the container.
buildah config --entrypoint /entrypoint.sh $newcontainer
Now the container will start the Flask web application server when an instance of the container image is deployed. Let’s set some metadata so that the context of the container can be understood. In the config, let’s set values for the author, who it is created by and add the label name
to identify the image.
buildah config --author "Dale Stirling" --created-by "dalethestirling" --label name=python3-flask-demo $newcontainer
With the values set in the configuration, these can be verified using the inspect sub command.
buildah inspect $newcontainer
In the output not only can you see the definition of the entry point script to be run and the metadata that was set, but also the Capabilities that the container can leverage as well as environment variables within the container.
Now the container is defined it is time to commit the changes that have been made within the context to be committed to the image. To do this the container image needs to be unmounted from the host and then the changes are added to an image.
buildah unmount $newcontainer
buildah commit $newcontainer python3-flask-demo
Podman is the CLI interface for deploying containers to runc
(or other OCI runtime) locally. The podman
command has the same semantics as docker
to allow for better cross-compatibility.
Using podman
we can now list images that are cached locally on the host.
podman images
You should now see localhost/python3-flask-demo
in the list of containers.
Having the image built we can inspect the image using either skopeo
or podman
.
skopeo inspect containers-storage:localhost/python3-flask-demo
podman inspect localhost/python3-flask-demo
Through the output of either of these commands, we can see another one of the benefits of using buildah
in this way. After all of the commands, we have run our container is still a single layer.
...
"Layers": [
"sha256:e5bed93f30ac57a4965e8e8ba5facda6208fcc412c5c0034e7d16f07d34c51cf"
],
...
This can be especially handy if you are extending a base container that contains several layers already.
Optimising the number of layers in your overlay filesystem can provide performance gains in reading operations as it has to search for the file down through the layers.
Using skopeo
also allows you to interrogate details about an image in remote repositories such as Docker hub.
skopeo inspect docker://docker.io/dalethestirling/python-demo:latest
Now we are at the point where an instance of the container can be created and run. Podman follows in line with the parameter and argument conventions used by docker
allowing for ease of use.
We will need to expose a port so that we can connect to the flask
web server and verify it is working. In the same way as you would with docker
this can be done using the -e
parameter to expose port 5000.
podman run -p 5000:5000 localhost/python3-flask-demo
curl http://127.0.0.1:5000
Now you have a working container that you have built from scratch. This container is a single layer and built from the latest OS packages.
Using the container tools project provides a way to build an OCI compliant container using many existing Linux tools for configuration that can be easily wrapped up into a bash script or similar.
While this demo is rudimentary. Our container is only 344 MB including flask
and its dependencies are not far off the RedHat UBI python image at 297.8 MB. This is a 46 MB difference.
When it comes to automating and testing this approach you can leverage the same tools you would when testing your application locally on your laptop and it can be done inline within your container context as you build the container, but more on that in future posts.
I have included all of the commands in a Gist here to get you started with scripting your container builds with these tools.