Using nullfs mounts
In the previous chapter, we talked about and used a volume mount to persist the data in our database. A volume mount is a great choice when you need somewhere persistent to store your application data within Kleene.
A nullfs mount is another type of mount, which lets you share a directory from the
host’s filesystem into the container. When working on an application, you can
use a nullfs mount to mount source code into the container. Another usecase can be
the need to make contents available from external storage such as nfs
.
In this chapter, we’ll see how we can use nullfs mounts and a tool called nodemon to watch for file changes, and then restart the application automatically. There are equivalent tools in most other languages and frameworks.
Quick volume type comparisons
The following table outlines the main differences between volume mounts and nullfs mounts.
Volumes | Nullfs mounts | |
---|---|---|
Host location | Kleened stores it under the <kleene-root>/volumes dataset |
You decide |
Example | --mount my-volume:/usr/local/data |
--mount /path/to/data:/usr/local/data |
Under the hood, volume mounts are nullfs mounts where Kleene manages the underlying ZFS filesystem.
Nullfs mounts are, as the name suggests, mountpoints created using the
nullfs(5)
file system layer of FreeBSD.
Trying out Kleene’s nullfs mounts
Before looking at how we can use nullfs mounts for developing our application, let’s run a quick experiment to get a practical understanding of how they work.
-
Open a terminal and and go to the
app
directory of the getting started repository. -
Run the following command to start
sh
in an fresh container with a nullfs mount.$ klee run -it --mount /path/to/getting-started:/mnt FreeBSD-13.2-RELEASE /bin/sh
The
--mount
option tells Kleene to create a nullfs mount, where/path/to/getting-started
is the app-repos on the host, and/mnt
is where that directory should be mounted inside the container. -
After running the command, Klee starts an interactive
sh
session in the root directory of the container’s filesystem. Now, list the contents of/mnt
within the container:# ls /mnt ls /mnt .dockerignore LICENSE docs .git README.md mkdocs.yml .github app requirements.txt .gitignore build.sh Dockerfile docker-compose.yml
This is the directory that you mounted when starting the container. Listing the contents of this directory displays the same files as in the
getting-started
directory on our laptop. -
Create a new file named
myfile.txt
.# touch /mnt/myfile.txt # ls /mnt .dockerignore LICENSE docs .git README.md mkdocs.yml .github app myfile.txt .gitignore build.sh requirements.txt Dockerfile docker-compose.yml
-
Now if you open this directory on the host, you’ll see the
myfile.txt
file has been created in the directory. -
From the host, delete the
myfile.txt
file and look into the container again. The file has disappeared. -
Type
exit
in the container-console to exit the container and close the session.
This demonstrated how files are shared between the host and the container, and how changes are immediately reflected on both sides. Now let’s see how we can use nullfs mounts during application development.
Run your app in a development container
The following steps describe how to run a development container that does the following:
- Nullfs-mount our source code stored on the host in
/home/jane/getting-started/app
, into the container. - Install all dependencies
- Start
nodemon
to watch for filesystem changes
So, let’s do it!
-
Make sure to delete any
webapp
containers previously created. -
Make a Dockerfile for a new image that does not contain the application source code. In this example, the easy way is to build on the existing image where
node
andyarn
are already installed. Ideally we would split up the first image in two, such that we had a ‘base image’ containing the necessary software packages and another image for setting up our application.Save the following content in
Dockerfile.dev
FROM webapp:latest RUN rm -rf /app # Listens on port 3000 CMD cd /app && yarn install && yarn run dev
in same directory as the other Dockerfile. The
CMD
instruction starts a Bourne shell (sh
) and runsyarn install
to install dependency packages and then runningyarn run dev
to start the development server. If we look in thepackage.json
, we’ll see that thedev
script startsnodemon
. -
Build the new image in a similar way as the previous image:
$ klee build -t webapp-dev -f Dockerfile.dev .
We explicitly tell Kleene which Dockerfile should be used for the build with
-f Dockerfile.dev
, thus avoiding the defaultDockerfile
we created previously. The above command should be executed in thegetting-started/app
directory. -
Run the following command from the
getting-started/app
directory.$ klee run -n testnet --mount /home/jane/getting-started/app:/app webapp-dev
- The
-d
flag is omitted so the container output will be printed to the terminal. - The
-n testnet
connects the container to our testing network. --mount /home/jane/getting-started/app:/app
- nullfs mount our application source code from the host into the/app
directory in the container.webapp-dev
- the image to use. This is our newly built image from above.
You should see output similar to this:
<initial output here> $ nodemon src/index.js 2.0.20 to restart at any time, enter `rs` watching path(s): *.* watching extensions: js,mjs,json starting `node src/index.js` Using sqlite database at /etc/todos/todo.db Listening on port 3000
Now you can hit
Ctrl
+C
to return to the terminal prompt. Don’t worry, your container is stilling running. Useklee lsc
to be sure. - The
-
Now, make a change to the app. In the
src/static/js/app.js
file, on line 109, change the “Add Item” button to simply say “Add”:- {submitting ? 'Adding...' : 'Add Item'} + {submitting ? 'Adding...' : 'Add'}
Save the file.
-
Refresh the page in your web browser, and you should see the change reflected almost immediately. It might take a few seconds for the Node server to restart. If you get an error, try refreshing after a few seconds.
Using nullfs mounts is useful for local development setups. The advantage is that
the development machine doesn’t need to have all of the build tools and
environments installed. With a single klee run
command, dependencies and
tools are ready to go.
Next steps
In order to prepare for production, you need to migrate your database from working in SQLite to something that can scale a little better. For simplicity, you’ll keep with a relational database and switch your application to use MariaDB. But, how should you run MariaDB? How do you allow the containers to talk to each other? You’ll learn about that next!