Skip to content

Immich VM

Immich is a self-hosted photo and video library, running as a VM on Proxmox. It serves as the primary photo backup and browsing platform, with storage on the OMV ZFS pool.

VM specs: 2 cores, 8 GB RAM (balloon to 4 GB), 60 GB OS disk, Ubuntu Server 24.04, IP 192.168.0.71

Related: Frames LXC

Frames LXC runs ImmichFrame instances for frames that are outside the local network. Those instances connect to the Immich server on this VM (http://192.168.0.71:2283) as their photo source, and are exposed via Tailscale Funnel rather than served directly from this VM. See Part 3 for the local-network frame setup on this VM.


Part 1 — VM Setup

1.1 — Proxmox VM creation

Download Ubuntu Server 24.04 ISO:

https://releases.ubuntu.com/24.04.3/ubuntu-24.04.3-live-server-amd64.iso

Create the VM in Proxmox with these settings:

Setting Value
Name Immich-VM
Machine q35
QEMU Agent enabled
Disk 60 GB (local-lvm)
Cores 2
RAM 8192 MB
CPU x86-64-v2-AES
104.conf
agent: 1
balloon: 4096
boot: order=scsi0;net0
cores: 2
cpu: x86-64-v2-AES
machine: q35
memory: 8192
meta: creation-qemu=10.0.2,ctime=1764475342
name: Immich-VM
net0: virtio=BC:24:11:B8:D1:83,bridge=vmbr0,firewall=1
numa: 0
ostype: l26
scsi0: local-lvm:vm-104-disk-0,iothread=1,size=60G
scsihw: virtio-scsi-single
smbios1: uuid=0555dfb1-1210-4f08-8aa3-4c9039a82c7b
sockets: 1
startup: up=60
vmgenid: 23edc914-6725-4904-9c25-0f15ad45bc94

Boot from ISO, run Ubuntu Server installer:

  • Static IP: 192.168.0.71/24
  • Hostname: immichvm
  • Enable OpenSSH server during install

1.2 — Initial OS setup

apt update && apt upgrade -y
apt install curl -y

Install Docker:

curl -sSL https://get.docker.com | sh

Create a non-root user and disable root login:

adduser jake
adduser jake sudo
adduser jake docker
exit
# Log back in as jake
sudo passwd -l root

Install docker-compose:

sudo apt install docker-compose -y

Verify Docker is running:

docker ps

1.3 — Mount OMV photo storage

The Immich upload library lives on the OMV ZFS pool, mounted via CIFS at /mnt/immich_storage.

Library subdirectories must exist before starting Immich

The mount point must already contain the Immich library subdirectories (library/, thumbs/, encoded-video/, upload/, profile/, backups/). If they are missing, immich-server will restart endlessly. Either create them on the OMV share first, or let Immich populate them on a local path and then copy them over before switching to the network mount.

Install CIFS support and create the mount point:

sudo apt install cifs-utils -y
sudo mkdir -p /mnt/immich_storage

Test the mount manually first:

sudo mount -t cifs //192.168.0.149/local_share/immich_storage/ /mnt/immich_storage \
  -o username=<omv-immich-user>,password=<omv-immich-password>,uid=$(id -u),gid=$(id -g),forceuid,forcegid
df -h   # confirm share is mounted

Make it permanent — add to /etc/fstab:

//192.168.0.149/local_share/immich_storage /mnt/immich_storage cifs username=<omv-immich-user>,password=<omv-immich-password>,uid=1000,gid=1000,iocharset=utf8 0 0

sudo reboot

Part 2 — Immich Installation

immich-docker-compose.yml
#
# WARNING: To install Immich, follow our guide: https://docs.immich.app/install/docker-compose
#
# Make sure to use the docker-compose.yml of the current release:
#
# https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
#
# The compose file on main may not be compatible with the latest release.

name: immich

services:
  immich-server:
    container_name: immich_server
    image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
    # extends:
    #   file: hwaccel.transcoding.yml
    #   service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
    volumes:
      # Do not edit the next line. If you want to change the media storage location on your system, edit the value of UPLOAD_LOCATION in the .env file
      - ${UPLOAD_LOCATION}:/data
      - /etc/localtime:/etc/localtime:ro
    env_file:
      - .env
    ports:
      - '2283:2283'
    depends_on:
      - redis
      - database
    restart: always
    healthcheck:
      disable: false

  immich-machine-learning:
    container_name: immich_machine_learning
    # For hardware acceleration, add one of -[armnn, cuda, rocm, openvino, rknn] to the image tag.
    # Example tag: ${IMMICH_VERSION:-release}-cuda
    image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}
    # extends: # uncomment this section for hardware acceleration - see https://docs.immich.app/features/ml-hardware-acceleration
    #   file: hwaccel.ml.yml
    #   service: cpu # set to one of [armnn, cuda, rocm, openvino, openvino-wsl, rknn] for accelerated inference - use the `-wsl` version for WSL2 where applicable
    volumes:
      - model-cache:/cache
    env_file:
      - .env
    restart: always
    healthcheck:
      disable: false

  redis:
    container_name: immich_redis
    image: docker.io/valkey/valkey:8@sha256:81db6d39e1bba3b3ff32bd3a1b19a6d69690f94a3954ec131277b9a26b95b3aa
    healthcheck:
      test: redis-cli ping || exit 1
    restart: always

  database:
    container_name: immich_postgres
    image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:bcf63357191b76a916ae5eb93464d65c07511da41e3bf7a8416db519b40b1c23
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_USER: ${DB_USERNAME}
      POSTGRES_DB: ${DB_DATABASE_NAME}
      POSTGRES_INITDB_ARGS: '--data-checksums'
      # Uncomment the DB_STORAGE_TYPE: 'HDD' var if your database isn't stored on SSDs
      # DB_STORAGE_TYPE: 'HDD'
    volumes:
      # Do not edit the next line. If you want to change the database storage location on your system, edit the value of DB_DATA_LOCATION in the .env file
      - ${DB_DATA_LOCATION}:/var/lib/postgresql/data
    shm_size: 128mb
    restart: always

