Installing Immich on FreeBSD

Immich is a self hosted manager for photos and videos, with apps for Android and iOS, face recognition, full text search inside images and sharing links.

Four ports have recently landed in the tree, so the service now runs natively, with rc scripts, newsyslog and pkg updates like any other server daemon:

  • www/immich, server and web interface
  • www/immich-ml, machine learning for search, faces and text recognition
  • www/immich-cli, command line tool for bulk imports
  • www/immich-public-proxy, a slim proxy for public shares only

This guide walks the whole way, from the empty machine to the first uploaded picture. Every command here has been tested.

Requirements

A 64 bit machine, so amd64 or aarch64. The ports are restricted to these architectures on purpose, on i386 there is neither a working ONNX runtime nor the native Node modules this needs.

Plan for plenty of memory. Server and learning service keep their models and caches in RAM, and image processing runs in parallel. A tight VM is the most common reason for jobs that die halfway through.

The media files need a lot of room somewhere you can grow later. A dedicated ZFS dataset is the most convenient choice.

Step 1: PostgreSQL with pgvector and VectorChord

Immich stores its search vectors in the database and needs two extensions for that. Both are available as ports:

pkg install postgresql18-server postgresql18-contrib \
    postgresql18-pgvector postgresql18-vchord
sysrc postgresql_enable=YES
service postgresql initdb

VectorChord has to be preloaded by the server. Add this to /var/db/postgres/data18/postgresql.conf:

shared_preload_libraries = 'vchord.so'

Then start it:

service postgresql start

Step 2: Create database and role

Role and database belong to Immich alone. The password goes into the configuration file later, so pick a long one:

su -l postgres -c "createuser -P immich"
su -l postgres -c "createdb -O immich immich"

The superuser creates the extensions inside that database. Doing it here means the immich role never needs superuser rights:

su -l postgres -c "psql -d immich -c 'CREATE EXTENSION vector'"
su -l postgres -c "psql -d immich -c 'CREATE EXTENSION vchord CASCADE'"
su -l postgres -c "psql -d immich -c 'CREATE EXTENSION cube'"
su -l postgres -c "psql -d immich -c 'CREATE EXTENSION earthdistance'"
su -l postgres -c "psql -d immich -c 'CREATE EXTENSION pg_trgm'"

This is where most installations get stuck. Immich runs schema migrations on startup, and those expect the extensions to be there. The alternative is to grant the role SUPERUSER temporarily and take it away afterwards, but the way above is cleaner.

Step 3: Set up the cache

Immich expects a Redis compatible cache. Valkey does the job:

pkg install valkey
sysrc valkey_enable=YES
service valkey start

Step 4: Install and configure Immich

pkg install immich

The connection settings go into a file of their own, and a template ships with the port:

cp /usr/local/etc/immich.env.sample /usr/local/etc/immich.env
chown root:wheel /usr/local/etc/immich.env
chmod 600 /usr/local/etc/immich.env

Adjust the contents, the password is the one from step 2:

DB_HOSTNAME=localhost
DB_USERNAME=immich
DB_DATABASE_NAME=immich
DB_PASSWORD=<your password>
REDIS_HOSTNAME=localhost

The rc script reads this file as root, the service user needs no access to it. That is why it is mode 0600 and owned by root.

Step 5: Start the service

The server listens on all addresses out of the box, and its worker opens a second port on a random number as well. Bind it to the loopback address and put a reverse proxy in front of it:

sysrc immich_server_enable=YES
sysrc immich_server_host=127.0.0.1
service immich_server start

If the media directory should live elsewhere, for instance on a dataset of its own:

sysrc immich_server_media_location=/tank/immich

The log tells you whether the migrations went through:

tail -f /var/log/immich/immich_server.log

After that the interface answers on port 2283. The first visit asks for the administrator account.

Set one option before the first import: under Administration > Settings > Storage Template, enable a scheme, by year and month for example. Without it Immich stores the files under random names, and the library is unreadable without the database. It can be changed later, but then Immich has to move the entire collection.

Step 6: Machine learning

Without this service there is no full text search in images, no face recognition, no duplicate detection and no text recognition:

