aboutsummaryrefslogtreecommitdiffstats
path: root/roles/jails
diff options
context:
space:
mode:
authorJan Tuomi <jan@jantuomi.fi>2026-07-15 13:18:27 +0300
committerJan Tuomi <jan@jantuomi.fi>2026-07-15 13:18:37 +0300
commit6524107008a601fa747cf9015c297a7e9303954e (patch)
tree71d99c5ffb2c9a40f4b09bf71a0da576d74c2da0 /roles/jails
parente3d5ce408d4bedde047819cb0f0f8bee19dbf2d3 (diff)
Set up immich jail
Diffstat (limited to 'roles/jails')
-rw-r--r--roles/jails/20_immich/defaults/main.yml27
-rw-r--r--roles/jails/20_immich/handlers/main.yml4
-rw-r--r--roles/jails/20_immich/tasks/main.yml131
-rw-r--r--roles/jails/20_immich/templates/jail.conf.j248
-rw-r--r--roles/jails/20_immich/templates/usr_local_bin_extract_image.sh47
-rw-r--r--roles/jails/20_immich/templates/usr_local_etc_immich_ml.env.j24
-rw-r--r--roles/jails/20_immich/templates/usr_local_etc_immich_server.env.j220
-rw-r--r--roles/jails/20_immich/templates/usr_local_etc_rc.d_immich_ml57
-rw-r--r--roles/jails/20_immich/templates/usr_local_etc_rc.d_immich_server54
9 files changed, 392 insertions, 0 deletions
diff --git a/roles/jails/20_immich/defaults/main.yml b/roles/jails/20_immich/defaults/main.yml
new file mode 100644
index 0000000..cb0bd35
--- /dev/null
+++ b/roles/jails/20_immich/defaults/main.yml
@@ -0,0 +1,27 @@
+userland: "15.0-RELEASE"
+immich_version: "v2.4.1"
+
+oci_images:
+ - name: immich-server
+ image: "ghcr.io/immich-app/immich-server:{{ immich_version }}"
+ check_path: /usr/src/app/server/dist/main.js
+ mounts:
+ - { type: nullfs, src: immich_data, dst: /data }
+ - name: immich-ml
+ image: "ghcr.io/immich-app/immich-machine-learning:{{ immich_version }}"
+ check_path: /opt/venv/bin/python
+ mounts:
+ - { type: nullfs, src: model-cache, dst: /cache }
+
+zfs:
+ - name: zroot/jails/volumes/immich_data
+ mountpoint: /immich_data
+ mode: "0755"
+ - name: zroot/jails/volumes/immich_model_cache
+ mountpoint: /model_cache
+ mode: "0755"
+
+pkg:
+ - skopeo
+
+services: []
diff --git a/roles/jails/20_immich/handlers/main.yml b/roles/jails/20_immich/handlers/main.yml
new file mode 100644
index 0000000..01e17dd
--- /dev/null
+++ b/roles/jails/20_immich/handlers/main.yml
@@ -0,0 +1,4 @@
+- name: Restart immich services
+ shell: |
+ service immich_server status && service immich_server restart || true
+ service immich_ml status && service immich_ml restart || true
diff --git a/roles/jails/20_immich/tasks/main.yml b/roles/jails/20_immich/tasks/main.yml
new file mode 100644
index 0000000..835cd50
--- /dev/null
+++ b/roles/jails/20_immich/tasks/main.yml
@@ -0,0 +1,131 @@
+- import_role:
+ name: jail
+ tasks_from: jail_setup
+
+# Override jail.conf with immich-specific version
+- name: Deploy jail.conf.d/immich.conf
+ template:
+ src: "{{ jail_role_dir }}/templates/jail.conf.j2"
+ dest: /etc/jail.conf.d/immich.conf
+ owner: root
+ group: wheel
+ mode: "0644"
+ delegate_to: "{{ jail_delegate_host }}"
+
+# Deploy extraction script
+- name: Deploy extract-image script
+ template:
+ src: "{{ jail_role_dir }}/templates/usr_local_bin_extract_image.sh"
+ dest: /usr/local/bin/extract-image.sh
+ owner: root
+ group: wheel
+ mode: "0755"
+
+# Create image directory
+- name: Create /image directory
+ file:
+ path: /image
+ state: directory
+ owner: root
+ group: wheel
+ mode: "0755"
+
+# Extract OCI images
+- name: Check if images are extracted
+ stat:
+ path: "/image/{{ item.name }}{{ item.check_path }}"
+ loop: "{{ oci_images }}"
+ loop_control:
+ label: "{{ item.name }}"
+ register: _images_extracted
+
+- name: Extract OCI images
+ shell: "/usr/local/bin/extract-image.sh {{ item.item.image }} {{ item.item.name }}"
+ loop: "{{ _images_extracted.results }}"
+ loop_control:
+ label: "{{ item.item.name }}"
+ when: not item.stat.exists
+
+# Create mount points in each rootfs
+- name: Create base mount points in image rootfs
+ file:
+ path: "/image/{{ item[0].name }}/{{ item[1] }}"
+ state: directory
+ owner: root
+ group: wheel
+ mode: "0755"
+ loop: "{{ oci_images | product(['proc', 'sys', 'dev', 'tmp', 'run']) | list }}"
+ loop_control:
+ label: "{{ item[0].name }}/{{ item[1] }}"
+
+- name: Create nullfs mount points in image rootfs
+ file:
+ path: "/image/{{ item.0.name }}{{ item.1.dst }}"
+ state: directory
+ owner: root
+ group: wheel
+ mode: "0755"
+ loop: "{{ oci_images | subelements('mounts', skip_missing=True) }}"
+ loop_control:
+ label: "{{ item.0.name }}:{{ item.1.dst }}"
+
+# Deploy resolv.conf into each rootfs
+- name: Deploy resolv.conf into image rootfs
+ copy:
+ content: "nameserver {{ lan_ipv4_gateway }}\n"
+ dest: "/image/{{ item.name }}/etc/resolv.conf"
+ owner: root
+ group: wheel
+ mode: "0644"
+ loop: "{{ oci_images }}"
+ loop_control:
+ label: "{{ item.name }}"
+
+# Deploy rc.d scripts
+- name: Deploy immich_server rc.d script
+ template:
+ src: "{{ jail_role_dir }}/templates/usr_local_etc_rc.d_immich_server"
+ dest: /usr/local/etc/rc.d/immich_server
+ owner: root
+ group: wheel
+ mode: "0755"
+
+- name: Deploy immich_ml rc.d script
+ template:
+ src: "{{ jail_role_dir }}/templates/usr_local_etc_rc.d_immich_ml"
+ dest: /usr/local/etc/rc.d/immich_ml
+ owner: root
+ group: wheel
+ mode: "0755"
+
+# Deploy env files
+- name: Deploy immich_server.env
+ template:
+ src: "{{ jail_role_dir }}/templates/usr_local_etc_immich_server.env.j2"
+ dest: /usr/local/etc/immich_server.env
+ owner: root
+ group: wheel
+ mode: "0600"
+ notify: Restart immich services
+
+- name: Deploy immich_ml.env
+ template:
+ src: "{{ jail_role_dir }}/templates/usr_local_etc_immich_ml.env.j2"
+ dest: /usr/local/etc/immich_ml.env
+ owner: root
+ group: wheel
+ mode: "0600"
+ notify: Restart immich services
+
+# Enable services
+- name: Enable immich services
+ community.general.sysrc:
+ name: "{{ item }}_enable"
+ value: "YES"
+ loop:
+ - immich_server
+ - immich_ml
+
+- import_role:
+ name: jail
+ tasks_from: jail_launch
diff --git a/roles/jails/20_immich/templates/jail.conf.j2 b/roles/jails/20_immich/templates/jail.conf.j2
new file mode 100644
index 0000000..482737f
--- /dev/null
+++ b/roles/jails/20_immich/templates/jail.conf.j2
@@ -0,0 +1,48 @@
+immich {
+ vnet;
+ persist;
+ exec.clean;
+ allow.raw_sockets;
+ mount.devfs;
+ allow.mount;
+ allow.mount.devfs;
+ allow.mount.linprocfs;
+ allow.mount.linsysfs;
+ allow.mount.tmpfs;
+ allow.mount.nullfs;
+ enforce_statfs = 1;
+
+ devfs_ruleset = 4;
+ host.hostname = "immich";
+ path = "/usr/local/jails/containers/immich";
+
+ exec.start = "/bin/sh /etc/rc";
+ exec.stop = "/bin/sh /etc/rc.shutdown";
+
+ # LAN epair
+ exec.prestart += "ifconfig epl{{ jail_num }}a destroy 2>/dev/null || true";
+ exec.prestart += "ifconfig epair{{ jail_num }}000 create";
+ exec.prestart += "ifconfig epair{{ jail_num }}000a name epl{{ jail_num }}a";
+ exec.prestart += "ifconfig epair{{ jail_num }}000b name epl{{ jail_num }}b";
+ exec.prestart += "ifconfig epl{{ jail_num }}b ether random";
+ exec.prestart += "ifconfig brlan0 addm epl{{ jail_num }}a";
+ exec.poststart += "ifconfig epl{{ jail_num }}b vnet immich";
+ exec.poststart += "ifconfig epl{{ jail_num }}a up";
+ exec.poststart += "jexec immich ifconfig epl{{ jail_num }}b up";
+ exec.poststart += "jexec immich ifconfig epl{{ jail_num }}b {{ jail_lan_cidr | ipv4_nth_cidr(jail_num | int + jail_lan_offset | int) }}";
+ exec.poststart += "jexec immich route delete default || true";
+ exec.poststart += "jexec immich route add default {{ ingress_ip }} || true";
+ exec.poststop += "ifconfig epl{{ jail_num }}a destroy 2>/dev/null || true";
+{% for img in oci_images %}
+
+ # Mounts for {{ img.name }} rootfs
+ mount += "linprocfs $path/image/{{ img.name }}/proc linprocfs rw 0 0";
+ mount += "linsysfs $path/image/{{ img.name }}/sys linsysfs rw 0 0";
+ mount += "devfs $path/image/{{ img.name }}/dev devfs rw 0 0";
+ mount += "tmpfs $path/image/{{ img.name }}/tmp tmpfs rw 0 0";
+ mount += "tmpfs $path/image/{{ img.name }}/run tmpfs rw 0 0";
+{% for mnt in img.mounts | default([]) %}
+ mount += "$path/{{ mnt.src }} $path/image/{{ img.name }}{{ mnt.dst }} nullfs rw 0 0";
+{% endfor %}
+{% endfor %}
+}
diff --git a/roles/jails/20_immich/templates/usr_local_bin_extract_image.sh b/roles/jails/20_immich/templates/usr_local_bin_extract_image.sh
new file mode 100644
index 0000000..e8226dd
--- /dev/null
+++ b/roles/jails/20_immich/templates/usr_local_bin_extract_image.sh
@@ -0,0 +1,47 @@
+#!/bin/sh
+# Extract a Docker image to a directory under /image/
+# Usage: extract-image.sh <image-ref> <name>
+# Example: extract-image.sh ghcr.io/immich-app/immich-server:v2.4.1 immich-server
+
+set -eu
+
+IMAGE="$1"
+NAME="$2"
+IMAGE_DIR="/image"
+TARGET="${IMAGE_DIR}/${NAME}"
+TMP_DIR="/tmp/oci-${NAME}"
+
+echo "Extracting ${IMAGE} to ${TARGET}..."
+
+# Clean up any previous extraction attempt
+rm -rf "${TMP_DIR}"
+mkdir -p "${TMP_DIR}"
+
+# Pull image layers
+skopeo copy --override-os linux "docker://${IMAGE}" "dir:${TMP_DIR}"
+
+# Extract all layers in order into new rootfs
+rm -rf "${TARGET}.new"
+mkdir -p "${TARGET}.new"
+
+# Parse layer digests from manifest and extract each layer
+grep -o '"sha256:[a-f0-9]*"' "${TMP_DIR}/manifest.json" | \
+ sed 's/"//g; s/sha256://' | \
+ while read hash; do
+ if [ -f "${TMP_DIR}/${hash}" ]; then
+ echo " Extracting layer ${hash}..."
+ tar -xzf "${TMP_DIR}/${hash}" -C "${TARGET}.new" 2>/dev/null || \
+ tar -xf "${TMP_DIR}/${hash}" -C "${TARGET}.new" 2>/dev/null || true
+ fi
+ done
+
+# Swap in the new rootfs
+if [ -d "${TARGET}" ]; then
+ mv "${TARGET}" "${TARGET}.old"
+fi
+mv "${TARGET}.new" "${TARGET}"
+
+# Cleanup
+rm -rf "${TMP_DIR}" "${TARGET}.old"
+
+echo "Done: ${TARGET}"
diff --git a/roles/jails/20_immich/templates/usr_local_etc_immich_ml.env.j2 b/roles/jails/20_immich/templates/usr_local_etc_immich_ml.env.j2
new file mode 100644
index 0000000..f2c14a7
--- /dev/null
+++ b/roles/jails/20_immich/templates/usr_local_etc_immich_ml.env.j2
@@ -0,0 +1,4 @@
+# Immich machine learning configuration
+TZ=Europe/Helsinki
+IMMICH_PORT=3003
+MACHINE_LEARNING_CACHE_FOLDER=/model_cache
diff --git a/roles/jails/20_immich/templates/usr_local_etc_immich_server.env.j2 b/roles/jails/20_immich/templates/usr_local_etc_immich_server.env.j2
new file mode 100644
index 0000000..87c563d
--- /dev/null
+++ b/roles/jails/20_immich/templates/usr_local_etc_immich_server.env.j2
@@ -0,0 +1,20 @@
+# Immich server configuration
+IMMICH_MEDIA_LOCATION=/data
+TZ=Europe/Helsinki
+IMMICH_PORT=80
+NODE_ENV=production
+
+# Database
+DB_HOSTNAME={{ jail_lan_cidr | ipv4_nth(2 + jail_lan_offset | int) }}
+DB_PORT=5432
+DB_USERNAME=immich
+DB_PASSWORD={{ pg_user_passwords.immich }}
+DB_DATABASE_NAME=immich
+
+# Redis
+REDIS_HOSTNAME={{ jail_lan_cidr | ipv4_nth(9 + jail_lan_offset | int) }}
+REDIS_PORT=6379
+REDIS_PASSWORD={{ valkey_password }}
+
+# Machine learning
+MACHINE_LEARNING_URL=http://127.0.0.1:3003
diff --git a/roles/jails/20_immich/templates/usr_local_etc_rc.d_immich_ml b/roles/jails/20_immich/templates/usr_local_etc_rc.d_immich_ml
new file mode 100644
index 0000000..d20e4a6
--- /dev/null
+++ b/roles/jails/20_immich/templates/usr_local_etc_rc.d_immich_ml
@@ -0,0 +1,57 @@
+#!/bin/sh
+
+# PROVIDE: immich_ml
+# REQUIRE: NETWORKING
+# KEYWORD: shutdown
+
+. /etc/rc.subr
+
+name="immich_ml"
+rcvar="${name}_enable"
+pidfile="/var/run/${name}.pid"
+logfile="/var/log/${name}.log"
+
+load_rc_config $name
+: ${immich_ml_enable:="NO"}
+: ${immich_ml_envfile:="/usr/local/etc/immich_ml.env"}
+: ${immich_ml_root:="/image/immich-ml"}
+
+start_cmd="${name}_start"
+stop_cmd="${name}_stop"
+status_cmd="${name}_status"
+
+immich_ml_start() {
+ echo "Starting ${name}."
+ set -a
+ . ${immich_ml_envfile}
+ set +a
+ export PATH=/opt/venv/bin:/usr/local/bin:/usr/bin:/bin
+ export VIRTUAL_ENV=/opt/venv
+ export PYTHONDONTWRITEBYTECODE=1
+ export PYTHONUNBUFFERED=1
+ export PYTHONPATH=/usr/src
+ /usr/sbin/daemon -P ${pidfile} -o ${logfile} \
+ /usr/sbin/chroot ${immich_ml_root} \
+ /opt/venv/bin/python -m immich_ml
+}
+
+immich_ml_stop() {
+ if [ -f ${pidfile} ]; then
+ echo "Stopping ${name}."
+ kill $(cat ${pidfile}) 2>/dev/null
+ rm -f ${pidfile}
+ else
+ echo "${name} is not running."
+ fi
+}
+
+immich_ml_status() {
+ if [ -f ${pidfile} ] && kill -0 $(cat ${pidfile}) 2>/dev/null; then
+ echo "${name} is running as pid $(cat ${pidfile})."
+ else
+ echo "${name} is not running."
+ return 1
+ fi
+}
+
+run_rc_command "$1"
diff --git a/roles/jails/20_immich/templates/usr_local_etc_rc.d_immich_server b/roles/jails/20_immich/templates/usr_local_etc_rc.d_immich_server
new file mode 100644
index 0000000..42946cc
--- /dev/null
+++ b/roles/jails/20_immich/templates/usr_local_etc_rc.d_immich_server
@@ -0,0 +1,54 @@
+#!/bin/sh
+
+# PROVIDE: immich_server
+# REQUIRE: NETWORKING
+# KEYWORD: shutdown
+
+. /etc/rc.subr
+
+name="immich_server"
+rcvar="${name}_enable"
+pidfile="/var/run/${name}.pid"
+logfile="/var/log/${name}.log"
+
+load_rc_config $name
+: ${immich_server_enable:="NO"}
+: ${immich_server_envfile:="/usr/local/etc/immich_server.env"}
+: ${immich_server_root:="/image/immich-server"}
+
+start_cmd="${name}_start"
+stop_cmd="${name}_stop"
+status_cmd="${name}_status"
+
+immich_server_start() {
+ echo "Starting ${name}."
+ set -a
+ . ${immich_server_envfile}
+ set +a
+ export PATH=/usr/local/bin:/usr/bin:/bin
+ export NODE_ENV=production
+ /usr/sbin/daemon -P ${pidfile} -o ${logfile} \
+ /usr/sbin/chroot ${immich_server_root} \
+ /usr/local/bin/node /usr/src/app/server/dist/main.js
+}
+
+immich_server_stop() {
+ if [ -f ${pidfile} ]; then
+ echo "Stopping ${name}."
+ kill $(cat ${pidfile}) 2>/dev/null
+ rm -f ${pidfile}
+ else
+ echo "${name} is not running."
+ fi
+}
+
+immich_server_status() {
+ if [ -f ${pidfile} ] && kill -0 $(cat ${pidfile}) 2>/dev/null; then
+ echo "${name} is running as pid $(cat ${pidfile})."
+ else
+ echo "${name} is not running."
+ return 1
+ fi
+}
+
+run_rc_command "$1"