volumes:
  model-cache:
immich.env
# You can find documentation for all the supported env variables at https://docs.immich.app/install/environment-variables

# The location where your uploaded files are stored
#UPLOAD_LOCATION=./library
# UPLOAD_LOCATION=/mnt/immich_photos
UPLOAD_LOCATION=/mnt/immich_storage
# The location where your database files are stored. Network shares are not supported for the database
DB_DATA_LOCATION=./postgres

# To set a timezone, uncomment the next line and change Etc/UTC to a TZ identifier from this list: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List
TZ=Etc/MST

# The Immich version to use. You can pin this to a specific version like "v2.1.0"
IMMICH_VERSION=v3.1.0

# Connection secret for postgres. You should change it to a random password
# Please use only the characters `A-Za-z0-9`, without special characters or spaces
DB_PASSWORD=jake

# The values below this line do not need to be changed
###################################################################################
DB_USERNAME=postgres
DB_DATABASE_NAME=immich

2.1 — Deploy with Docker Compose

mkdir ~/immich-app && cd ~/immich-app
wget -O docker-compose.yml https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
wget -O .env https://github.com/immich-app/immich/releases/latest/download/example.env
nano .env

Key values to set in .env:

Variable Value Notes
UPLOAD_LOCATION /mnt/immich_storage Path to the CIFS-mounted OMV share
DB_DATA_LOCATION ./postgres Local path — network shares not supported for the database
TZ Etc/MST Uncomment and set timezone
DB_PASSWORD (set a strong password) Use only A-Za-z0-9, no special characters
IMMICH_VERSION v3.1.0 Pin to a specific release — do not leave as release

Pin the version

Set IMMICH_VERSION to a specific release tag (e.g. v3.1.0) rather than leaving it as release. Immich releases frequently and occasionally includes breaking changes — pinning gives you control over when to upgrade.

Start Immich:

sudo docker compose up -d

Browse to http://192.168.0.71:2283 to complete initial setup.

2.2 — Tailscale for remote access

Install Tailscale on the VM for access outside the local network:

curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
tailscale status

Enable HTTPS via Tailscale serve (proxies Immich on port 443):

sudo tailscale serve --bg --https 443 http://localhost:2283

To disable:

sudo tailscale serve --https=443 off

2.3 — Upgrading Immich

Run from ~/immich-app. Update IMMICH_VERSION in .env to the new release tag first, then:

sudo docker compose down
sudo docker compose pull
sudo docker compose up -d
sudo docker image prune

Part 3 — ImmichFrame (Local Network Frames)

ImmichFrame is a separate service that serves photo slideshows from the Immich library to digital picture frames. Instances run on the Immich VM serving frames within the local network.

immichframe-docker-compose.yml
name: immichframe
services:
  immichframe:
    container_name: immichframe
    image: ghcr.io/immichframe/immichframe:latest
    #restart: on-failure
    restart: always
    ports:
      - "8080:8080"
    env_file:
      - .env
    environment:
      TZ: "Etc/MST"
