diff options
| author | Jan Tuomi <jan@jantuomi.fi> | 2026-07-15 13:18:27 +0300 |
|---|---|---|
| committer | Jan Tuomi <jan@jantuomi.fi> | 2026-07-15 13:18:37 +0300 |
| commit | 6524107008a601fa747cf9015c297a7e9303954e (patch) | |
| tree | 71d99c5ffb2c9a40f4b09bf71a0da576d74c2da0 /roles/jails/20_immich | |
| parent | e3d5ce408d4bedde047819cb0f0f8bee19dbf2d3 (diff) | |
Set up immich jail
Diffstat (limited to 'roles/jails/20_immich')
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" |
