Self-Hosted Photo Gallery Setup
We earn commissions when you shop through the links on this page, at no additional cost to you.
I got tired of paying Google, Apple, and Amazon just to store family photos. So I set up Immich on a spare machine at home—and in about an hour, I had a private photo gallery with unlimited storage, automatic mobile uploads, and zero subscription fees. Here's exactly how I did it.
Why Self-Host Your Photos?
When you use cloud providers, you're trading privacy for convenience. Your photos sit on someone else's server, subject to their terms of service and their algorithms. I prefer owning my data. Self-hosting a photo gallery gives you:
- Privacy: No AI analysis, no targeted ads, no data sharing.
- Control: You decide retention policies, sharing rules, and backups.
- No recurring costs: One-time hardware investment beats monthly subscriptions.
- Offline access: Photos stay available even if your internet drops.
The downside? You maintain the hardware and manage backups yourself. But if you already run a homelab, this is trivial.
Choosing Your Solution: Immich vs. Nextcloud
I evaluated two main contenders: Immich and Nextcloud. Nextcloud is the heavyweight—it's a full file sync platform that includes photos as one feature. Immich is purpose-built for photo management, inspired by Google Photos.
I chose Immich because it's faster, has better mobile integration, and doesn't bloat my system with unnecessary file sync overhead. If you want one tool that handles files, photos, calendar, and contacts, Nextcloud is your answer. But for a dedicated photo gallery? Immich wins.
Hardware Requirements
I'm running this on an old Intel NUC with 8GB RAM and a 2TB external SSD. Overkill? Probably. The minimum is:
- 2GB RAM (tight, but doable)
- 10GB free disk for the app and database
- Storage for your photo library (depends on your collection)
- Stable network connection
If you want to run machine learning features (smart search, object detection), you'll want at least 4GB RAM and ideally a GPU. I started without GPU acceleration and added it later.
Setting Up Immich with Docker Compose
I prefer Docker Compose because it's reproducible and easy to upgrade. Here's my production setup:
version: '3.8'
services:
immich-server:
image: ghcr.io/immich-app/immich-server:latest
container_name: immich_server
command: start.sh immich
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro
environment:
DB_HOSTNAME: immich-db
DB_USERNAME: postgres
DB_PASSWORD: ${DB_PASSWORD}
DB_NAME: immich
REDIS_HOSTNAME: immich-redis
UPLOAD_LOCATION: /usr/src/app/upload
ports:
- "2283:3001"
depends_on:
- immich-db
- immich-redis
restart: unless-stopped
immich-microservices:
image: ghcr.io/immich-app/immich-server:latest
container_name: immich_microservices
command: start.sh microservices
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro
environment:
DB_HOSTNAME: immich-db
DB_USERNAME: postgres
DB_PASSWORD: ${DB_PASSWORD}
DB_NAME: immich
REDIS_HOSTNAME: immich-redis
UPLOAD_LOCATION: /usr/src/app/upload
depends_on:
- immich-db
- immich-redis
restart: unless-stopped
immich-db:
image: postgres:15-alpine
container_name: immich_db
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: immich
volumes:
- immich-db:/var/lib/postgresql/data
restart: unless-stopped
immich-redis:
image: redis:7-alpine
container_name: immich_redis
restart: unless-stopped
volumes:
immich-db:
networks:
default:
name: immich
Save this as docker-compose.yml. Now create a .env file in the same directory:
UPLOAD_LOCATION=/mnt/photos
DB_PASSWORD=your_secure_password_here
Replace /mnt/photos with your actual storage path, and set a strong password. Then start the stack:
mkdir -p /mnt/photos
docker compose up -d
That's it. Immich is now running on port 2283. Open your browser to http://localhost:2283 and you'll see the login screen.
UPLOAD_LOCATION to a mount point on a dedicated drive or NAS. This way, if you ever need to rebuild the containers, your photos stay safe.Reverse Proxy with Caddy
I don't want to type port numbers every time I access my gallery from my phone. I use Caddy as a reverse proxy. Here's my Caddyfile:
photos.home.internal {
reverse_proxy localhost:2283
}
This assumes you have local DNS set up (I use AdGuard Home). If you're accessing from outside your network, use your actual domain and let Caddy handle HTTPS automatically:
photos.example.com {
reverse_proxy localhost:2283 {
header_uri /api/* ?
}
}
Reload Caddy with sudo systemctl reload caddy and you're live.
Mobile Upload and Sync
The Immich mobile app (iOS and Android) is where this shines. Install it from the App Store or Google Play, log in with your server address and credentials, and enable "Background Backup." Every photo you take automatically uploads to your server—no manual steps.
I have it configured to only upload over WiFi to save mobile data, but you can change that. The app also supports offline viewing, so you can browse your entire library even without an internet connection.
Running Machine Learning Features
By default, Immich doesn't index photos for smart search. If you want AI-powered features (find all photos of "dogs" or "mountains"), you need to enable the recognition service. Add this to your compose file:
immich-ml:
image: ghcr.io/immich-app/immich-ml:latest
container_name: immich_ml
volumes:
- immich-ml:/usr/src/app/cache
environment:
REDIS_HOSTNAME: immich-redis
restart: unless-stopped
volumes:
immich-ml:
Then add MACHINE_LEARNING_ENABLED=true to your .env file and restart. On my NUC without a GPU, face recognition and object detection run in the background—slow, but it works. GPU support speeds this up dramatically if you have a spare NVIDIA card.
Backups and Data Safety
Self-hosting means you're responsible for backups. I run a nightly backup to a second drive using Borg:
#!/bin/bash
BACKUP_DIR="/mnt/backup/immich"
PHOTO_DIR="/mnt/photos"
borg create \
$BACKUP_DIR::photos-$(date +%Y%m%d) \
$PHOTO_DIR \
--progress \
--compression lz4
borg prune $BACKUP_DIR --keep-daily 7 --keep-weekly 4 --keep-monthly 12
Add this to cron to run daily. I also keep a monthly backup off-site (encrypted, uploaded to S3-compatible storage). Two backups: one local for quick recovery, one remote for disaster recovery.
Storage Optimization
My photo library is 500GB and growing. To keep storage in check, I use Immich's duplicate detection to find and remove near-duplicates. Go to Admin Settings > Library and run "Duplicate Detection."
I also disable thumbnail generation for videos (saves space) and set a quality threshold for thumbnails. These settings are in Admin > Library Settings.
Accessing from Outside Your Network
If you want to access your gallery from anywhere, you have options:
- Tailscale: Simple, secure, no port forwarding needed. My preference.
- Cloudflare Tunnel: Free tier works. More complex setup but no VPN required on mobile.
- Port forwarding: Risky. I don't recommend it for photos.
I use Tailscale. Once installed on my server and phone, I access photos.home.internal from anywhere, and Tailscale handles encryption and routing.
Sharing Photos with Others
Immich lets you create public links to albums. Share a link with family, and they can view (but not delete) your photos. No account required. I set expiry dates on links and disable downloads for sensitive albums.
Next Steps
Start with the Docker Compose setup above. Give it a few hours to process your existing photos (if you import them). Then install the mobile app and let it back up your future photos automatically.
Once you're comfortable, consider adding GPU acceleration, setting up automated backups, and configuring Tailscale for external access. The core setup is solid as-is—everything else is optimization.
Your photos deserve to be yours. Self-hosting gives you that back.
Discussion