immichframe.env
ImmichServerUrl="http://192.168.0.71:2283"

# Either ApiKey or ApiKeyFile must be specified.
ApiKey="WLzWI1y9S6drkOry7oAYQDj1xcEPoPhkQCPcSWjSs"
# ApiKeyFile=/path/to/key

# AuthenticationSecret=
Interval=15
# TransitionDuration=2
ImageZoom=true
ImagePan=true
Layout=splitview
# DownloadImages=false
# ShowMemories=false
# ShowFavorites=false
# ShowArchived=false
# ImagesFromDays=
# ImagesFromDate=
# ImagesUntilDate=
RenewImagesDuration=1
# Rating=5
Albums=959ff5b2-c735-4809-89e5-287d6d356c3f
# ExcludedAlbums=ALBUM3,ALBUM4
# People=PERSON1,PERSON2
# Webcalendars=https://calendar.mycalendar.com/basic.ics,webcal://calendar.mycalendar.com/basic.ics
# RefreshAlbumPeopleInterval=12
# ShowClock=true
# ClockFormat=hh:mm
# ClockDateFormat=eee, MMM d
# ShowProgressBar=true
# ShowPhotoDate=true
# PhotoDateFormat=yyyy-MM-dd
# ShowImageDesc=true
ShowPeopleDesc=true
# ShowAlbumName=true
# ShowImageLocation=true
# ImageLocationFormat=City,State,Country
# PrimaryColor=#F5DEB3
# SecondaryColor=#000000
# Style=none
# BaseFontSize=17px
WeatherApiKey=a20ed32596096e04c11ebc5ca37bab27
ShowWeatherDescription=true
# WeatherIconUrl=https://openweathermap.org/img/wn/{IconId}.png
UnitSystem=imperial
WeatherLatLong="32.2217,-110.9265"
# Language=en
# Webhook=

Local vs. remote frames

