Introduction

Certain directories within a jail can be swapped out to (datasets) "outside". This allows this data to be stored independently of the jail.

Goals

The goal, similar to Docker, is to depend as little as possible on the jail itself. This way, if the jail is corrupted or deleted for some reason, we are able to restore the previous configuration and data with minimal effort. Also, these directories can then be efficiently and regularly backed up via snapshot, which also simplifies further backups. Not to mention moves or migrations.

Last update:

  • 23.12.2025: Integration of directories rewritten
  • 30.11.2024: Revision
  • 17.11.2024: TrueNAS references removed, converted to Bastille and adapted to the FreeBSD Server article series and Bastille
  • 03.03.2024: Minor adjustments
  • 23.01.2024: Some tweaking and configuration added to TrueNAS
  • 18.08.2023: Initial version

Terminology

  • JAILNAME = Name of the Jail
  • POOLNAME = The ZFS pool name where the data is to be stored, e.g., work
  • SOURCEPATH = The directory (DATASET) on FreeBSD, e.g., /usr/local/bastille/data/JAILNAME, which refers to the ZFS pool data in the dataset bastille_data/JAILNAME
  • DESTINATIONPATH = The directory within the jail, e.g., /usr/local/bastille/jails/JAILNAME/root/mnt/data
  • ID = Required user ID within the jail, e.g., 123

Directory structure

Important: Since ZFS is used here, there is a distinction between the pool or dataset and the actual directory in which the dataset is mounted

  • the jails are stored in the ZFS pool work in the directory bastille and
  • the outsourced data is stored in the ZFS pool data in the directory bastille_data. This pool may offer significantly more storage space. This is very useful for large data dumps such as images or file shares that simply have no place in the jail. Read-only if necessary.

However, to prevent these different pools from causing confusion, these datasets are bundled in the Bastille directory /usr/local/bastille/. The complete picture is as follows:

─ work/bastille -> /usr/local/bastille/
─ data/bastille_data -> /usr/local/bastille/data

└── /usr/local/bastille/jails # Jail directory
    └── JAILNAME              # Jail name
        └── root              # Root of the jails, the / within the jail
└── /usr/local/bastille/data  # Jail data directory
    └── JAILNAME              # Jail name
        └── conf              # Configurations, ex. /mnt/conf
        └── data              # Jail data, ex. /mnt/data
        └── db                # Database, ex. /var/db/pgsql
        └── ...               # etc

NullFS

But how does that work? Quite simply via NullFS. NullFS essentially places the contents of directory A in directory B, even across jail boundaries. For example, /usr/local/bastille/data/JAILNAME/data is then located in the jail under /mnt/data. Very handy! However, the correct permissions are an important factor. If there is a user USER with the ID 123 in the jail who has the appropriate access rights to a directory, then the directory outside the jail MUST have the same rights. Fortunately, the user ID is sufficient for this, so you don't have to create the corresponding user name in the FreeBSD server every time.

Everything described below takes place on the FreeBSD server. NOT within the jail!

Creating data directories First, the base directory is created in POOLNAME work with work/bastille_data and mounted in /usr/local/bastille/data/ with
zfs create -o /usr/local/bastille/data data/bastille_data.

The directory /usr/local/bastille/data/JAILNAME/data is then simply created with the command zfs create -p work/bastille_data/JAILNAME/data. The parent directory /usr/local/bastille/data/JAILNAME is also automatically created with the parameter -p.

Permissions

chmod -R ID:ID /usr/local/bastille/data/JAILNAME/data

Integrate data directories

Important! Since the target directories often do not yet exist in the jail, they must be created in advance. Some of the directories are only created when the packages are installed, but this is too late. This is because during installation, the content (e.g., configuration files) is copied to the target directory.

Manually

This directory can be manually integrated for testing purposes:

mount -t nullfs /usr/local/bastille/data/JAILNAME/conf /usr/local/bastille/jails/JAILNAME/root/mnt/conf
mount -t nullfs /usr/local/bastille/data/JAILNAME/data /usr/local/bastille/jails/JAILNAME/root/mnt/data
mount -t nullfs /usr/local/bastille/data/JAILNAME/db /usr/local/bastille/jails/JAILNAME/root/var/db/pgsql

Automatically

In order for Bastille to automatically mount the directories, they are entered into the fstab belonging to the jail. ee /usr/local/bastille/data/JAILNAME/fstab

/usr/local/bastille/data/JAILNAME/conf /usr/local/bastille/jails/JAILNAME/root/mnt/conf nullfs rw 0 0
/usr/local/bastille/data/JAILNAME/data /usr/local/bastille/jails/JAILNAME/root/mnt/data nullfs rw 0 0
/usr/local/bastille/data/JAILNAME/db /usr/local/bastille/jails/JAILNAME/root/var/db/pgsql nullfs rw 0 0

Voilá