I stopped renting my own memories. Google Photos is a great product, but it is a subscription that only ever gets more expensive, and every photo I take lives on someone else’s terms. So I moved the whole library to Immich, self-hosted, with storage that is effectively unlimited and costs me pennies per GB.

Here is the architecture I landed on.

The shape of it

  • Compute: a Linux EC2 instance runs Immich in Docker.
  • Storage: photos and videos don’t live on the instance’s disk. An S3 bucket is mounted into the filesystem, so Immich writes to a normal path while the bytes actually land in S3. Storage grows without me ever resizing a volume.
  • Private access: the whole thing sits on my Tailscale tailnet, so from any of my devices it’s just a private hostname, no ports exposed to the internet.
  • Public + SSL: Cloudflare terminates TLS and fronts the one hostname I do want reachable, so the mobile app has a clean HTTPS endpoint.

Storage: the S3 mount

This is the part that makes it “unlimited.” Instead of a big EBS volume I have to grow and pay for whether it’s full or not, I mount an S3 bucket into the filesystem and point Immich’s upload location at it.

# Mount the bucket into a local path (rclone or mountpoint-s3 both work)
rclone mount photos-bucket:immich /mnt/immich \
  --vfs-cache-mode writes \
  --allow-other \
  --daemon

Immich then treats /mnt/immich like any other directory. Uploads, thumbnails, and the ML-generated data all flow to S3. I pay for exactly the bytes I store, lifecycle rules push old originals to cheaper storage classes, and I never think about disk size again.

Illustrative config - tune the cache mode, storage class, and IAM policy to your own setup before you rely on it.

Immich itself

Standard Immich docker-compose, with the upload location pointed at the mount:

services:
  immich-server:
    image: ghcr.io/immich-app/immich-server:release
    environment:
      UPLOAD_LOCATION: /mnt/immich
      DB_PASSWORD: ${DB_PASSWORD}
    volumes:
      - /mnt/immich:/usr/src/app/upload
    restart: always

Postgres and Redis run alongside it. The machine-learning container handles face and object recognition - it is genuinely as good as the big-cloud photo search now, and it runs on my box, on my photos.

Private by default, public where I choose

Everything is reachable over Tailscale first. My phone, my laptop, and my other machines all see Immich at a stable private hostname with no public exposure at all. For the one case where I want a clean public endpoint (sharing an album, the mobile app off my tailnet), Cloudflare sits in front and handles SSL, so there is never a raw origin certificate to manage.

What it costs

Effectively: a small EC2 instance, plus S3 at a few cents per GB per month with lifecycle rules doing the heavy lifting. Compared to a photos subscription that scales with your library forever, it pays for itself quickly - and I own every byte.

If you want the other half of my homelab, the Home Assistant and Lutron writeup is here.