The ImmichFrame instances on this VM serve frames on the local network that can reach 192.168.0.71 directly. A separate Frames LXC container handles frames that are physically outside the local network — it runs its own ImmichFrame instances exposed via Tailscale Funnel, each with its own .ts.net hostname. Both point back to the Immich server on this VM (http://192.168.0.71:2283) as their photo source.

3.1 — Deploy ImmichFrame

mkdir ~/immich-frame && cd ~/immich-frame
wget -O docker-compose.yml https://raw.githubusercontent.com/immichFrame/ImmichFrame/main/docker/docker-compose.yml
wget -O .env https://raw.githubusercontent.com/immichFrame/ImmichFrame/main/docker/example.env
nano .env

Key .env values:

Variable Value Notes
ImmichServerUrl http://192.168.0.71:2283 Points to the Immich server on this VM
ApiKey (Immich API key) Create in Immich → User Settings → API Keys
Albums (album UUID) UUID from the Immich album URL
Interval 15 Seconds between photos
WeatherApiKey (OpenWeatherMap key) Optional — for weather display
WeatherLatLong "32.2217,-110.9265" Coordinates for weather location
sudo docker compose up -d

The instance runs on port 8080 as configured in the compose file.

3.2 — Deploying a second ImmichFrame instance

Each additional instance needs its own directory and a different host port. The container always listens on 8080 internally — only the host-side port changes.

mkdir ~/immich-frame-2 && cd ~/immich-frame-2
# Copy and adjust the compose file from the first instance
cp ~/immich-frame/docker-compose.yml .
cp ~/immich-frame/.env .

Edit docker-compose.yml and change the port mapping:

ports:
  - "8083:8080"   # host port 8083 → container port 8080

Update .env with the album UUID and any other settings specific to this frame, then:

sudo docker compose up -d

To expose the second instance outside the local network via Tailscale:

sudo tailscale serve --bg --https 8444 http://localhost:8083
# To turn off: sudo tailscale serve --https=8444 off

To reset all Tailscale serve rules:

sudo tailscale serve reset

3.3 — Upgrading ImmichFrame

Run from each instance directory:

sudo docker compose down
sudo docker compose pull
sudo docker compose up -d
sudo docker image prune


Part 4 — ImmichFrame App on Frameo Frames

Frameo-brand digital frames can run the ImmichFrame Android app, replacing the default Frameo interface with a slideshow pulled from an ImmichFrame server instance.

4.1 — Prerequisites

  • ADB installed on your workstation
  • Frame connected to the local network, IP known (e.g. 192.168.0.27)
  • ImmichFrame .apk file downloaded locally

4.2 — Enable ADB on the frame

adb start-server
adb tcpip 5555
adb connect 192.168.0.27:5555
adb shell ping 192.168.0.71   # confirm frame can reach the Immich VM
exit

4.3 — Install ImmichFrame and disable Frameo

adb install /path/to/ImmichFrame_v47.apk
adb shell am start com.immichframe.immichframe/.MainActivity
adb shell su -c "pm disable net.frameo.frame"
adb reboot

4.4 — Configure the app on the frame

After reboot, open the ImmichFrame app on the frame and set the server URL to the local ImmichFrame instance:

http://192.168.0.71:8080

Nixplay frames

Nixplay-brand frames require a more involved jailbreak process to sideload Android apps, involving WSL2/Debian, rkflashtool, and direct flash partition writes. Only attempt this with a frame you are willing to risk bricking.

immich_NIXPLAY_jailbreak.txt
NIXPLAY

https://share.google/aimode/QxbpCHiBO2GIx4a6l

To install the Windows Subsystem for Linux (WSL) and set up the Debian environment required for the jailbreak, follow these steps:

Virtualization: Ensure "Virtualization" is enabled in your computer's BIOS/UEFI settings. Without this, WSL 2 will not start.
MAG B550 Tomahawk
del to enter bios on restart
OC menu
OC explore mode = expert
CPU features > enable SVM mode
save and exit

1. Basic WSL Installation
On modern versions of Windows 10 and 11, you can install WSL with a single command.
Open PowerShell or Command Prompt as an Administrator.
Type the following command and press Enter:

wsl --install

Restart your computer when the process completes to finish enabling the necessary Windows features.

2. Install Debian Distribution
Since the Nixplay jailbreak instructions are specifically written for Debian, you should install that exact distribution.

Open PowerShell and view available distributions:

wsl --list --online

Install Debian specifically using:

wsl --install -d Debian

3. Initialize Your Linux Environment

Once installed, launch Debian from your Start Menu.
The first time you open it, you will be prompted to create a UNIX username and password. These do not need to match your Windows credentials.
jake
jake

Immediately update your new environment to ensure all security patches are current:

sudo apt update && sudo apt upgrade -y

4. Preparation for the Jailbreak
Once inside your Debian terminal, you can install the specific tools mentioned in the Micropipes guide:

Install necessary packages:

sudo apt install adb fastboot cpio dos2unix rkflashtool android-sdk-platform-tools

Access Windows files: Your Windows C: drive is automatically mounted inside Debian at /mnt/c/. You can navigate to your downloaded files using cd /mnt/c/Users/YourUsername/Downloads/.

1. Install usbipd on Windows
This is the primary installation that creates the "bridge" between your physical USB hardware and the WSL environment.

Open PowerShell as an Administrator and run:

winget install usbipd

2. Configure Debian (WSL)

You must install the client tools inside your Debian terminal so it knows how to receive the USB device forwarded from Windows.
Open your Debian terminal.
Run the following commands:
sudo apt update
sudo apt install usbip hwdata usbutils abootimg


Once these packages are installed in Debian, return to your Windows PowerShell (Admin) to finish the connection:

Hold frame factory reset button for 5 seconds

in powershell:

usbipd list

Bind your Nixplay frame:

usbipd bind --busid <BUSID>

Attach to WSL:

usbipd attach --wsl --busid <BUSID>

After attaching, you can run lsusb in your Debian terminal to confirm the Rockchip device is visible and ready for the jailbreak script.

In debian:

lsusb

ensure adb recognizes the frame, and that frame is the only android device attached:

adb devices

run backup shell script

make sure adb server is not running on windows side

in powershell:
adb kill-server

in debian:
adb start-server


move script and all necessary files like rkflashtool to linux home folder ~/

cp /mnt/c/users/knigh/downloads/nixplay/backup_script.sh backup_script.sh
convert dos to unix
dos2unix backup_script.sh
chmod +x backup_script.sh

sudo ./backup_script.sh

# on debian
mkdir recovery_wd
cd recovery_wd
cp ../recovery.img .
# creates zImage, initrd.img, and bootimg.cfg.
# `apt install abootimg` if you need to
abootimg -x recovery.img

mkdir ramdisk
cd ramdisk
gzip -dc ../initrd.img | cpio -i

jake@DESKTOPJK:~/recovery_wd/ramdisk$ cd ../..
jake@DESKTOPJK:~$ mkdir simg
jake@DESKTOPJK:~$ sudo mount -t ext4 system.img simg
jake@DESKTOPJK:~$ cd simg
jake@DESKTOPJK:~/simg$ sudo mkdir sbin
jake@DESKTOPJK:~/simg$ cd sbin

# i found i had to copy ALL apps from the recovery sbin into the new system sbin, not just adbd
jake@DESKTOPJK:~/simg/sbin$ sudo cp ../../recovery_wd/ramdisk/sbin/* .
jake@DESKTOPJK:~/simg/sbin$ sudo chmod 755 adbd
jake@DESKTOPJK:~/simg/sbin$ cd ../..
jake@DESKTOPJK:~$ sync
jake@DESKTOPJK:~$ sudo umount simg

# write to the system partition
jake@DESKTOPJK:~$ sudo rkflashtool w system < system.img
jake@DESKTOPJK:~$ sudo rkflashtool b

jake@DESKTOPJK:~$ lsusb
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 001 Device 006: ID 2207:310d Fuzhou Rockchip Electronics Company RK3126 in Mask ROM mode
Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
jake@DESKTOPJK:~$ mkdir boot_wd
jake@DESKTOPJK:~$ cd boot_wd
jake@DESKTOPJK:~/boot_wd$ cp ../boot.img .
jake@DESKTOPJK:~/boot_wd$ sudo abootimg -x boot.img
writing boot image config in bootimg.cfg
extracting kernel in zImage
extracting ramdisk in initrd.img
extracting second stage image in stage2.img
jake@DESKTOPJK:~/boot_wd$ mkdir ramdisk
jake@DESKTOPJK:~/boot_wd$ cd ramdisk
jake@DESKTOPJK:~/boot_wd/ramdisk$ gzip -dc ../initrd.img | cpio -i
7712 blocks
jake@DESKTOPJK:~/boot_wd/ramdisk$ nano init.rc

add these lines in the on_boot section, before "start core defaults"
setprop ro.adb.secure 0
setprop ro.debuggable 1
setprop service.adb.root 1
setprop sys.usb.config adb

write /sys/class/android_usb/android0/enable 0
write /sys/class/android_usb/android0/idVendor 2207
write /sys/class/android_usb/android0/idProduct 0006
write /sys/class/android_usb/android0/functions adb
write /sys/class/android_usb/android0/enable 1
start adbd

jake@DESKTOPJK:~/boot_wd/ramdisk$ find . | cpio -o -H newc | gzip > ../initrd-new.img
7713 blocks
jake@DESKTOPJK:~/boot_wd/ramdisk$ cd ..
jake@DESKTOPJK:~/boot_wd$ sudo abootimg --create ../boot-new.img -f bootimg.cfg -k zImage -r initrd-new.img
reading config file bootimg.cfg
reading kernel from zImage
reading ramdisk from initrd.new.img
Writing Boot Image ../boot-new.img
jake@DESKTOPJK:~/boot_wd$ cd ..
jake@DESKTOPJK:~$ sudo rkflashtool w boot < boot-new.img
rkflashtool: info: rkflashtool v5.2
rkflashtool: info: Detected RK3126...
rkflashtool: info: interface claimed
rkflashtool: info: working with partition: boot
rkflashtool: info: found offset: 0x00016000
rkflashtool: info: found size: 0x00006000
rkflashtool: info: writing flash memory at offset 0x0001bfe0... Done!
jake@DESKTOPJK:~$ sudo rkflashtool b
rkflashtool: info: rkflashtool v5.2
rkflashtool: info: Detected RK3126...
rkflashtool: info: interface claimed
rkflashtool: info: rebooting device...

disable nixplay apps
adb shell pm list packages
adb shell
w10p03:/ # pm disable com.kitesystems.nix.prod
Package com.kitesystems.nix.prod new state: disabled
w10p03:/ # pm disable com.nixplay.webviewtest
Package com.nixplay.webviewtest new state: disabled
w10p03:/ # pm disable com.kitesystems.nix.frame

Part 5 — immich-go (Bulk Upload)

immich-go is a CLI tool for bulk-uploading photos from local folders to the Immich server.

Create an API key in Immich → User Settings → API Keys with the following permissions:

Permission
asset.read
asset.statistics
asset.update
asset.upload
asset.copy
asset.replace
asset.delete
asset.download
album.create
album.read
albumAsset.create
server.about
stack.create
tag.asset
tag.create
user.read

Note

Full immich-go usage and workflow are not yet documented here.