Building your runtime environments
Dockerfile
It all starts with a Dockerfile. For a thorough walk-through of what a Dockerfile is, it is recommended to consult Docker’s documention or Kleene’s getting started guide, which is an adapted version of Docker’s guide, to highlight differences.
Kleened builds images by reading the instructions from a Dockerfile, using a subset of the instructions known from Docker. You can find Kleene’s specification reference in the Dockerfile reference.
Here are the most common and basic instructions:
Instruction | Description |
---|---|
FROM <image> |
Defines a base for your image. This is used to create a new designated filesystem for the image that is being built. |
RUN <command> |
Executes any commands within the designated filesystem. RUN also has a shell form for running commands. |
WORKDIR <directory> |
Sets the working directory for any RUN , CMD , and COPY instructions that follow it in the Dockerfile. |
COPY <src> <dest> |
Copies new files or directories from <src> and adds them to the filesystem of the container at the path <dest> . |
CMD <command> |
Defines the default command that runs when starting containers based on this image. |
The default filename to use for a Dockerfile is Dockerfile
, without a file
extension. Using the default name allows you to run the klee build
command
without having to specify additional command flags.
Some projects may need distinct Dockerfiles for specific purposes. A common
convention is to name these Dockerfile.<something>
. Such Dockerfiles can then
be used through the --file
(or -f
shorthand) option on the klee build
command.
Refer to the klee build
section
in the klee
reference documentation to learn about build configuration.
Context
A build’s context is the set of files located at a path specified
by the positional PATH
argument to the build command, i.e.,
$ klee build [OPTIONS] PATH
The build process can refer to any of the files or directories in the context
using the COPY
instruction.
Note
Presently, the
PATH
argment should refer to a path on the host machine and not the client whereklee
is running. If you are using Klee on the same system as Kleened, those two are the same.
How image building works
Kleene images are essentially created by cloning the filesystem of another image. The cloned file system is then used to create a container that is used to execute instructions from the Dockerfile. When there is no more instructions, Kleene saves all relevant metadata and converts the container’s filesystem into an image-filesystem and the build is complete.
Since the basis for an image build is a ZFS-clone, it is duplicated with practically zero storage costs. Only the data that is written during the build process takes up actual space on the hosts filesystem.
Note that unlike images of, e.g., Docker and Podman, Kleene has no concept of layers. Kleene uses zfs snapshots and clones for creating images and containers.
Example: Creating an image
Here’s a simple Dockerfile example to get you started with building images. We’ll take a simple “Hello World” Python Flask application, and bundle it into an image that can be easily deployed by Kleene.
Remember to prepare the Kleene host if haven’t already been done:
$ klee image create -t FreeBSD fetch-auto
... a lot of output here ...
$ klee network create --subnet 10.13.37.0/24 testnet
Since no tag was given, kleene automatically uses latest
, meaning that the
nametag of the image created above will be FreeBSD:latest
.
Now, let’s say we have a hello.py
file with the following content:
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello World!"
If you’re not familiar with Python, it’s just a simple web server that will contain a single page that says “Hello World”. However, remember to preserve indentation.
Here’s the Dockerfile that will be used to create an image for our application:
FROM FreeBSD:latest
# install app dependencies
RUN pkg install -y py39-flask
# install app
COPY hello.py /
# final configuration
ENV FLASK_APP=hello
CMD flask run --host 0.0.0.0 --port 8000
First, we define the FROM
-instruction:
FROM FreeBSD:latest
Here the FROM
instruction sets the
parent image of our “Hello World”-app to the base image that we created just before.
All following instructions are executed on (a clone of) this base image.
# install app dependencies
RUN pkg install -y py39-flask
The RUN
instruction executes a shell
command that installs Flask and all it’s dependencies, including Python.
In this example, our context is a full FreeBSD base system matching that of the host.
Also note the # install app dependencies
comment line. Comments in
Dockerfiles begin with the #
symbol. As your Dockerfile evolves, comments can
be instrumental to document how your dockerfile works for any future readers
and editors of the file.
COPY hello.py /
Now we use the COPY
instruction to
copy our hello.py
file from the local build context into the
root directory of our image. After being executed, we’ll end up with a file
called /hello.py
inside the image.
ENV FLASK_APP=hello
The ENV
instruction sets an
environment variable we’ll need later. This is a flask-specific variable,
that configures the command later used to run our hello.py
application.
Without this, flask wouldn’t know where to find our application to be able to
run it.
CMD flask run --host 0.0.0.0 --port 8000
Finally, CMD
instruction sets the
command that is run when the user starts a container based on this image. In
this case we’ll start the flask development server listening on all addresses
on port 8000
.
Building the image and running the app
To test our Dockerfile, we’ll first build it using the klee build
command:
$ klee build -t test:latest .
Here -t test:latest
option specifies the name (required) and tag (optional)
of the image we’re building. .
specifies the build context as
the current directory. In this example, this is where Kleene expects to find the
Dockerfile and the local files the Dockerfile needs to access, in this case
your Python application (hello.py
).
So, in accordance with the build command issued and how build context works, your Dockerfile and python app need to be in the same directory.
Now run your newly built image:
$ klee run --network testnet test:latest
Now the application should be running on your computer. We did not specify
an IP-address, so Kleene automatically found a unused one from the subnet
of the testnet
network. There are several ways to identify it, however,
using jls
or using klee container inspect <container-id>
where
<container-id>
is outputted by klee run ...
.
If you run this container locally, you can open a browser and navigate to
http://localhost:8000
. If you run the container on a remote server you
can make a SSH-tunnel ssh -L 8000:<container IP>:8000 <your-host>
before navigating to http://localhost:8000
.
Build configuration
Since the image creation uses a build container for running build commands, it can be configured like any other container. This can be necessary when some some build steps require non-standard privileges, as illustrated in the image snapshots example. Conversely, there might be a need to restrict the build environment for security reasons.
The configuration parameters used to configure the build container with
klee build
is almost identical to the container configuration of klee run
.
See the the reference documentation
for details.
Image design
Since Kleene is a new tool, there is not any well-established patterns for image design, except for what is being used by other similar tools. However, here follows a few tentative suggestion on image design.
How to keep your images small
In order to keep build times low and minimize storage footprint, it is a good idea to try and keep image sizes small. Here are a few rules of thumb to try an achieve that:
-
If there are multiple images with a lot in common, consider creating a ‘core’ image with the shared components, and basing the images on that instead of installing/configuring the shared components across all images.
-
Consider using the production image as the base image for a debug image, if needed. Additional testing or debugging tooling can be added on top of the production image.
-
When building images, always tag them with useful tags which codify version information, intended destination (
prod
ortest
, for instance), stability, or other information that is useful when deploying the application in different environments. Do not rely on the automatically-createdlatest
tag.
Where and how to persist application data
-
Store data using volumes.
-
One case where it is appropriate to use nullfs mounts is during development, where it is desirable to mount a source directory or newly built binaries into the container. For production, use a volume instead, mounting it into the same location as the bind mount that was used during development.
-
For production, use files mounted into the container for sensitive application data used by services.