How to Install Immich on Ubuntu 24.04 — Self-Host Your Photo Library
Cloud photo services are convenient until they stop being convenient. Prices climb, features disappear behind new tiers, your account gets auto-scanned, and one day Google decides your medical photos look suspicious and locks you out with no appeal. Immich is the community-built escape hatch: a self-hosted photo and video backup platform that looks and feels like Google Photos but runs entirely on your own VPS. Automatic mobile uploads, facial recognition, natural-language search, shared albums, memories, and a polished iOS and Android app — all of it running on infrastructure you control.
This guide walks you end to end: a clean Ubuntu 24.04 VPS becomes a production-ready Immich server in about 35 minutes, with TLS, reverse proxy tuning for multi-gigabyte video uploads, machine learning enabled, mobile apps configured, and a repeatable backup routine.
Recommended plan: Immich is memory-hungry once machine learning kicks in. The CloudCore Professional plan at EUR 19.99/month gives you 6 vCPU, 12 GB RAM, and 100-200 GB NVMe — the sweet spot for a single household or small team photo library.
Table of Contents
Why Self-Host Instead of Using Google Photos or iCloud?
Every major cloud photo service starts cheap and gets expensive once your library grows beyond a single phone. Here is how the cost curve actually looks once you are a household of three with a decade of photos and video:
| Storage tier | Google One | iCloud+ | Immich on CloudCore Professional |
|---|---|---|---|
| 200 GB | EUR 2.99/mo | EUR 2.99/mo | Included (EUR 19.99 flat) |
| 2 TB | EUR 9.99/mo | EUR 9.99/mo | Included (add block storage EUR 5/mo) |
| 6 TB | EUR 24.99/mo | EUR 29.99/mo | EUR 19.99 + storage volume |
| 12 TB | EUR 49.99/mo | Not available | EUR 19.99 + storage volume |
| Family members | 5 max | 5 max | Unlimited |
| Who reads your photos? | Google ML scans all content | Apple scans for CSAM | Nobody. You hold the keys. |
| Moving to another service | Export via Takeout (hours) | Manual export per device | rsync, you own the files |
| Account shutdown risk | Real — automated, no human review | Real — same story | None. Your VPS, your rules |
Cost at scale. A 2 TB Google One plan is EUR 120/year forever. A CloudCore Professional VPS costs EUR 240/year total — and that same VPS also runs your Jellyfin media server, your Nextcloud, your Nginx, and any other self-hosted tool you want. Past the 6 TB point, Immich on a VPS with attached block storage is flatly cheaper than any cloud equivalent, and the savings compound every year.
GDPR by default. For EU users, keeping family photos on an EU-hosted VPS under your name means you never signed them over to a US ad-tech company. There is no shadow profile, no facial embeddings in a corporate database, no training-data hoovering.
What You Get With Immich
Immich is not a minimal viable product. In feature parity with Google Photos, it has shipped almost everything the big players offer:
- Automatic mobile backup from iOS and Android, background upload on new photos
- CLIP-powered semantic search — type "red car on a beach" and get matching photos from a decade ago
- Face recognition and clustering — group people automatically, tag them, merge clusters
- Albums, smart albums, and nested albums with custom cover photos
- Shared links with optional expiry, password, and download toggles for non-users
- Memories — "on this day" surfaced automatically
- Map view with EXIF-based geolocation clustering
- Video transcoding via integrated FFmpeg for web-friendly playback
- RAW support — DNG, CR2, NEF, ARW, and others
- External libraries — point Immich at an existing NAS folder without reuploading
- Multi-user with per-user quotas
- Hardware-accelerated ML on NVIDIA, Intel, and ARM
Prerequisites
Before you start, confirm you have the following in place:
- A VPS running Ubuntu 24.04 LTS with root or sudo access
- At least 4 vCPU cores and 8 GB of RAM (6 vCPU / 12 GB strongly recommended for smooth ML)
- At least 50 GB of free disk for the OS and container images, plus enough space for your photo library (plan 4 MB per photo, 30-100 MB per minute of 4K video)
- A domain name pointed to the server's public IP (for TLS via Let's Encrypt)
- Ports 80 and 443 open in your firewall
- SSH access from your workstation
ssh root@your-server-ipIf you are not running as root, prefix subsequent commands with sudo.
Step 1: Prepare the Server
Start with a clean, fully patched system. Update the package index, upgrade installed packages, and install a small set of utilities you will need along the way.
apt update && apt upgrade -y
apt install -y curl wget gnupg2 ca-certificates lsb-release ufw gitCreate a dedicated user for running the stack. Running Docker Compose as root is possible but unnecessary — the Immich containers set their own internal UIDs, and keeping the host-side service files under a non-root account is cleaner for backups and permissions.
adduser --disabled-password --gecos "" immich
usermod -aG sudo immichConfigure the firewall to allow SSH and HTTPS traffic:
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw --force enableSet the timezone so EXIF dates and memory cards line up correctly:
timedatectl set-timezone Europe/BerlinReplace Europe/Berlin with your actual timezone (use timedatectl list-timezones to browse).
Step 2: Install Docker and Docker Compose
Immich ships exclusively as a Docker Compose stack, so Docker Engine with the Compose v2 plugin is mandatory. Add Docker's official apt repository, then install the latest Engine and plugins in one shot.
install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc chmod a+r /etc/apt/keyrings/docker.ascecho "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \ https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \ | tee /etc/apt/sources.list.d/docker.list > /dev/null
apt update apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Add the immich user to the docker group so you do not need sudo for every Compose command:
usermod -aG docker immichVerify the install:
docker --version
docker compose versionExpected output (versions may differ):
Docker version 27.3.1, build ce12230
Docker Compose version v2.29.7For a deeper walkthrough of Docker itself, see our companion guide: How to Install Docker on Ubuntu 24.04.
Step 3: Download the Immich Compose Files
Immich publishes its official docker-compose.yml and an example.env alongside each release. Rather than cloning the entire repository, you pull the two files directly into a deployment directory.
Switch to the immich user and create a working directory:
su - immich
mkdir -p ~/immich && cd ~/immichDownload the latest release assets:
wget -O docker-compose.yml https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
wget -O example.env https://github.com/immich-app/immich/releases/latest/download/example.envCopy the example env to your real env file:
cp example.env .envTake a quick look at what you downloaded:
ls -laYou should see four files: docker-compose.yml, example.env, .env, and eventually your upload directory.
Step 4: Configure the Environment
The .env file controls every adjustable setting. Open it in your editor of choice:
nano .envThree variables matter most:
# Where photo and video originals are stored on the host
UPLOAD_LOCATION=/home/immich/immich/libraryWhere PostgreSQL data lives
DB_DATA_LOCATION=./postgresTag for the container images — pin to a known-good release rather than "release"
IMMICH_VERSION=v1.119.0Database credentials (generate a strong random password)
DB_PASSWORD=CHANGE_ME_TO_A_LONG_RANDOM_STRINGThese defaults work but you can override them
DB_USERNAME=postgres
DB_DATABASE_NAME=immichGenerate a strong database password
Never keep the default password. Generate one with:
openssl rand -base64 36Copy the result into DB_PASSWORD. Save and close (Ctrl+O, Enter, Ctrl+X in nano).
Pick your UPLOAD_LOCATION carefully
UPLOAD_LOCATION is the directory on the host that will hold every original photo and video, plus the generated thumbnails and encoded video proxies. It is the single most important path in your entire deployment — this is what you back up, what you rsync off-site, and what balloons to hundreds of gigabytes over time.
Best practice:
- On the main disk:
/home/immich/immich/libraryworks fine for libraries under 100 GB - On a mounted block storage volume:
/mnt/photos/library— ideal for large libraries, mount before starting Immich - Never put it inside
./postgresor the Immich config directory; keep media and database storage fully separate
mkdir -p /home/immich/immich/libraryExternal block storage for large libraries
If you attached a 1 TB volume and mounted it at /mnt/photos, set:
UPLOAD_LOCATION=/mnt/photos/libraryAnd ensure the immich user can write to it:
sudo chown -R immich:immich /mnt/photosStep 5: Launch the Immich Stack
With the environment configured, start the full stack in detached mode:
docker compose up -dThe first run pulls five images — about 3 GB total — and initializes the Postgres database. On a Professional plan this takes two to four minutes.
Watch progress with:
docker compose logs -fPress Ctrl+C to detach from the log stream (the containers keep running).
Check that all services came up:
docker compose psExpected output:
NAME IMAGE STATUS
immich_machine_learning ghcr.io/immich-app/immich-machine-learning:v1.119 Up 1 minute (healthy)
immich_postgres ghcr.io/immich-app/postgres:14-vectorchord Up 1 minute (healthy)
immich_redis docker.io/redis:6.2-alpine Up 1 minute (healthy)
immich_server ghcr.io/immich-app/immich-server:v1.119 Up 1 minute (healthy)All four containers must report healthy. If any shows unhealthy, jump to the Troubleshooting section.
Verify the web UI is serving on port 2283:
curl -I http://localhost:2283Expected:
HTTP/1.1 200 OKStep 6: First User Signup
The first person to register on a fresh Immich instance becomes the administrator — no invite code, no email verification. This happens before you put Nginx and TLS in front, so you do it over a temporary SSH tunnel to keep the signup off the public internet.
From your local workstation, open an SSH tunnel that forwards port 2283:
ssh -L 2283:localhost:2283 root@your-server-ipLeave that terminal open, then visit http://localhost:2283 in your browser.
You will see the Immich welcome screen. Click Get Started and fill in:
- Admin email (used for login; receives no mail by default)
- Admin password (use a password manager — this protects your entire photo library)
- Name (shown in the UI)
Create additional user accounts
If family members will share this server, create a user for each one now so they can install the mobile app against their own account.
Go to Administration > Users > Create user. Each user gets:
- Email (login identifier)
- Password (give them something temporary; they can change it)
- Name
- Optional storage quota in gigabytes
Step 7: Mobile App Setup and Auto Backup
Automatic backup from your phone is the killer feature. It runs silently in the background, uploads on Wi-Fi only by default, and makes Immich actually feel like Google Photos rather than a manual upload tool.
iOS
http://your-server-ip:2283 while you test — you will change this to your HTTPS domain after Step 10)iOS aggressively kills background tasks. The Immich maintainers document a workaround: enable Background App Refresh for Immich in iOS Settings, and keep the app in your recent apps list rather than force-quitting it. Background uploads happen reliably when the phone charges overnight.
Android
Android background uploads are far more reliable than iOS and will pick up new photos within minutes of capture.
Verify uploads are working
Take a photo on your phone, wait 30-60 seconds, and refresh the Immich web UI. The new photo should appear in the timeline. If it does not, check Settings > Backup in the mobile app for upload status and pending count.
Step 8: Machine Learning — CLIP Search and Face Recognition
The Immich machine learning container runs three models by default:
- CLIP (Contrastive Language-Image Pretraining) for natural-language search
- Face detection using RetinaFace
- Face recognition using ArcFace to cluster faces into people
Trigger a full library rescan
Once you have seeded the library with a few hundred photos, go to Administration > Jobs in the web UI. From there you can:
- Run Smart Search (CLIP) across all photos to build embeddings
- Run Face Detection followed by Facial Recognition to populate the People view
- Run Thumbnail Generation if any previews are missing
- CLIP embedding: ~5-10 photos per second
- Face detection: ~3-5 photos per second
- Thumbnail generation: ~20-30 photos per second
Try the semantic search
Click the search bar and type something descriptive like:
sunset on a mountainbirthday cake with candlesred carperson holding a book
Identify and name faces
Open the People view (sidebar). Immich has already clustered similar faces. Click a face cluster and give it a name — from that point forward, you can search by person name, and new photos of that person are auto-tagged.
Merge clusters that belong to the same person (common for photos at different ages). Hide clusters that contain only strangers, mirrors, or statues.
GPU acceleration (optional)
If your VPS has an NVIDIA GPU, switch the ML container to the CUDA image for a 10-20x speedup. Edit docker-compose.yml and change the immich-machine-learning service to use :cuda:
immich-machine-learning:
image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}-cuda
extends:
file: hwaccel.ml.yml
service: cudaThen:
docker compose up -dSee immich.app/docs for OpenVINO (Intel) and ARM-NN (Raspberry Pi, Apple Silicon) variants.
Step 9: Albums and Shared Links
Albums in Immich are flat collections of photos you curate manually. Select photos from the timeline (shift-click for ranges, or lasso-drag), then Add to Album. Each album can have a custom cover, description, and start/end dates.
Shared albums
Inside an album, click Share, then Add users. Any Immich user on the same server can be invited as either Viewer or Contributor. Contributors can add their own photos to the album — perfect for family events where multiple people have their own copies.
Shared links (external)
For people who do not have an Immich account, create a public shared link:
Immich generates a URL like https://photos.yourdomain.com/share/abc123xyz. Anyone with the link views a gallery without seeing your main UI or creating an account.
Step 10: Nginx Reverse Proxy with TLS
Serving Immich on plain HTTP at port 2283 works on a LAN but is unacceptable over the public internet. You want a proper domain, an HTTPS certificate from Let's Encrypt, and a proxy tuned for very large uploads. Nginx handles all three.
Install Nginx and Certbot:
sudo apt install -y nginx certbot python3-certbot-nginxCreate the site configuration:
sudo tee /etc/nginx/sites-available/immich > /dev/null <<'EOF' server { listen 80; server_name photos.yourdomain.com;# Certbot writes a challenge file to this path during cert issuance location /.well-known/acme-challenge/ { root /var/www/html; }
# Redirect everything else to HTTPS location / { return 301 https://$host$request_uri; } }
server { listen 443 ssl http2; server_name photos.yourdomain.com;
# Certbot fills in these paths in the next step ssl_certificate /etc/letsencrypt/live/photos.yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/photos.yourdomain.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5;
# Critical: allow multi-gigabyte video uploads client_max_body_size 5G;
# Long timeouts for big file uploads proxy_read_timeout 600s; proxy_send_timeout 600s; send_timeout 600s;
# Disable buffering so uploads stream through to Immich proxy_request_buffering off; proxy_buffering off; proxy_http_version 1.1;
# Required for WebSocket notifications proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";
proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme;
location / { proxy_pass http://127.0.0.1:2283; } } EOF
Replace photos.yourdomain.com with your actual domain. Enable the site and issue the certificate:
sudo ln -s /etc/nginx/sites-available/immich /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t
sudo certbot --nginx -d photos.yourdomain.com --non-interactive --agree-tos -m [email protected]
sudo systemctl reload nginxCertbot auto-renews via the installed systemd timer — no further action needed.
Update the mobile app
Now that HTTPS works, open the Immich app on each phone, go to Settings > Server URL, and change it from http://your-server-ip:2283 to https://photos.yourdomain.com.
Why 5 GB body size matters
Modern phones produce 4K 60fps video and ProRes clips from pro cameras that easily exceed 2 GB per file. A default Nginx install caps uploads at 1 MB, so any large video upload fails silently with a 413 error. Setting client_max_body_size 5G gives comfortable headroom; raise it further if you regularly archive RAW photo bursts or hours-long video files.
Step 11: Backups — rsync and pg_dump
Immich's data has two parts: the media files under UPLOAD_LOCATION and the Postgres database that indexes them, stores thumbnails-to-original mappings, and holds face embeddings. Both must be backed up together, or a restored library will not match its metadata.
Back up the database with pg_dump
Immich ships a recommended dump command. Create a helper script at /home/immich/backup-db.sh:
tee /home/immich/backup-db.sh > /dev/null <<'EOF' #!/bin/bash set -eBACKUP_DIR=/home/immich/backups/db STAMP=$(date +%Y-%m-%d_%H-%M-%S) mkdir -p "$BACKUP_DIR"
docker exec -t immich_postgres \ pg_dumpall --clean --if-exists --username=postgres \ | gzip > "$BACKUP_DIR/immich-$STAMP.sql.gz"
Keep the last 14 daily dumps
find "$BACKUP_DIR" -name "immich-*.sql.gz" -mtime +14 -delete EOF
chmod +x /home/immich/backup-db.sh
Test it once manually:
/home/immich/backup-db.sh
ls -lh /home/immich/backups/db/You should see a compressed SQL dump, typically 20-500 MB depending on library size.
Mirror media with rsync
For off-site media backup, rsync to another VPS, a home NAS, or cloud storage. Example to a remote NAS over SSH:
tee /home/immich/backup-media.sh > /dev/null <<'EOF' #!/bin/bash rsync -avz --delete \ --exclude='encoded-video/' \ --exclude='thumbs/' \ --exclude='upload/' \ /home/immich/immich/library/ \ [email protected]:/volume1/immich-backup/ EOF
chmod +x /home/immich/backup-media.sh
The excluded directories are regenerated by Immich from originals, so backing them up wastes space. Back up the library directory only — originals plus sidecars plus external libraries.
Schedule everything with cron
Edit the immich user's crontab:
crontab -eAdd:
# Database dump at 02:15 every night
15 2 * /home/immich/backup-db.sh >> /home/immich/backups/backup.log 2>&1Media rsync at 03:00 every night
0 3 * /home/immich/backup-media.sh >> /home/immich/backups/backup.log 2>&1Restore procedure (for reference)
If disaster strikes:
# Stop the stack
cd ~/immich && docker compose downWipe and recreate the database volume
docker volume rm immich_immich_postgres || true
docker compose up -d databaseRestore the dump
zcat /home/immich/backups/db/immich-2026-04-15_02-15-00.sql.gz \
| docker exec -i immich_postgres psql --username=postgresRestore the media
rsync -avz backup@nas:/volume1/immich-backup/ /home/immich/immich/library/Start everything
docker compose up -dTest this procedure on a staging VPS at least once — an untested backup is an assumption, not a backup.
Upgrading Immich
Immich releases roughly every two weeks with new features and schema migrations. Upgrades are straightforward:
cd ~/immich1. Read the release notes first — some releases have breaking changes
Visit https://github.com/immich-app/immich/releases
2. Update the version pin in .env
nano .env
change IMMICH_VERSION=v1.119.0 to the new version
3. Pull and restart
docker compose pull
docker compose up -dSchema migrations run automatically on startup. Always take a fresh pg_dump immediately before upgrading — if a migration fails, you can revert by restoring the dump and pinning back to the previous version.
Troubleshooting
| Problem | Likely cause | Fix |
|---|---|---|
413 Request Entity Too Large on large videos | Nginx body limit too low | Raise client_max_body_size to 5G or higher, reload Nginx |
| Mobile app says "cannot connect" after adding TLS | Old HTTP URL cached | Update server URL in app settings, force-close and reopen |
| ML jobs stuck at 0% progress | immich_machine_learning unhealthy, model download failed | docker compose logs immich-machine-learning, restart service; ensure outbound HTTPS is allowed from the container |
| Postgres container restart loop | Bad DB_PASSWORD change after first run | Passwords are baked into the volume on first init — either revert the .env change or wipe the postgres volume and restore from pg_dump |
| Uploads succeed but photos do not appear | Storage template job backlog | Administration > Jobs, force-run Storage Template and Metadata Extraction |
| "Out of disk space" errors | UPLOAD_LOCATION filesystem full | Expand the volume or move UPLOAD_LOCATION to a larger mount and rsync existing data |
| Face recognition groups strangers together | Default confidence threshold too low | Admin > Settings > Machine Learning > raise Facial Recognition min confidence to 0.8 |
| Slow first page load over HTTPS | Large EXIF reads on cold cache | Normal on first visit after restart; subsequent loads use Redis cache |
View service logs
cd ~/immich
docker compose logs -f immich-server
docker compose logs -f immich-machine-learning
docker compose logs -f redis
docker compose logs -f databaseReset a forgotten admin password
docker exec -it immich_server /bin/bash
inside the container:
node /usr/src/app/dist/main reset-admin-passwordFAQ
How much disk space do I need for Immich?
Plan for roughly 4 MB per smartphone photo and 30-100 MB per minute of 4K video. A typical user backing up 10 years of photos needs 500 GB to 2 TB. The CloudCore Professional plan starts with 100-200 GB NVMe, and you can attach block storage for larger libraries — EUR 5/month per 200 GB is the typical rate.
Is Immich production ready?
Immich is under heavy active development and still labeled beta by the maintainers, but tens of thousands of users rely on it daily for their primary photo archive. As long as you follow the documented backup strategy (database dump plus media rsync, tested restore), it is safe for personal and small-team use. Read every release note before upgrading, especially when minor versions change.
Can Immich replace Google Photos completely?
Yes for most users. Immich offers automatic mobile backup, face recognition, object and text search via CLIP, albums, shared links, memories, map view, and polished iOS and Android apps. A few Google Photos features do not exist yet: photo books printing, family-group management, and the deeply-integrated Google Lens features. If those matter to you, keep Google Photos as a secondary and use Immich as your sovereign primary archive.
Does Immich support GPU acceleration for machine learning?
Yes. Immich supports NVIDIA CUDA, Intel OpenVINO, and ARM NN (Apple Silicon, Raspberry Pi) for the machine learning container. On CPU, CLIP indexing processes roughly 5-20 photos per second. With a modest NVIDIA GPU you can index tens of thousands of photos in minutes instead of hours. See immich.app/docs for per-backend setup.
How do I migrate from Google Photos to Immich?
Use Google Takeout to download your library as a zip (choose 50 GB split files for easier handling). Extract all files to a staging folder, then use the Immich CLI:
npm install -g @immich/cli
immich login https://photos.yourdomain.com YOUR_API_KEY
immich upload --recursive /path/to/takeoutThe CLI reads Google's JSON sidecar files to restore original capture dates, preserves EXIF, and skips duplicates on retry. A 100 GB Takeout typically imports in 8-20 hours depending on network and disk.
Why does the upload fail for large videos?
Almost always Nginx. The default client_max_body_size is 1 MB, which blocks anything larger than a high-res photo. Raise it to 5 GB or more with client_max_body_size 5G; in your server block and reload Nginx. The Immich API itself has no hard upload limit beyond available disk.
Can multiple family members share one Immich server?
Yes, and this is where self-hosting shines. The admin creates additional users from Administration > Users, each with their own private library and optional storage quota. Users can then share albums with each other, contribute to joint albums (wedding, vacation, birthdays), and invite external viewers via shared links. One VPS, one monthly bill, unlimited family members.
Next Steps
You now have a production Immich instance with mobile backup, semantic search, face recognition, and proper backups. A few natural follow-ons:
- Pair Immich with a full media server. Immich handles photos and home videos; pair it with Jellyfin for movies and TV shows on the same VPS.
- Add a personal cloud for documents and files. Immich specializes in photos — for everything else (documents, contacts, calendars, file sync), install Nextcloud alongside it on the same server.
- Harden the server with Fail2Ban and CrowdSec — see How to Install CrowdSec on Ubuntu 24.04.
- Automate off-site backups to Backblaze B2 or a second VPS in a different region. Your media on one VPS plus nightly rsync to another location gives you the 3-2-1 backup strategy professionals actually use.
- Browse the upstream docs at immich.app/docs for advanced topics: external libraries, hardware video transcoding, OAuth SSO, and the REST API.
Need more headroom? The CloudCore Professional plan at EUR 19.99/month gives you 6 vCPU, 12 GB RAM, and 100-200 GB NVMe — comfortable for a family photo archive plus Jellyfin, Nextcloud, and a handful of other services on one VPS. Launch your server and have Immich running in under an hour.