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
.apkfile 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.