Self-hosted Google Photos alternative with machine learning, automated backups, and secure remote access.
| Feature | Description |
|---|---|
| 🖼️ Photo Management | Upload, organize, and browse your photos with a beautiful UI |
| 🤖 Machine Learning | Facial recognition, object detection, and smart search |
| 🔒 Secure Access | HTTPS everywhere via Cloudflare Tunnel + Caddy |
| 💾 Auto Backups | Scheduled PostgreSQL backups to NAS (7 daily, 4 weekly, 6 monthly) |
| 🚀 One-Command Setup | Interactive installer handles all configuration |
| ☁️ Cloud Backup | Per-user backups to Proton Drive via rclone |
┌─────────────────────────────────────────────────────────────┐
│ Internet │
└─────────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Cloudflare Tunnel │
└─────────────────────────┬───────────────────────────────────┘
│
┌───────────────┼───────────────┐
▼ ▼ ▼
┌─────────┐ ┌──────────┐ ┌──────────┐
│ Caddy │ │ Immich │◄───│ Immich │
│ (TLS) │───►│ Server │ │ ML │
└─────────┘ └────┬─────┘ └──────────┘
│
┌───────────────┼───────────────┐
▼ ▼ ▼
┌──────────┐ ┌─────────────┐ ┌─────────────┐
│ Postgres │ │ Local SSD │ │ NAS/NFS │
└──────────┘ │ /srv/immich │ │ /mnt/photos │
│ (thumbnails,│ │ (originals, │
│ transcodes)│ │ backups) │
└─────────────┘ └──────┬──────┘
│
┌──────┴──────┐
│ DB Backup │
└─────────────┘
| Data | Location | Why |
|---|---|---|
| Originals (library) | NAS /mnt/photos/immich/library |
Large files, accessed less frequently |
| DB Backups | NAS /mnt/photos/immich/backups |
Safe off-server backup |
| Thumbnails | Local SSD /srv/immich/thumbs |
Fast access, regenerable |
| Transcoded Videos | Local SSD /srv/immich/encoded-video |
Fast access, regenerable |
| Upload Buffer | Local SSD /srv/immich/upload |
Fast writes during upload |
| Profiles | Local SSD /srv/immich/profile |
Small files |
git clone https://github.com/rustygreen/immich-server
cd immich-server
./scripts/install.shIf you see Permission denied when running a script, either make scripts executable:
chmod +x scripts/*.shor run them via bash:
bash ./scripts/install.shThe interactive installer prompts for your:
- 📧 Email (for Let's Encrypt certificates)
- 🌐 Domain (e.g.,
photos.example.com) - 🔑 Cloudflare API Token & Tunnel token
- 📂 Storage locations
That's it! The script handles everything else automatically.
Before running the installer, you'll need:
| Requirement | Details |
|---|---|
| 🖥️ Linux Server | Ubuntu/Debian recommended |
| 🌐 Domain | Managed by Cloudflare |
| 🔑 Cloudflare API Token | Create one → |
| 🚇 Cloudflare Tunnel | Create one → |
🔑 Cloudflare API Token
- Log in to Cloudflare Dashboard
- Go to My Profile → API Tokens → Create Token
- Use the Edit zone DNS template, or create a custom token with:
- Permissions:
Zone > DNS > Edit - Zone Resources:
Include > Specific zone > your-domain.com
- Permissions:
- Copy the token — you'll need it during installation
🚇 Cloudflare Tunnel
- In Cloudflare Dashboard, go to Zero Trust → Networks → Tunnels
- Click Create a tunnel → Select Cloudflared
- Name it (e.g.,
immich-server) and click Save tunnel - On the connector setup page, select Docker and copy the token from the command:
Copy just the token part (the long string after
docker run cloudflare/cloudflared:latest tunnel --no-autoupdate run --token eyJhIjoiYWJj...--token) - Configure the public hostname:
- Subdomain:
photos - Domain: your domain
- Service:
http://immich:3001
- Subdomain:
- Save the tunnel
📡 DNS Record (if not auto-created)
- Go to DNS → Records for your domain
- Add a CNAME record:
- Name:
photos - Target:
<tunnel-id>.cfargotunnel.com - Proxy status: Proxied (orange cloud)
- Name:
Configure NAS/NFS Mount
If using a Synology NAS or other NFS share:
- Enable NFS on your NAS and create a shared folder (e.g.,
/volume1/photos) - Set permissions to allow your server's IP with read/write access
- On your Linux server, edit the filesystem table:
sudo nano /etc/fstab
- Add this line at the end (replace the IP and path with your NAS details):
192.168.1.100:/volume1/photos /mnt/photos nfs defaults,_netdev,nofail,soft,timeo=30,retrans=3,noatime 0 0⚠️ Thesoft,timeo=30,retrans=3options are important - they prevent system hangs if the NAS disconnects. - Save and exit (
Ctrl+X, thenY, thenEnter) - Mount the share:
sudo mkdir -p /mnt/photos sudo mount -a
💡 Using local storage only? Set
UPLOAD_LOCATION=/srv/immichto keep everything on local SSD.
This setup uses split storage for optimal performance and reliability:
- NAS (network): Original photos and backups — large files, accessed infrequently
- Local SSD: Thumbnails and transcodes — frequently accessed, regenerable if lost
This prevents NAS connectivity issues from breaking the UI, while keeping your originals safely on network storage.
Local cache location: /srv/immich/ (created automatically by installer)
All configuration is done through the .env file. No need to modify any other files.
| Variable | Description |
|---|---|
TZ |
Timezone (e.g., America/New_York) |
DB_PASSWORD |
PostgreSQL password (auto-generated) |
UPLOAD_LOCATION |
Where original photos are stored (NAS) |
BACKUP_LOCATION |
Where database backups go (NAS) |
ACME_EMAIL |
Email for Let's Encrypt certificates |
DOMAIN |
Your photos domain (e.g., photos.example.com) |
CF_API_TOKEN |
Cloudflare API token for DNS challenges |
TUNNEL_TOKEN |
Cloudflare Tunnel token |
ML_URL |
Machine Learning URL (leave unset for local ML) |
ML_PORT |
Port to expose ML service (default: 3003) |
ML_MEMORY |
Memory limit for ML container (default: 4G) |
🖥️ Multi-Node Setup (Optional)
You can offload the ML container to a second machine to reduce load on your main server.
On Main Node (NUC 1):
-
Edit
.envand set ML_URL to point to your second node:ML_URL=http://192.168.1.101:3003
-
Start only the main services:
docker compose --profile main up -d
On ML Node (NUC 2):
-
Clone the repo:
git clone https://github.com/rustygreen/immich-server cd immich-server -
Create a minimal
.env:echo "ML_PORT=3003" > .env echo "ML_MEMORY=4G" >> .env
-
Start only the ML service:
docker compose --profile ml up -d
Architecture with 2 nodes:
┌─────────────────────┐ ┌─────────────────────┐
│ NUC 1 │ │ NUC 2 │
│ ┌───────────────┐ │ │ ┌───────────────┐ │
│ │ Immich Server │──┼────────►│ │ Immich ML │ │
│ │ Postgres │ │ HTTP │ │ (port 3003) │ │
│ │ Redis │ │ │ └───────────────┘ │
│ │ Caddy │ │ └─────────────────────┘
│ │ Cloudflared │ │
│ └───────────────┘ │
└─────────────────────┘
Import Script Settings (Optional)
If you want to use the bulk import script (./scripts/import-photos.sh), add these to your .env:
| Variable | Default | Description |
|---|---|---|
IMMICH_API_KEY |
(required) | Your Immich API key |
IMPORT_DIR |
/mnt/photos/upload |
Source folder for imports |
IMPORT_IMMICH_URL |
http://immich:2283 |
Immich server URL |
IMPORT_DELAY |
30 |
Seconds between folder imports |
IMPORT_DELETE_ON_SUCCESS |
true |
Delete source after import |
Get your API key from Immich: Account Settings → API Keys → New API Key
📁 Import Watch Folders (Multi-User)
The Import Watch service monitors folders and automatically imports photos/videos to the correct user's Immich library.
Setup:
-
Get API keys for each user from Immich: Account Settings → API Keys → New API Key
-
Add users to your
.envfile:./scripts/add-import-user.sh rusty "your-api-key-here" ./scripts/add-import-user.sh lauren "laurens-api-key"
Or manually add to
.env:IMPORT_USER_RUSTY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx IMPORT_USER_LAUREN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
-
Build and start:
docker compose up -d --build import_watch
-
Share the import folder on your network (optional):
- The default location is
/srv/immich/import - Share via Samba for Windows access
- The default location is
Folder structure:
/srv/immich/import/
├── rusty/ # Drop files here → imports to Rusty's library
├── lauren/ # Drop files here → imports to Lauren's library
├── miller/
├── hunter/
└── harper/
Optional settings in .env:
| Variable | Default | Description |
|---|---|---|
IMPORT_WATCH_DIR |
/srv/immich/import |
Watch folder location |
IMPORT_SCAN_INTERVAL |
30 |
Seconds between scans |
IMPORT_DELETE_AFTER |
true |
Delete files after import |
☁️ Proton Drive Backup (Multi-User)
Backup each user's photos to their own Proton Drive account automatically.
Prerequisites:
-
Install rclone:
curl https://rclone.org/install.sh | sudo bash apt install jq # For user ID lookup
-
Configure a Proton Drive remote for each user:
rclone config # Name: proton_rusty (lowercase, matches PROTON_USER_RUSTY) # Storage: protondrive # Follow prompts to authenticate with Rusty's Proton account # Repeat for each user: # proton_lauren, proton_miller, etc.
-
Get Immich user IDs:
- Go to Immich → Administration → Users
- Click on a user
- Copy the UUID from the URL:
/admin/users/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
-
Add to your
.env:# Proton emails (determines rclone remote name) PROTON_USER_RUSTY=rusty@proton.me PROTON_USER_LAUREN=lauren@protonmail.com # Immich user IDs (maps username to library folder) PROTON_IMMICH_ID_RUSTY=abc12345-6789-... PROTON_IMMICH_ID_LAUREN=def98765-4321-...
-
Test with a dry run:
./scripts/backup-to-proton.sh --dry-run
-
Schedule daily backups (add to crontab):
crontab -e # Add this line: 0 3 * * * /path/to/homelab/scripts/backup-to-proton.sh
How it works:
/srv/immich/library/
├── abc12345-.../ → proton_rusty:Photos/Immich/
├── def98765-.../ → proton_lauren:Photos/Immich/
└── ...
Each user's Immich library syncs to their personal Proton Drive with incremental updates.
Optional settings in .env:
| Variable | Default | Description |
|---|---|---|
IMMICH_LIBRARY |
/srv/immich/library |
Immich library location |
PROTON_DEST_FOLDER |
Photos/Immich |
Folder in Proton Drive |
PROTON_LOG_FILE |
/var/log/proton-backup.log |
Log file location |
| Command | Description |
|---|---|
docker compose logs -f |
View live logs |
docker compose restart |
Restart all services |
./scripts/update.sh |
Pull latest images & restart |
./scripts/verify.sh |
Check stack health |
./scripts/import-photos.sh |
Bulk import photos (requires API key in .env) |
./scripts/backup-to-proton.sh |
Backup photos to Proton Drive |
./scripts/reset.sh |
Reset to fresh state |
docker compose down |
Stop everything |
Tunnel not connecting
Verify your TUNNEL_TOKEN in .env matches the token from Cloudflare Zero Trust → Tunnels.
HTTPS certificate errors
Verify your CF_API_TOKEN has DNS edit permissions for your zone.
NFS mount fails or system hangs
- Verify NAS IP and share path with
showmount -e <NAS_IP> - Check NAS permissions allow your server's IP
- Ensure
nfs-commonis installed:sudo apt install nfs-common - Use
softmount option to prevent hangs: addsoft,timeo=30,retrans=3to fstab
Photos not appearing
Verify UPLOAD_LOCATION exists and has correct permissions:
ls -la /mnt/photosNeed to start over?
./scripts/reset.shThis removes all config and lets you re-run the installer fresh.
MIT © Rusty Green