I’ve been paying Adobe a monthly ransom for the privilege of not deleting my own photos. That subscription felt less like software and more like a landlord, except this one also wanted to scan my vacation shots for marketing insights. Google Photos was the alternative, but handing over every birthday and blurry pet picture to an ad company never sat right with me.

So I tore it all down. The fix wasn’t another cloud service or a compromise on privacy. It was Immich, a self-hosted photo manager that runs on my own hardware. In my case, spread across a multi-node K3s cluster that already handles other AI workloads. The migration took me under twenty minutes from start to finish, and the only cost is electricity and a little disk space.

What sold me wasn’t the absence of a subscription line item. Immich gives you automatic backup from your phone via its mobile app, on-device face recognition that doesn’t phone home, and a search interface that finds that one screenshot from three years ago. You point it at your storage volume, set up the sync schedule with docker compose or through Helm charts on Kubernetes, and walk away.

Your photos stay in formats you control. No proprietary vaults, no lock-in beyond what you impose on yourself. walks through the exact setup: spinning up Immich as containers on bare-metal GPU nodes (yes, it can use hardware acceleration for machine learning features). Configuring reverse proxying so you can reach it from anywhere without exposing ports recklessly, and pointing your phone’s auto-backup at your new private server.

If you’re already running Kubernetes for other projects, or even if you’re comfortable with docker-compose.yml, you’ll have this done before your coffee cools. Let’s take back ownership of our memories.

The Cloud Tax Is Getting Harder to Ignore

Google’s paid tiers start at $1.99 monthly for 100 GB. But most photo-heavy households need the 2 TB plan at $9.99 per month. That’s roughly $120 annually for storage alone. The math gets uglier when you factor in what your photos are worth to them. Consumer trust has slipped noticeably since last year. Surveys from Pew and others show steady erosion of confidence around AI training on personal data.

One hundred and twenty dollars buys a lot of electricity. My self-hosted Immich server draws about 15 watts at idle on hardware I already owned—roughly $1.50 per month in electricity, with hardware amortizing over several years. But cost isn’t the real driver here. It’s control during failure events. Last June, my ISP dropped for nine hours during a regional fiber cut.

My photos sat on Google’s servers, unreachable through my phone’s LTE fallback because I’d enabled auto-delete of local originals to save space on a 128GB handset.

That outage crystallized what local-first means: physical storage survives network partitions. Immich stores originals on a ZFS pool in my basement rack. When the internet vanishes, I open the mobile app over Wi-Fi and view every photo from 2018 onward at full resolution. You keep face recognition, timeline views, shared albums, and mobile uploads working locally.

Google Photos remains excellent software—its searchable object recognition and face grouping are genuinely impressive features that Immich matches with its built-in machine learning pipeline using TensorFlow models. But those algorithms require sending image analysis to Google’s infrastructure by default.

Google caps you at the terabyte tier before prices climb toward $20 monthly. A self-hosted setup scales with whatever drives you already own, which you can typically add for well under $100 per terabyte. The shift isn’t about paranoia; it’s about arithmetic plus agency. When Adobe faced backlash over its Terms of Service last summer regarding content scanning, the pattern became visible: your media library is just another training corpus unless you physically control it.

Plan Monthly Cost Control Offline Access
Google One 2TB Paid subscription Limited None
Self-hosted array ~Electricity cost only Full Complete

These numbers form one conclusion. Users aren’t leaving because Google Photos broke. They’re leaving because ownership beats rental when trust erodes and outages bite simultaneously. Twenty minutes of setup against years of rental payments makes that comparison do most of the persuasion on its own.

Pre-Flight Checklist – What You Need Before Installing Immich

Ubuntu 22.04 LTS or Debian 12 are your safest bets here. I’ve run Immich on both, and the install scripts assume one of these two distributions. Docker Engine v27+ is non-negotiable. The docker compose plugin ships separately on older versions, and fighting that mismatch wastes more time than any photo migration ever will. Git CLI basics matter less than you think—you’ll clone the repo, pull updates, and occasionally check a tag.

Before you touch a single config file, decide where originals live. If they stay on the host machine, you’re betting on that disk’s health with zero redundancy. I attached a USB drive to my server permanently instead; it cost me a mount-point headache once because /etc/fstab needed the drive’s UUID rather than its device name.

Here’s a rough RAM usage chart from my own testing under load:

Scenario Idle RAM 1 concurrent backup upload 5 users browsing
Fresh install ~700MB ~1.2GB ~1.8GB
After ML face detection ~900MB ~1.4GB ~2.1GB

Those numbers assume default settings and no transcoding in progress. Enable video encoding and add another 400–600MB per active job.

Now pin your version before installing anything. I pulled release/v1.x.y directly from GitHub instead of main, because every major update has shipped at least one breaking config change since v1.94. Check release notes for schema migrations before upgrading across versions every few months when new features land. My library hit roughly 85% capacity after two years of phone photos; plan for growth at that rate unless you’re shooting RAW exclusively.

Setting Up Immich in 20 Minutes

