Yes, this is not clickbait! I set up Immich on FreeBSD yesterday without going insane, so I am documenting how I did this magic. I am not using ML in this setup. Note: I did steal some documentation and read the install script from https://github.com/arter97/immich-native ## Requirements - A FreeBSD server (duh, tested in FreeBSD 15) - A Linux machine (for bootstrapping and building) - SSH connection between the two machines (or any other remote access) Note: Do know the distinct difference on the shell prefixes of each code block, `$` = normal user recommended, `#` = root/sudo required # Step 1: Bootstrapping First, get your Linux machine to install `debootstrap`, it's available on any Debian-based distro, and some other distros Debian/Ubuntu: ``` # apt install debootstrap ``` Alpine Linux: ``` # apk add debootstrap ``` Then, create a Devuan rootfs ```shell-session $ mkdir immich # debootstrap --arch=amd64 excalibur immich https://deb.devuan.org/merged trixie ``` **The error about cron depending on systemd is not fatal** Chroot into the new rootfs ```shell-session # chroot immich ``` **At this point, you should be running commands on the chroot shell and _not your host._** Debloat and update ``` # echo "nameserver 9.9.9.9" > /etc/resolv.conf # apt autoremove cron* sysvinit-core # apt update # apt upgrade ``` Install your favorite TUI text editor Install dependencies ``` # apt install -y curl wget valkey-server ca-certificates sudo git # install -d /usr/share/postgresql-common/pgdg # curl -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc --fail https://www.postgresql.org/media/keys/ACCC4CF8.asc # echo 'deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt trixie-pgdg main' > /etc/apt/sources.list.d/pgdg.list # curl -fsSL https://deb.nodesource.com/setup_22.x | bash - # curl -s https://repo.jellyfin.org/install-debuntu.sh | bash NOTE: Because Jellyfin doesn't recognize Devuan, type distro debian with version trixie NOTE: At this part, CTRL+C immediately when it begins to install jellyfin. # apt update # apt --no-install-recommends install postgresql-client jellyfin-ffmpeg7 nodejs python3-pip python3-venv python3-dev uuid-runtime autoconf build-essential unzip jq perl libnet-ssleay-perl libio-socket-ssl-perl libcapture-tiny-perl libfile-which-perl libfile-chdir-perl libpkgconfig-perl libffi-checklib-perl libtest-warnings-perl libtest-fatal-perl libtest-needs-perl libtest2-suite-perl libsort-versions-perl libpath-tiny-perl libtry-tiny-perl libterm-table-perl libany-uri-escape-perl libmojolicious-perl libfile-slurper-perl liblcms2-2 libgl1 # npm install corepack@latest -g # corepack enable ``` Adding the user ``` # mkdir -p /var/lib/immich/home # mkdir /var/lib/immich/app # mkdir /var/lib/immich/cache # mkdir -p /var/log/immich # adduser --home /var/lib/immich/home --shell=/sbin/nologin --no-create-home --disabled-password immich # chown -R immich:immich /var/lib/immich /var/log/immich # chmod 700 /var/lib/immich ``` Building (yes, this is a lot of steps) ``` # su immich $ git clone https://github.com/immich-app/immich /tmp/immich --depth=1 -b v2.7.5 $ cd /tmp/immich $ grep -Rl /usr/src | xargs -n1 sed -i -e "s@/usr/src@/var/lib/immich@g" $ grep -RlE "\"/build\"|'/build'" | xargs -n1 sed -i -e "s@\"/build\"@\"/var/lib/immich/app\"@g" -e "s@'/build'@'/var/lib/immich/app'@g" $ corepack use pnpm@latest $ curl -O https://raw.githubusercontent.com/extism/js-pdk/main/install.sh $ sed -i -e 's@sudo@@g' -e "s@/usr/local/binaryen@$HOME/binaryen@g" -e "s@/usr/local/bin@$HOME/.local/bin@g" install.sh $ export PATH="$PATH:/var/lib/immich/home/.local/bin" $ ./install.sh $ rm install.sh $ cd server $ pnpm install --frozen-lockfile --force $ pnpm run build $ pnpm prune --prod --no-optional --config.ci=true $ cd ../open-api/typescript-sdk $ pnpm install --frozen-lockfile --force $ pnpm run build $ cd ../../web $ pnpm install --frozen-lockfile --force $ pnpm run build $ cd ../plugins $ pnpm install --frozen-lockfile --force $ pnpm run build $ cd .. $ cp -aL server/node_modules server/dist server/bin /var/lib/immich/app/ $ cp -a web/build /var/lib/immich/app/www $ cp -a server/package.json pnpm-lock.yaml /var/lib/immich/app/ $ mkdir -p /var/lib/immich/app/corePlugin $ cp -a plugins/dist /var/lib/immich/app/corePlugin/ $ cp -a plugins/manifest.json /var/lib/immich/app/corePlugin/ $ cp -a LICENSE /var/lib/immich/app/ $ cp -a i18n /var/lib/immich/ $ cd /var/lib/immich/app $ mkdir upload $ pnpm store prune $ pnpm install sharp $ mkdir geodata $ cd geodata $ wget -o - https://download.geonames.org/export/dump/admin1CodesASCII.txt $ wget -o - https://download.geonames.org/export/dump/admin2Codes.txt $ wget -o - https://download.geonames.org/export/dump/cities500.zip $ wget -o - https://raw.githubusercontent.com/nvkelso/natural-earth-vector/v5.1.2/geojson/ne_10m_admin_0_countries.geojson $ unzip cities500.zip $ date --iso-8601=seconds | tr -d "\n" > geodata-date.txt $ rm cities500.zip ``` You're almost done, create this file at `/var/lib/immich/app/start.sh` ```shell #!/bin/bash export PATH=/usr/lib/jellyfin-ffmpeg:/var/lib/immich/home/.local/bin:$PATH set -a . /var/lib/immich/env set +a cd $APP exec node /var/lib/immich/app/dist/main "$@" ``` And this at `/var/lib/immich/env` Generate a random password in place of YOUR_STRONG_RANDOM_PW and remember it for database setup. ```shell DB_PASSWORD=YOUR_STRONG_RANDOM_PW NODE_ENV=production DB_USERNAME=immich DB_DATABASE_NAME=immich UPLOAD_LOCATION=./library IMMICH_VERSION=release IMMICH_HOST=0.0.0.0 IMMICH_PORT=2283 DB_HOSTNAME=192.168.123.1 REDIS_HOSTNAME=127.0.0.1 ``` Cleanup ```shell-session $ rm -rf /tmp/* ~/.wget-hsts ~/.pnpm ~/.local/share/pnpm ~/.cache $ exit ``` While still being in the chroot, create the file `/sbin/swinit` ```shell #!/bin/bash set -x /rescue/ifconfig lo0 inet 127.0.0.1 up /rescue/ifconfig epair0b inet 192.168.123.2/24 up /rescue/route add default 192.168.123.1 nohup /usr/sbin/swinit-init & ``` Create `/sbin/swinit-init` (this is a tiny and WiP supervisor) ```shell #!/bin/sh echo Waiting for PostgreSQL to start on FreeBSD... while :; do if nc -z 192.168.123.1 5432 ; then break fi sleep 2 done echo Starting Valkey. sudo -u valkey valkey-server /etc/valkey/valkey.conf sleep 1 echo Starting Immich. sudo -u immich /var/lib/immich/app/start.sh & IMMICH_PID=$! sleep inf ``` And then create `/sbin/swshutdown` ```shell #!/bin/sh pkill valkey-server pkill node true ``` Then finish up ``` # chmod +x /sbin/swinit /sbin/swshutdown /sbin/swinit-init # exit ``` **At this point, you should be running commands on the _host_ again.** Pack the Immich chroot ```shell-session # umount immich/proc # tar -C immich -czf immich.tar.gz . ``` Send it to your FreeBSD server, with SFTP: ``` $ sftp sftp> put immich.tar.gz ``` # 3. Installation **At this point, you should be using your FreeBSD server for this.** Install dependencies ```shell-session # pkg install postgresql18-server postgresql18-contrib postgresql18-pgvector sudo # sysrc gateway_enable="YES" # sysrc pf_enable="YES" # sysrc pf_rules="/etc/pf.conf" # sysrc cloned_interfaces="bridge1" # sysrc ifconfig_bridge1="inet 192.168.123.1/24 description jailnet up" # sysrc linux_enable="YES" # sysrc jail_enable="YES" # sysrc postgresql_enable="YES" # service linux start # echo 'if_epair_load="YES"' >> /boot/loader.conf # echo 'if_bridge_load="YES"' >> /boot/loader.conf # echo 'pf_load="YES"' >> /boot/loader.conf # echo 'nmdm_load="YES"' >> /boot/loader.conf # kldload if_epair # kldload if_bridge # kldload pf # kldload nmdm ``` Create this at `/etc/pf.conf`, replace `em0` with your main internet-accessible interface ``` ext_if = "em0" jail_net = "192.168.123.0/24" nat on $ext_if from $jail_net to any -> ($ext_if) pass in all pass out all ``` Then start the networking stuff ```shell-session # service gateway start # ifconfig bridge0 inet 192.168.123.1/24 description jailnet up # service pf start ``` Compile VectorChord ```shell-session # pkg install git rust llvm21 curl # curl -fsSL https://github.com/tensorchord/VectorChord/archive/refs/tags/1.1.1.tar.gz | tar -xz # cd VectorChord-1.1.1 # make build PG_CONFIG="$(which pg_config)" # cp build/sharedir/extension/* /usr/local/share/postgresql/extension/ # cp build/pkglibdir/* /usr/local/lib/postgresql/ # cd .. ``` Setup database ```shell-session # service postgresql initdb # service postgresql start # sudo -u postgres psql postgres=# ALTER SYSTEM SET shared_preload_libraries = "vchord"; postgres=# \q # service postgresql restart # sudo -u postgres psql ``` ```sql CREATE USER immich WITH ENCRYPTED PASSWORD 'YOUR_STRONG_RANDOM_PW'; CREATE DATABASE immich OWNER immich; ALTER USER immich WITH SUPERUSER; CREATE EXTENSION IF NOT EXISTS vchord CASCADE; \q ``` ```shell-session # sudo -u postgres psql -U immich ``` ```sql CREATE EXTENSION IF NOT EXISTS vchord CASCADE; \q ``` Unpack Devuan rootfs ```shell-session # mkdir -p /usr/jail/immich # tar -C /usr/jail/immich -xzf immich.tar.gz ``` Create `/etc/jail.conf` Note that this a compacted version of my own setup, which also has FreeBSD jails, so it's a bit messy. ``` # Global parameters exec.start = "/bin/sh /etc/rc"; exec.stop = "/bin/sh /etc/rc.shutdown"; exec.clean; mount.devfs; allow.raw_sockets; allow.sysvipc; # VNET-specific parameters vnet; vnet.interface = "epair${nid}b"; exec.prestart = "ifconfig epair${nid} create up"; exec.prestart += "ifconfig epair${nid}a up"; exec.prestart += "ifconfig bridge1 addm epair${nid}a"; exec.poststop = "ifconfig epair${nid}a destroy"; # Path parameters path = "/usr/jail/${name}"; host.hostname = "${name}.jail"; immich { # This is a Linux jail. # If you want to use external storage, uncomment the two and replace /mnt/hdds/immich with whatever path $nid = 0; allow.mount; exec.prestart += "mount -t linprocfs linprocfs /usr/jail/immich/proc"; exec.prestart += "mount -t linsysfs linsysfs /usr/jail/immich/sys"; exec.prestart += "mount -t tmpfs tmpfs /usr/jail/immich/tmp"; exec.prestart += "mount -t nullfs /rescue /usr/jail/immich/rescue"; # exec.prestart += "mount -t nullfs /mnt/hdds/immich /usr/jail/immich/var/lib/immich/app/upload"; exec.poststop += "umount /usr/jail/immich/proc"; exec.poststop += "umount /usr/jail/immich/sys"; exec.poststop += "umount /usr/jail/immich/tmp"; exec.poststop += "umount /usr/jail/immich/rescue"; # exec.poststop += "umount /usr/jail/immich/var/lib/immich/app/upload"; exec.start = "/bin/sh /sbin/swinit"; exec.stop = "/bin/sh /sbin/swshutdown"; } ``` Start jail ```shell-session # service jail start ``` And if set up correctly, you should be able to access Immich! Example NGINX config for reverse proxying immich ```nginx server { listen 80; listen [::]:80; listen 443 ssl; listen [::]:443 ssl; listen 443 quic; listen [::]:443 quic; server_name immich.example.com; client_max_body_size 50000M; proxy_request_buffering off; client_body_buffer_size 1024k; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_http_version 1.1; proxy_redirect off; proxy_read_timeout 600s; proxy_send_timeout 600s; send_timeout 600s; location / { proxy_pass http://192.168.123.2:2283; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } ssl_certificate /etc/letsencrypt/live/immich.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/immich.example.com/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; } ``` # What the hell was that? Yep, you did a very cursed but possible thing in FreeBSD using Linuxulator, FreeBSD's decent Linux compatibility layer, it seems to be good enough to flawlessly run Immich.