DEV Community

Gennyi07
Gennyi07

Posted on

Immich on Android/Termux without Docker, root, or glibc (aarch64 port)

A few weeks ago I set myself a challenge: run Immich — a full self-hosted photo management platform — natively on my Android phone, without Docker, without root, and without any cloud dependency.

The result is immich-native-android, a working port for Android/Termux on aarch64. This is what it took to get there.


Why this is not trivial

Immich is designed to run on Linux servers via Docker. It's a multi-service stack: PostgreSQL, Redis, a Node.js server, and a Python ML service for face recognition and smart search.

Android is not Linux. The differences that matter:

  • Bionic libc instead of glibc — most prebuilt native binaries simply crash or silently fail
  • No FUSE without root — you can't mount remote filesystems as local paths
  • No systemd — no service manager, no socket activation
  • Phantom Process Killer — Android 12+ aggressively kills background processes
  • Read-only /tmp — many build systems assume they can write there
  • No FHS layout — paths like /usr/bin don't exist in Termux

Every single one of these required a workaround.


The architecture

proot Debian (container)
└── PostgreSQL 17 + VectorChord extension
        ↓ localhost:5432
Termux (native aarch64)
├── Redis
├── Node.js 20  →  Immich server      (port 2283)
└── Python + ONNX  →  Immich ML       (port 3003)
Enter fullscreen mode Exit fullscreen mode

PostgreSQL runs inside a proot Debian container because it needs glibc and kernel features unavailable in Termux. Everything else runs natively.


The problems, one by one

1. Node.js version

The latest Node.js caused silent startup failures on Bionic. The fix was pinning to Node 20 LTS and forcing npm to treat the platform as Linux:

npm_config_platform=linux
npm_config_arch=arm64
npm_config_libc=glibc
Enter fullscreen mode Exit fullscreen mode

This tricks Sharp and other native modules into using their linux-arm64 prebuilts instead of trying (and failing) to detect Android.

2. Native dependencies

Most npm packages ship prebuilt binaries compiled against glibc. On Bionic they either crash immediately or produce silent errors.

Sharp (image processing) required full recompilation from source against Termux's libvips:

SHARP_FORCE_GLOBAL_LIBVIPS=1
Enter fullscreen mode Exit fullscreen mode

bcrypt — same treatment, rebuilt from source.

Python ML dependencies (ONNX Runtime, InsightFace) had no Android/aarch64 wheels at all. Each was compiled inside a Python venv using uv and Rust-based build tools:

uv sync --python-platform manylinux_2_28_aarch64
Enter fullscreen mode Exit fullscreen mode

watchfiles was removed entirely — it requires maturin (a Rust build tool) that doesn't work on Android's Bionic.

Other fixes that were necessary across the build:

NODE_OPTIONS=--max-old-space-size=6144  # prevents OOM during build
TMPDIR=$HOME/tmp                         # /tmp is read-only on Android
npm_config_script_shell=$(which bash)   # install scripts need bash, not sh
Enter fullscreen mode Exit fullscreen mode

3. PostgreSQL in proot

PostgreSQL cannot run natively in Termux — it needs glibc and kernel features that Android doesn't provide. The solution: run it inside a proot Debian container.

The catch: PostgreSQL crashes when started as root inside proot. The fix was creating a dedicated postgres user inside the container and launching the service under that user via a scripted proot session.

4. WebDAV external library — patching Immich source

This was the most complex problem.

The goal: index photos stored on a remote NAS without copying them to the phone.

The problem: Android has no FUSE support without root, so you can't mount a network share as a local path. The only Android solution (RSAF) exposes files through the Storage Access Framework — completely inaccessible from Termux.

The solution: patch Immich's source code before building.

Immich's LibraryService validates external library paths with path.isAbsolute(), which rejects HTTP URLs. I added bypass logic for WebDAV paths before the TypeScript compilation step:

if (importPath.startsWith('http://')) {
  // WebDAV path — skip isAbsolute() check
}
Enter fullscreen mode Exit fullscreen mode

The NAS phone runs rclone serve webdav over Tailscale. Immich indexes the photos without ever copying them locally.

5. Phantom Process Killer

Android 12+ kills background processes not started by the foreground app. Immich runs as four separate processes (PostgreSQL, Redis, Node, Python). Without intervention, Android kills them within minutes.

Fix via Shizuku + ADB (run once after each reboot):

adb shell device_config set_sync_disabled_for_tests persistent
adb shell device_config put activity_manager max_phantom_processes 2147483647
Enter fullscreen mode Exit fullscreen mode

What works

  • Full Immich web UI on localhost:2283
  • Photo/video upload and backup
  • Album management
  • External library indexing via WebDAV (remote NAS, no local copy)
  • Thumbnail generation
  • Timeline and map view
  • Machine learning (face recognition, CLIP smart search) — works on this setup, stability may vary

What doesn't (yet)

  • No GPU/NPU acceleration — all ML runs on CPU
  • Immich updates require re-running the install script
  • Shizuku must be re-enabled after each reboot

Tested on

Samsung Galaxy S25 — Snapdragon 8 Elite, aarch64, Android 15
Immich v2.5.6


The repo

Everything is documented and scripted:

👉 github.com/Gennyi07/immich-native-android

Based on arter97/immich-native, heavily reworked for Android/Bionic.

If you find it useful, a ⭐ on GitHub helps the project get discovered.


Final thought

The Android/Bionic compatibility layer is genuinely hostile to software not designed for it. Every layer of this stack required a workaround. But it works — and it runs on hardware most people already own.

If you've been looking for a way to run Immich without a dedicated server, this might be it.

Top comments (0)