The install itself is deliberately boring: clone the repo, run docker compose up -d, and you’re done. The official docker-compose.yml ships with three services: server, microservices, and redis, plus a Postgres database. Run docker compose up -d from the cloned directory, then hit port 2283 for the web UI.

Storage paths matter more than any other config decision; set UPLOAD_LOCATION to a mounted volume on your NAS or dedicated SSD rather than your OS drive. I structure mine as /srv/immich/library, which creates subdirectories for each user plus an .immich folder for thumbnail generation. Keep this path short; deep nested paths occasionally trip up some container filesystem operations.

Pulling the wrong container tag wastes ten minutes and clutters your logs with architecture mismatches. The Immich images live on GitHub Container Registry under ghcr.io/immich-app. For a stable home setup, pin to the release channel: ghcr.io/immich-app/immich-server:release pulls the latest tested build. The registry serves both amd64 and arm64 variants, but Docker resolves them automatically on modern systems via manifest lists.

If you’re on an older Docker Engine or running through a proxy that strips manifest metadata, you’ll see a cryptic “exec format error” at container start—that’s your signal to specify --platform linux/amd64 explicitly in your compose file.

The .env file holds five secrets minimum: DB_PASSWORD, DB_USERNAME, JWT_SECRET, and two Redis-related values if you’re using external caching. Generate each with OpenSSL directly: openssl rand -base64 32 outputs forty-three characters of entropy in one command. Your compose file references these via ${DB_PASSWORD} syntax rather than hardcoding values inline, so rotating credentials later requires editing one file and running two commands.

Reverse proxy setup is where most people burn their twenty minutes. Caddy handles this in three lines: yourdomain.com { reverse_proxy localhost:2283 }. One final check before starting containers: verify port conflicts with ss -tulpn | grep 2283. Immich’s web interface binds port 2283 by default; the database listens on its own internal network so you rarely expose it externally unless running remote clients.

Post-Deploy:

Port 2283 is clear, so point a browser at http://your-server-ip:2283. The setup wizard walks you through creating an admin account. Use something stronger than “admin” or “admin123” — automated scanners hit login pages within hours of deployment, and weak credentials are the first thing they try.

A few hundred images should process within minutes on modest hardware. A library of 50,000 will take hours. I ran my initial test with 500 photos and watched the job queue climb from 12 to 847 pending thumbnail jobs before stabilizing. On an Intel N100 system, that queue pegged CPU usage at 100% for roughly twenty minutes.

Storage planning matters more than CPU speed. Each original file stays untouched on disk while Immich generates compressed previews alongside it — that roughly doubles storage consumption unless you configure storage templates to organize files by date structure. I set IMMICH_STORAGE_TEMPLATE to library/{year}/{month}/{month-day}/ and watched the .immich folder grow to 40GB on import day one with an 8,000-photo library.

Set up automated backups of the PostgreSQL data directory with pg_dump or you’ll lose your entire library index if that volume dies. I run a nightly cron: 0 3 * * * pg_dump -U immich -F c -f /backup/immich-db-$(date +\%Y\%m\%d).dump. Without that dump, the database volume is the single point of failure for your entire photo library.

The mobile app is where Immich finally beats Google Photos for daily use. Background backup triggers when the app opens rather than running continuously, which saves battery but means photos taken right before you sleep might wait until morning to sync. I noticed this within the first 24 hours — photos from 11pm sessions consistently showed up in the morning queue.

If storage gets tight, adjust backup settings per album rather than disabling everything at once. For multi-user setups, add family members under Administration → Users; each person gets their own timeline, face clusters, and storage quota you set in bytes. The machine learning pipeline runs entirely on your hardware — object recognition, scene classification, and facial clustering happen locally via Immich’s built-in ML workers, with no phone-home calls to Google’s servers.

You control the thresholds too: under Administration → Machine Learning Settings, adjust face recognition confidence (default 0.6), clustering distance, or disable smart search entirely if your CPU chokes.

My cluster runs this behind a reverse proxy with Tailscale for remote access — no open ports, no cloud relay, just encrypted WireGuard tunnels back to the machine that already hosts my other services. Twenty minutes gets you there. The time pays back every time you search “beach” and find that photo from three summers ago without scrolling through 4,000 images manually.

Google Photos trained you to expect zero friction from your photo library; Immich matches that expectation while handing you ownership of every byte instead of renting your memories back at escalating subscription tiers.

Self-hosting demands maintenance commitment. Updates arrive roughly monthly on GitHub releases; read the changelog before pulling new images so breaking API changes don’t strand your library mid-migration. My photos live on disks I control, with deduplication that caught 12GB of redundant files on import day one. Twenty minutes to reclaim a decade of photos, a bit of YAML and some disk space for permanent ownership — it keeps paying dividends every time the phone auto-syncs without asking permission.


Keep Reading

The real lesson isn’t about Immich specifically; it’s that the default tools are rarely the only tools, and the migration cost is almost always lower than the imagined one. The question worth sitting with: if photo backup took under half an hour to escape, what else in your digital life is still paying rent unnecessarily?