Last active 2 days ago

manual.md Raw

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

$ 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

# 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 netcat-openbsd
# 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

Step 2: 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

#!/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.

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

$ rm -rf /tmp/* ~/.wget-hsts ~/.pnpm ~/.local/share/pnpm ~/.cache
$ exit

While still being in the chroot, create the file /sbin/swinit

#!/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)

#!/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

#!/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

# umount immich/proc
# tar -C immich -czf immich.tar.gz .

Send it to your FreeBSD server, with SFTP:

$ sftp <ip to server>
sftp> put immich.tar.gz

Step 3. Installation

At this point, you should be using your FreeBSD server for this.

Install dependencies

# 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

# service gateway start
# ifconfig bridge0 inet 192.168.123.1/24 description jailnet up
# service pf start

Compile VectorChord

# 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

# 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
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
# sudo -u postgres psql -U immich
CREATE EXTENSION IF NOT EXISTS vchord CASCADE;
\q

Unpack Devuan rootfs

# 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

# service jail start

And if set up correctly, you should be able to access Immich!

Example NGINX config for reverse proxying immich

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 has the power to run a whole chroot rootfs and seems to be good enough to flawlessly run Immich.