|
#!/bin/bash |
|
|
|
set -euo pipefail |
|
shopt -s nullglob |
|
|
|
ABSOLUTE_SCRIPT_PATH="$(realpath "${BASH_SOURCE[0]}")" |
|
SCRIPT_NAME=$(basename "${ABSOLUTE_SCRIPT_PATH}") |
|
SCRIPT_PARENT_DIR="$(dirname "$ABSOLUTE_SCRIPT_PATH")" |
|
DEBIAN_RELEASE="$(. /etc/os-release && echo "${VERSION_CODENAME}")" |
|
|
|
USAGE="WARNING: EXPERIMENTAL AND WORK IN PROGRESS, USE ONLY FOR TESTING! |
|
|
|
Create persistent Debian 'jails' with systemd-nspawn on TrueNAS SCALE |
|
Allows installing software, such as docker, without modifying the host OS |
|
With full access to all files on the host via bind mounts |
|
|
|
Put this script on one of your ZFS datasets (under the /mnt/ directory) |
|
It will create a 'jails' dir next to it, where it stores all the jails |
|
|
|
Run this script as root user |
|
|
|
Usage: [ENV_VAR=value] ./${SCRIPT_NAME} COMMAND [ARG...] |
|
|
|
Commands: |
|
new Create a new jail in the jails dir and start it |
|
wizard Interactive guide to create and start a new jail |
|
up Bring up all jails by running all *.sh files in the jails dir |
|
help Show this help message |
|
|
|
All arguments passed to the new command will be added to the systemd-nspawn command that starts the jail |
|
For available arguments see: https://manpages.debian.org/${DEBIAN_RELEASE}/systemd-container/systemd-nspawn.1.en.html |
|
|
|
Environment variables: |
|
JAILMAKER_DEBUG=0 |
|
JAILMAKER_JAIL_NAME=${DEBIAN_RELEASE} |
|
JAILMAKER_INSTALL_DOCKER=0 |
|
JAILMAKER_INSTALL_NVIDIA=0 |
|
JAILMAKER_GPU_PASSTHROUGH=0 |
|
|
|
Default settings can be overridden via environment variables, documentation about advanced settings is inside ${SCRIPT_NAME} |
|
|
|
Examples: |
|
|
|
# Start a new jail with read/write access to /mnt/ssd/appdata and readonly access to /mnt/Tank/Media |
|
./${SCRIPT_NAME} new --bind=/mnt/ssd/appdata --bind-ro=/mnt/Tank/Media |
|
|
|
# Start a new jail named: my-jail-name |
|
JAILMAKER_JAIL_NAME=my-jail-name ./${SCRIPT_NAME} new |
|
|
|
# Start a new jail with GPU passthrough set-up (if a GPU device can be found during creation) |
|
JAILMAKER_GPU_PASSTHROUGH=1 ./${SCRIPT_NAME} new |
|
|
|
# Start a new jail, install docker and enable debug mode to output all the steps this script takes |
|
JAILMAKER_INSTALL_DOCKER=1 JAILMAKER_DEBUG=1 ./${SCRIPT_NAME} new |
|
|
|
# Bring up all jails by running all *.sh files in the jails dir |
|
./${SCRIPT_NAME} up |
|
" |
|
|
|
fail() { |
|
echo -e "$1" >&2 && exit 1 |
|
} |
|
|
|
[[ $UID -ne 0 ]] && echo "${USAGE}" && fail "Run this script as root..." |
|
|
|
#################################################################### |
|
# Read from user-defined environment variables or use default values |
|
#################################################################### |
|
|
|
# Basic settings, should be safe to override |
|
|
|
# Print each line this script executes in debug mode - valid values: 0 (default), 1 |
|
JAILMAKER_DEBUG="${JAILMAKER_DEBUG:-0}" && [[ "${JAILMAKER_DEBUG}" -eq 1 ]] && set -x |
|
# Default name for the jail is the debian release name, e.g. bullseye - valid values: a string without the '/' character in it |
|
JAILMAKER_JAIL_NAME="${JAILMAKER_JAIL_NAME:-${DEBIAN_RELEASE}}" |
|
# By default we will not install docker inside the jail - valid values: 0 (default), 1 |
|
JAILMAKER_INSTALL_DOCKER="${JAILMAKER_INSTALL_DOCKER:-0}" |
|
# By default we will not install nvidia drivers - valid values: 0 (default), 1 |
|
JAILMAKER_INSTALL_NVIDIA="${JAILMAKER_INSTALL_NVIDIA:-0}" |
|
# By default we will not setup GPU passthrough (unless we're installing nvidia drivers) - valid values: 0, 1 |
|
JAILMAKER_GPU_PASSTHROUGH="${JAILMAKER_GPU_PASSTHROUGH:-${JAILMAKER_INSTALL_NVIDIA}}" |
|
|
|
# Advanced settings, be careful if you change these! |
|
|
|
# By default make a jail which matches the debian version of TrueNAS - valid values: a debian release name (only bullseye has been tested) |
|
JAILMAKER_DEBIAN_RELEASE="${JAILMAKER_DEBIAN_RELEASE:-${DEBIAN_RELEASE}}" |
|
# By default we will use (or create) the 'jails' dir inside this script's parent directory - valid values: a path to a directory (parent directory should exist) |
|
JAILMAKER_JAILS_DIR="${JAILMAKER_JAILS_DIR:-${SCRIPT_PARENT_DIR}/jails}" |
|
# Only allow creating jails inside an /mnt sub-folder, unless safety check if off - valid values: 0, 1 (default) |
|
JAILMAKER_JAILS_DIR_SAFETY_CHECK="${JAILMAKER_JAILS_DIR_SAFETY_CHECK:-1}" |
|
|
|
# Since this file will be executed as root, we need to set appropriate permissions (if not already set) |
|
[[ "$(stat -c%a "${ABSOLUTE_SCRIPT_PATH}")" -ne 700 ]] && chmod 700 "${ABSOLUTE_SCRIPT_PATH}" |
|
|
|
validate_input() { |
|
local jails_dir_parent_dir |
|
jails_dir_parent_dir="$(dirname "${JAILMAKER_JAILS_DIR}")" |
|
|
|
# Check if working directory exists |
|
if [[ ! -d ${jails_dir_parent_dir} ]]; then |
|
fail "Destination directory '${jails_dir_parent_dir}' DOES NOT exist." |
|
# Check if working directory is inside the /mnt directory (so it won't be lost on TrueNAS OS updates) |
|
elif [[ "$JAILMAKER_JAILS_DIR_SAFETY_CHECK" -ne 0 && "${JAILMAKER_JAILS_DIR}" != /mnt/* ]]; then |
|
fail "The destination path must begin with '/mnt/'\nProvided destination was '${JAILMAKER_JAILS_DIR}.'" |
|
# Check for illegal characters in the directory name |
|
elif [[ "${JAILMAKER_JAIL_NAME}" == */* ]]; then |
|
fail "Name may not contain the '/' character.\nJAILMAKER_JAIL_NAME: ${JAILMAKER_JAIL_NAME}." |
|
fi |
|
} |
|
|
|
new() { |
|
validate_input |
|
|
|
local arch original_jail_name x jail_path relative_jail_path additional_flags |
|
arch=$(dpkg --print-architecture) |
|
|
|
# Create the jails dir if it does not exist |
|
[[ -d "${JAILMAKER_JAILS_DIR}" ]] || mkdir "${JAILMAKER_JAILS_DIR}" |
|
|
|
# Only root should be allowed access to this directory |
|
chmod 700 "${JAILMAKER_JAILS_DIR}" |
|
|
|
original_jail_name="${JAILMAKER_JAIL_NAME}" |
|
jail_name=${original_jail_name} |
|
x=1 |
|
|
|
# Append counter to jail path if there already is a jail with this name |
|
while [[ -d "${JAILMAKER_JAILS_DIR}/${jail_name}" ]]; do jail_name="${original_jail_name}-$((x++))"; done |
|
|
|
jail_path="${JAILMAKER_JAILS_DIR}/${jail_name}" |
|
|
|
# Make the new jail directory |
|
mkdir "${jail_path}" |
|
|
|
# Download and extract the root filesystem for the jail |
|
curl -L "https://github.com/debuerreotype/docker-debian-artifacts/raw/dist-${arch}/${JAILMAKER_DEBIAN_RELEASE}/slim/rootfs.tar.xz" | tar -xJ -C "${jail_path}" --numeric-owner |
|
|
|
# Remove resolv.conf, systemd configures this |
|
rm "${jail_path}/etc/resolv.conf" |
|
|
|
# Bind mount this script inside the jail and run it with the '__do_not_run_this_manually__' command |
|
# This will install software inside the jail |
|
systemd-nspawn -q -D "${jail_path}" --bind-ro="${ABSOLUTE_SCRIPT_PATH}" --bind-ro=/lib/modules -E JAILMAKER_DEBUG="${JAILMAKER_DEBUG}" -E JAILMAKER_DEBIAN_RELEASE="${JAILMAKER_DEBIAN_RELEASE}" -E JAILMAKER_INSTALL_DOCKER="${JAILMAKER_INSTALL_DOCKER}" -E JAILMAKER_INSTALL_NVIDIA="${JAILMAKER_INSTALL_NVIDIA}" "${ABSOLUTE_SCRIPT_PATH}" __do_not_run_this_manually__ |
|
|
|
# Start with the flags passed as argument |
|
additional_flags="$1" |
|
|
|
# Add flags for GPU access |
|
if [[ "${JAILMAKER_GPU_PASSTHROUGH}" -eq 1 ]]; then |
|
|
|
additional_flags+=" --property=DeviceAllow='char-drm rw'" |
|
|
|
# Detect intel GPU device and if present add bind flag |
|
[[ -d /dev/dri ]] && additional_flags+=" --bind=/dev/dri" |
|
|
|
# Detect nvidia GPU devices and if present add bind flag |
|
for d in /dev/nvidia*; do additional_flags+=" --bind='${d}'"; done |
|
fi |
|
|
|
if [[ "${JAILMAKER_INSTALL_DOCKER}" -eq 1 ]]; then |
|
# TODO: are any of these flags required for GPU access when NOT using docker? |
|
additional_flags+=" --capability=all --system-call-filter='add_key keyctl bpf'" |
|
fi |
|
|
|
# If there are flags to add, put a space in front if it isn't already there |
|
[[ -n "${additional_flags}" && "${additional_flags}" != " "* ]] && additional_flags=" ${additional_flags}" |
|
|
|
# Use SYSTEMD_SECCOMP=0: https://github.com/systemd/systemd/issues/18370 |
|
# Use SYSTEMD_NSPAWN_LOCK=0: otherwise jail won't start jail after a shutdown (but why?) |
|
# Would give "directory tree currently busy" error and I'd have to run |
|
# `rm /run/systemd/nspawn/locks/*` and remove the .lck file from JAILMAKER_JAILS_DIR |
|
# Disabling locking isn't a big deal as systemd-nspawn will prevent starting a container |
|
# with the same name anyway: as long as we're starting jails using the accompanying .sh script, |
|
# it won't be possible to start the same jail twice |
|
|
|
relative_jail_path=$(realpath --relative-to="${JAILMAKER_JAILS_DIR}" "${jail_path}") |
|
|
|
# Write the static part of the startup shell script (no variable substitution) |
|
cat <<-'EOF' >"${jail_path}.sh" |
|
#!/bin/bash |
|
umask 077 |
|
cd "$(dirname "${BASH_SOURCE[0]}")" |
|
EOF |
|
|
|
# Append the dynamic part of the startup shell script (with variable substitution) |
|
echo "SYSTEMD_SECCOMP=0 SYSTEMD_NSPAWN_LOCK=0 systemd-nspawn --quiet --boot --directory='${relative_jail_path}'${additional_flags} &> '${relative_jail_path}.log' &" >>"${jail_path}.sh" |
|
|
|
# Make executable and allow access only for root user |
|
chmod 700 "${jail_path}.sh" |
|
|
|
# Start the jail |
|
"${jail_path}.sh" |
|
|
|
cat <<-EOF |
|
The jail will now boot in the background... |
|
|
|
List all the running jails with: |
|
machinectl list |
|
|
|
When it's running you can start a shell with: |
|
machinectl shell ${jail_name} |
|
|
|
To poweroff a jail (from inside the jail): |
|
poweroff |
|
|
|
In order to start the jail automatically after TrueNAS boot, configure '${jail_path}.sh' as Post Init Script from the TrueNAS web interface. |
|
|
|
In order to start all jails automatically after TrueNAS boot, configure '${ABSOLUTE_SCRIPT_PATH} up' as Post Init Script from the TrueNAS web interface. |
|
EOF |
|
} |
|
|
|
# This function may be called on each boot of TrueNAS to start all jails |
|
up() { |
|
validate_input |
|
[[ -d "${JAILMAKER_JAILS_DIR}" ]] || fail "Nothing to start: the '${JAILMAKER_JAILS_DIR}' directory does not exist." |
|
# Find all the *.sh files in the jails directory, and start them |
|
for d in "${JAILMAKER_JAILS_DIR}/"*.sh; do "${d}" || true; done |
|
} |
|
|
|
complete_install_inside_jail() { |
|
# Add a safeguard so we don't accidentally install anything on the TrueNAS host |
|
[[ $(cli -c 'system version' 2>/dev/null) == TrueNAS* ]] && echo "This function should only be called inside a jail!" |
|
|
|
# Since apt is no longer executable on TrueNAS, the script won't continue, |
|
# even if the safeguard above stops working |
|
apt update |
|
|
|
# We need dbus for `machinectl shell`, systemd to properly boot the jail |
|
# and systemd-sysv to for the `poweroff` command |
|
apt -y --no-install-recommends install dbus systemd systemd-sysv |
|
|
|
if [[ "${JAILMAKER_INSTALL_DOCKER}" -eq 1 ]]; then |
|
|
|
# Install docker according to official guide: |
|
# https://docs.docker.com/engine/install/debian/ |
|
# Even shorter (but not recommended) would be: |
|
# curl -fsSL https://get.docker.com | sh |
|
|
|
apt -y --no-install-recommends install ca-certificates curl gnupg |
|
mkdir -p /etc/apt/keyrings |
|
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg |
|
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian ${JAILMAKER_DEBIAN_RELEASE} stable" | tee /etc/apt/sources.list.d/docker.list >/dev/null |
|
|
|
apt update |
|
apt -y --no-install-recommends install docker-ce docker-ce-cli containerd.io docker-compose-plugin |
|
fi |
|
|
|
if [[ "${JAILMAKER_INSTALL_NVIDIA}" -eq 1 ]]; then |
|
# TODO: document alternative method of installing nvidia drivers (via apt from the host) |
|
# with --bind-ro=/etc/apt (also cleanup apt after installing: rm -rf /var/lib/apt/lists/*) |
|
# Should I `hold` these packages once installed? |
|
# TODO: Investigate auto update command before starting the jail, |
|
# to ensure jail is always started with matching nvidia driver version |
|
|
|
apt -y --no-install-recommends install kmod |
|
|
|
local nvidia_version nvidia_url |
|
|
|
nvidia_version=$(modinfo nvidia-current --field version) |
|
nvidia_url="https://us.download.nvidia.com/XFree86/Linux-x86_64/${nvidia_version}/NVIDIA-Linux-x86_64-${nvidia_version}.run" |
|
|
|
curl -fL --output "/tmp/nvidia_driver.run" "${nvidia_url}" || fail "Failed to download and install nvidia driver.\nDetected version of the driver on TrueNAS host: ${nvidia_version}.\nAttempted download url: ${nvidia_url}." |
|
|
|
sh /tmp/nvidia_driver.run --ui=none --no-questions --no-kernel-modules |
|
rm /tmp/nvidia_driver.run |
|
|
|
if [[ "${JAILMAKER_INSTALL_DOCKER}" -eq 1 ]]; then |
|
|
|
# Install container-toolkit according to official guide: |
|
# https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html |
|
|
|
local distribution |
|
distribution="$(. /etc/os-release && echo "${ID}${VERSION_ID}")" |
|
|
|
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg |
|
curl -s -L https://nvidia.github.io/libnvidia-container/${distribution}/libnvidia-container.list | sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | tee /etc/apt/sources.list.d/nvidia-container-toolkit.list |
|
apt update |
|
apt -y --no-install-recommends install nvidia-docker2 |
|
fi |
|
fi |
|
} |
|
|
|
wizard() { |
|
# Reset variables to defaults (ignore env variables) |
|
JAILMAKER_INSTALL_DOCKER=0 |
|
JAILMAKER_GPU_PASSTHROUGH=0 |
|
JAILMAKER_INSTALL_NVIDIA=0 |
|
JAILMAKER_DEBUG=0 |
|
additional_flags= |
|
|
|
read -r -p "Do you want to create a new jail? [y/N]: " CHOICE |
|
if ! [[ "${CHOICE}" == 'y' || "${CHOICE}" == 'Y' ]]; then |
|
echo "${USAGE}" && exit |
|
fi |
|
|
|
echo "This wizard will walk you through the steps of creating a new jail." |
|
echo "" |
|
read -r -p "Enter a name for the jail: " JAILMAKER_JAIL_NAME |
|
# Fallback to default value if user pressed enter |
|
JAILMAKER_JAIL_NAME="${JAILMAKER_JAIL_NAME:-${DEBIAN_RELEASE}}" |
|
echo "" |
|
echo "You may pass additional arguments to systemd-nspawn, e.g. to mount directories." |
|
echo "For example: --bind=/mnt/dir1 --bind-ro=/mnt/dir" |
|
echo "This would mount /mnt/dir with read/write access and /mnt/dir with readonly access:" |
|
echo "" |
|
read -r -p "Enter additional arguments for systemd-nspawn: " additional_flags |
|
echo "" |
|
read -r -p "Do you want to install docker inside the jail? [y/N]: " CHOICE |
|
echo "" |
|
case ${CHOICE} in |
|
[yY]*) JAILMAKER_INSTALL_DOCKER=1 ;; |
|
esac |
|
read -r -p "Do you want to access the GPU inside the jail? [y/N]: " CHOICE |
|
echo "" |
|
if [[ "${CHOICE}" == 'y' || "${CHOICE}" == 'Y' ]]; then |
|
JAILMAKER_GPU_PASSTHROUGH=1 |
|
echo "It's possible to use an nvidia GPU if we install the correct driver version." |
|
echo "When a TrueNAS SCALE update changes the nvidia driver version, a driver mismatch will occur." |
|
echo "You'll need to MANUALLY update the nvidia driver inside the jail..." |
|
echo "" |
|
read -r -p "Do you want to install nvidia drivers inside the jail? [y/N]: " CHOICE |
|
echo "" |
|
case ${CHOICE} in |
|
[yY]*) JAILMAKER_INSTALL_NVIDIA=1 ;; |
|
esac |
|
fi |
|
read -r -p "Do you want to see all the steps the jailmaker.sh script takes (debug mode)? [y/N]: " CHOICE |
|
echo "" |
|
case ${CHOICE} in |
|
[yY]*) JAILMAKER_DEBUG=1 ;; |
|
esac |
|
new "${additional_flags}" |
|
} |
|
|
|
case "${1-""}" in |
|
|
|
new) |
|
new "${*:2}" |
|
;; |
|
|
|
'' | wizard) |
|
wizard |
|
;; |
|
|
|
up) |
|
up |
|
;; |
|
|
|
__do_not_run_this_manually__) |
|
complete_install_inside_jail |
|
;; |
|
|
|
*) |
|
echo "${USAGE}" |
|
;; |
|
esac |
Thank you Sir and thank you for the wonderful script. I finally got everything talking to one another (Internet, apps). I am obviously doing something wrong with portainer. I never had an issue using the docker-compose truechart and pointing it at a docker-compose.yml file. I can't see how I would use that approach here. I am at a complete loss, which saddens me as I am sure this system seems to work swimmingly for everyone else. And sadly how to install and access portainer is not within the scope of this gist.