pkg install immich-ml
sysrc immich_ml_enable=YES
service immich_ml start

The service listens on the loopback address, port 3003, and the server’s rc script points there by itself. Check in the interface under Administration > Settings > Machine Learning that it really says http://127.0.0.1:3003. The default inherited from the container world is http://immich-machine-learning:3003, and as long as that is in there, nothing happens at all.

The models are downloaded on first use and stored under /var/db/immich-ml. Inference runs on the CPU, there is no GPU acceleration on FreeBSD because onnxruntime does not support it here. For a moderate collection that is fine, the first pass over a large library takes patience.

Details like the number of workers or how long models stay loaded belong in /usr/local/etc/immich-ml.env.

Step 7: Reverse proxy with TLS

Immich brings no TLS of its own, and the apps want an encrypted connection. An example for nginx:

server {
    listen 443 ssl;
    server_name photos.example.org;

    ssl_certificate     /usr/local/etc/letsencrypt/live/photos.example.org/fullchain.pem;
    ssl_certificate_key /usr/local/etc/letsencrypt/live/photos.example.org/privkey.pem;

    client_max_body_size 50G;
    proxy_read_timeout 600s;
    proxy_send_timeout 600s;

    location / {
        proxy_pass http://127.0.0.1:2283;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        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;
    }
}

Three things are easy to forget on the first try: a generous client_max_body_size, otherwise videos fail to upload; the upgrade headers, otherwise the interface hangs on the loading screen because Immich uses WebSockets; and long timeouts, because large files transfer in one go.

Step 8: Import an existing collection

For the first big import the command line is more convenient than the app:

pkg install immich-cli

Log in with an API key from the interface (Account Settings > API Keys):

immich login https://photos.example.org/api <api key>

Then upload recursively, the server detects duplicates itself:

immich upload --recursive /path/to/your/pictures

With --album the files land in an album right away, with --dry-run you see beforehand what would happen.

Step 9: Public shares without exposing the server

Sending links to family or clients does not require making the whole instance reachable. The public proxy serves only the albums shared inside Immich:

pkg install immich-public-proxy
cp /usr/local/etc/immich-public-proxy.env.sample /usr/local/etc/immich-public-proxy.env

The file holds where the server sits internally and under which address the proxy is reached from outside:

IMMICH_URL=http://127.0.0.1:2283
PUBLIC_BASE_URL=https://photos.example.org
sysrc immich_public_proxy_enable=YES
service immich_public_proxy start

It listens on port 3000 and belongs behind a reverse proxy with TLS just the same. The Immich server itself stays unreachable from the internet.

Running it

Backups. Back up both, the database and the media directory. Immich’s built in database backup writes into a folder below the media location and does not contain the images. Restoring such a dump works like this, and the extensions have to exist before it is loaded:

su -l postgres -c "createdb -O immich immich"
su -l postgres -c "psql -q -d immich -c 'CREATE EXTENSION vector'"
su -l postgres -c "psql -q -d immich -c 'CREATE EXTENSION vchord CASCADE'"
gzip -dc immich-db-backup-<timestamp>.sql.gz | su -l postgres -c "psql -q -d immich"

Upgrading. Immich runs schema migrations automatically on startup. So take a backup first, and going back to an older version is not supported upstream.

After a PostgreSQL major upgrade pgvector and VectorChord have to be rebuilt, otherwise the server does not find its extensions.

After a Node major upgrade rebuild www/immich so the native modules match the runtime version.

Logs live under /var/log/immich and are rotated through /usr/local/etc/newsyslog.conf.d/immich.conf.

Two traps that cost time

HDR video. Immich builds its HDR filter chain around tonemapx, a filter that plain multimedia/ffmpeg does not ship. The port therefore pulls multimedia/jellyfin-ffmpeg and points FFMPEG_PATH and FFPROBE_PATH at it in the rc script. Forcing the regular ffmpeg instead gets you no thumbnail at all for HDR footage, without any error in the interface. A different build goes into immich.env through those same two variables.

The models and the CPU. The first full pass over a grown library keeps the machine busy for a long time. Plan it for a night, and do not take the service for broken just because search finds little at the start.

Schreibe einen Kommentar