In any development project, you usually have to worry about many components: databases, dependencies, configurations, network settings and so on. Things can get too complicated when you need to transfer your project to another colleague or, even worse, transfer the project to a production environment, and very bad things can happen… Those problems has been solved by some useful tools, such as those for creating and managing containers.

AppJail is the framework we will use to create a container with our project: an API using the FastAPI framework. By using this tool we ensure that we can share and reproduce our project to guarantee that it will run smoothly with virtually the same environment as on the host. As a bonus, the host will not be affected.

The project

We use the following structure to simplify the development of our project:

1
2
mkdir project/
mkdir project/app/

Note: All files mentioned in this document are relative to the project/ directory.

Now we create a app/requirements.txt to put the dependencies that our project needs:

fastapi[all]

Note: For simplicitly, we will install FastAPI with optional dependencies and features, but you probably won’t need all of them in a production environment. See documentation for details.

Our app/main.py is only a few lines long:

1
2
3
4
5
6
7
from fastapi import FastAPI

app = FastAPI()

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

A simple hello world is displayed each time the user makes an HTTP request with the GET method.

The Makejail

The next step is to create the Makejail.

INCLUDE gh+AppJail-makejails/python

We have included the Makejail from the centralized repository to install Python.

# Optional, see below for details.
INCLUDE options/network.makejail

Another Makejail with only the network options we use for this jail. The content is as follows:

ARG network
ARG interface=python

OPTION virtualnet=${network}:${interface} default
OPTION nat

Of course, we need the options/ directory beforehand (remember: relative to the project/ directory):

1
mkdir options/

Another way to achieve the above is to define option by option on the command-line when building the jail as we will see later.

WORKDIR /app
COPY app/

Using WORKDIR we create a directory named /app inside the jail. As we have changed the working directory, COPY is affected and the target directory is relative to that directory, so we can omit the second argument and the files in app/ directory will be copied from the host to the jail. Remember that the / suffix is for copying all the files, not the directory itself.

CMD pw useradd -n pyapp -d /app -s /bin/sh
CMD mkdir -p /app
CMD chown -R pyapp:pyapp /app

We create a dedicated user named pyapp to run the script with less privileges than root.

PKG py%{PYTHON_MAJOR}%{PYTHON_MINOR}-pip

devel/py-pip is not installed by default, so we install it. We can install pip explicitly with the corresponding python version, but the Makejail for Python provides some build arguments.

PKG py%{PYTHON_MAJOR}%{PYTHON_MINOR}-wheel
PKG rust

lang/rust is required by a FastAPI dependency. It is recommended to install devel/py-wheel when installing python dependencies.

USER pyapp
RUN pip install --user -r requirements.txt

We install the dependencies by running pip using the dedicated user.

STAGE cmd

USER pyapp
WORKDIR /app
RUN /app/.local/bin/uvicorn main:app --reload --host 0.0.0.0

The cmd stage is used to run the python script using appjail run as we will se later. The working directory is reset with each stage, so we need to specify it again. USER is also reset for each stage.

The command uvicorn main:app refers to:

  • main: the file main.py (the Python ‘module’).
  • app: the object created inside of main.py with the line app = FastAPI().
  • --reload: make the server restart after code changes. Only use for development.
  • --host: as the default is 127.0.0.1, we need to change to the jail’s IP address, but for simplicity, we use 0.0.0.0 to listen on all IP addresses of all interfaces.

All of the above instructions in a single file can be seen below:

INCLUDE gh+AppJail-makejails/python
# Optional, see below for details.
INCLUDE options/network.makejail

WORKDIR /app
COPY app/

CMD pw useradd -n pyapp -d /app -s /bin/sh
CMD mkdir -p /app
CMD chown -R pyapp:pyapp /app

PKG py%{PYTHON_MAJOR}%{PYTHON_MINOR}-pip
PKG py%{PYTHON_MAJOR}%{PYTHON_MINOR}-wheel
PKG rust

USER pyapp
RUN pip install --user -r requirements.txt

STAGE cmd

USER pyapp
WORKDIR /app
RUN /app/.local/bin/uvicorn main:app --reload --host 0.0.0.0

The product

Since we are going to use a virtual network configuration, we first need to create one:

1
2
3
4
# appjail network add development 172.0.0.0/10
# appjail network list
NAME         NETWORK    CIDR  BROADCAST       GATEWAY    MINADDR    MAXADDR         ADDRESSES  DESCRIPTION
development  172.0.0.0  10    172.63.255.255  172.0.0.1  172.0.0.1  172.63.255.254  4194302    -

All the requirements have been done correctly, so all we have to do is open a shell and run:

1
appjail makejail -j pyapp -- --network development

The command appjail makejail refers to:

  • -j pyapp: name of the jail. If not defined, a random name is chosen.
  • --network development: the virtual network to be used.

As mentioned in previous sections, we can specify option by option on the command-line instead of using options/network.makejail. Of course, remove the instruction that includes that file if you want to use the command-line.

1
appjail makejail -j pyapp -o virtualnet="development:pyapp default" -o nat

Once the above process is finished, we can run the API:

1
2
3
4
5
6
7
# appjail run pyapp
INFO:     Will watch for changes in these directories: ['/app']
INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [93321] using WatchFiles
INFO:     Started server process [93323]
INFO:     Waiting for application startup.
INFO:     Application startup complete.

And we can make an HTTP request but we only need the jail’s IP address.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# appjail jail list -j pyapp
STATUS  NAME   TYPE  VERSION       PORTS  NETWORK_IP4
UP      pyapp  thin  13.1-RELEASE  -      172.0.0.2
# curl -i 'http://172.0.0.2:8000'
HTTP/1.1 200 OK
date: Sun, 05 Mar 2023 00:10:07 GMT
server: uvicorn
content-length: 25
content-type: application/json

{"message":"Hello world"}

Simplify your life

There is a time when we develop a python project and a dependency requires another dependency to be compiled. This is the case for watchfiles in the FastAPI framework, which requires lang/rust to compile its stuff. This takes a long time depending on your hardware, but there is a simple way to spend less time.

We can use the precompiled binaries in the FreeBSD repositories if the dependencies are already ported, so we just have to change our Makejail a bit to do this job.

INCLUDE gh+AppJail-makejails/python
# Optional, see below for details.
INCLUDE options/network.makejail

WORKDIR /app
COPY app/

CMD pw useradd -n pyapp -d /app -s /bin/sh
CMD mkdir -p /app
CMD chown -R pyapp:pyapp /app

PKG py%{PYTHON_MAJOR}%{PYTHON_MINOR}-fastapi
PKG py%{PYTHON_MAJOR}%{PYTHON_MINOR}-uvicorn

STAGE cmd

USER pyapp
WORKDIR /app
RUN uvicorn main:app --reload --host 0.0.0.0