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.