Immich - Hardened Self-Hosted Photo Backup with ML Smart Sea
by Lynxroute
Immich - CIS Level 1 hardened self-hosted photo backup with ML smart search on Ubuntu 24.04 LTS.
What is Immich
Immich is an open-source self-hosted photo and video backup platform - a private alternative to Google Photos and iCloud. The stack runs as four docker containers: a Node and Python server exposing the REST API and web UI, a Python ML container running CLIP for natural-language smart search and face detection, PostgreSQL 14 with VectorChord and pgvecto.rs for embedding queries, and Valkey 9 (Redis-compatible) for cache and queues. The iOS and Android apps perform background auto-upload at full original quality with EXIF and GPS metadata preserved. Operators get shared albums, public links, multi-user libraries with quotas, partner sharing, external libraries, RAW support, and HEIC and HEVC transcoding.
Why self-host Immich
Self-hosting puts every photo, every face embedding, and every ML inference inside your own Azure tenant - no per-seat SaaS fee, no third-party access to private albums. Ideal for households leaving Google Photos or iCloud, photographers with RAW workflows that exceed free-tier quotas, and organisations with data residency requirements under GDPR or similar frameworks. AGPL-3.0 license, no vendor lock-in.
What this VM image adds
Security hardening:
- No pre-seeded admin account - on first visit the Immich web UI prompts you to register the first user, who becomes the admin. Immich auto-locks admin sign-up once the users table is non-empty, so subsequent attempts are rejected without operator action
- PostgreSQL password generated at first boot - 32 random characters, written into /opt/immich/.env (mode 0640), never baked into the image
- All four containers reachable only via the docker bridge - host port 2283 is bound to 127.0.0.1 only, the only public-facing endpoint is Nginx on TCP 443
- Nginx reverse proxy with TLS - HTTP to HTTPS redirect, hardened cipher suite, WebSocket support, security headers, 50 GB client_max_body_size for RAW and 4K uploads
- All four container images pre-pulled at build time so first launch is fast and not dependent on upstream registry availability
- Docker daemon log rotation pre-configured to prevent json-file logs from filling the root volume
- UFW firewall - only TCP 22, 80, 443 are exposed; fail2ban for SSH brute-force protection; AppArmor mandatory access control
- CVE scan - every image is scanned for vulnerabilities with Trivy before release
- Certbot pre-installed - one command issues a Let's Encrypt certificate after you point a domain at the VM
OS hardening (CIS Level 1):
- CIS Level 1 hardened - CIS Ubuntu 24.04 LTS Level 1 Benchmark via ansible-lockdown
- auditd - system call auditing for critical paths
- SSH hardening - PasswordAuthentication disabled, key-only access
- Kernel hardening - SYN cookies, ASLR, rp_filter, TCP BBR
- /tmp as tmpfs - nosuid, nodev, noexec
- Azure IMDS endpoints - egress rules pre-configured (169.254.169.254, 168.63.129.16)
Compliance artifacts (inside the VM):
- SBOM - CycloneDX 1.6 at /etc/lynxroute/sbom.json
- CIS Conformance Report - OpenSCAP HTML at /etc/lynxroute/cis-report.html
- Tailored CIS profile - /usr/share/doc/lynxroute/CIS_TAILORED_PROFILE.md
- Server credentials file - /root/immich-credentials.txt with web UI URL, mobile-app endpoint, the per-instance PostgreSQL password, and stack-management commands (no admin email/password - the operator registers the first admin via the web UI)
Quick Start
- Deploy VM from Azure Marketplace (Standard_D2s_v3 or larger recommended)
- Open NSG: TCP 443 from YOUR IP/32 only (widen after admin registration), TCP 80 if you want Let's Encrypt, TCP 22 from management IPs
- SSH: ssh -i key.pem azureuser@<PUBLIC_IP>
- Read connection details: sudo cat /root/immich-credentials.txt
- Open https://<PUBLIC_IP>, accept the self-signed cert, click Sign up - the first registered user becomes the admin. Sign-up auto-locks afterwards
- Install the Immich app, set Server URL to https://<PUBLIC_IP>/api, log in, enable Background Backup
- Public TLS: sudo certbot --nginx -d your.domain.com
The photo library lives at /opt/immich/library and Postgres data at /opt/immich/postgres - attach an Azure Managed Disk there for libraries larger than the root disk.