How to Deploy Immich on a VPS
Updated Jul 2026
Step-by-step guide to self-hosting Immich on your own VPS with Docker Compose — a self-hosted Google Photos alternative with auto-backup, face and object search.
- A VPS with at least 4 GB RAM (6 GB comfortable — the ML container loads models)
- A fresh Ubuntu 24.04 server with root/sudo SSH access
- A domain you can point at the server (for HTTPS and the mobile apps)
- Generous disk or attached block storage — your photo library grows without bound
What Immich is
Immich is an open-source, self-hosted photo and video backup platform — a direct replacement for Google Photos, running on a box you own. The iOS and Android apps back up your camera roll automatically in the background, just like the app you're leaving. Once your library is on the server you get a scrollable timeline, albums, shared links, machine-learning face recognition, and object/scene search ("beach", "dog", "receipts") that runs locally instead of in someone else's data center.
The appeal is straightforward: your photos stop being a subscription. Google charges you monthly, forever, for storage you don't control and can't move cleanly. A VPS running Immich costs the same whether you store 10 GB or 500 GB — you pay for the server and the disk under it, and the library is yours to back up, migrate, or delete on your terms.
One honest caveat before you start: Immich has historically shipped fast, with
occasional breaking changes between releases. That pace is great for features —
face search and the mobile apps improved enormously over a short span — but it
means you should pin to a specific release tag rather than :latest, and read
the release notes before every upgrade. The Upgrades section below covers how.
Treat this as a service you maintain deliberately, not one you install and forget.
Server sizing — RAM floor, disk is the real question
Immich's realistic minimum is 4 GB RAM; 6 GB is comfortable. The reason isn't the web app — it's the machine-learning container, which loads models into memory to power face recognition and smart search. On a 2 GB box that container gets OOM-killed the moment it tries to run a job, and you'll see search and face detection silently fail. Four gigabytes is the floor for the whole stack running together; six gives the ML worker room to breathe while thumbnails and transcodes happen alongside it.
But RAM is the easy part. The constraint that actually decides your setup is
disk. A photo and video library grows without bound — every phone backing up to
the server adds gigabytes a month, and video is brutal. This is the number to
plan around, and it's why UPLOAD_LOCATION (the folder Immich stores originals in)
should point at your largest, most expandable storage, not the small root
volume the OS lives on.
That single fact is why we point people at Kamatera for this workload: you can dial the exact RAM you need and attach block storage on demand, growing the disk as the library grows without rebuilding the server. Hetzner is the value pick — a plan with 4 GB RAM and a large local disk (or an attached Volume) covers most households cheaply. Whichever you choose, size the disk for where the library will be in a year, not where it starts. Running out of space mid-backup is the most common way to have a bad day with Immich.
Prepare the server
Immich runs as a set of Docker containers, so the base layer is the same one every
guide on this site assumes. If you haven't set up a box yet, work through
Docker & Compose on Ubuntu first — it
walks through a fresh Ubuntu 24.04 server, a non-root deploy user, Docker Engine
and the Compose plugin, a ufw firewall, and automatic security updates. This
guide picks up from there and won't repeat those steps.
The one firewall detail specific to Immich: the web UI listens on port 2283, but you should not open that port to the world. You'll put a reverse proxy in front (next section) and expose only 80 and 443. Keep 2283 reachable on localhost only. If you followed the base guide, your firewall already allows just SSH, 80, and 443 — which is exactly right here.
Log in as your non-root user and make a directory for the stack:
mkdir ~/immich && cd ~/immich
Install Immich
Immich's official install is a Docker Compose stack. You download two files — the
release docker-compose.yml and the matching example.env — into your directory,
edit the env file, and bring it up:
wget -O docker-compose.yml https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
wget -O .env https://github.com/immich-app/immich/releases/latest/download/example.env
Now edit the .env file. The two settings that matter most:
nano .env
UPLOAD_LOCATION— the host path where your originals live. Point this at your large/expandable disk (see sizing above). If you attached block storage mounted at, say,/mnt/photos, setUPLOAD_LOCATION=/mnt/photos/immich. This is the single most important line in the file; getting it wrong means your library lands on the wrong volume and moving it later is a chore.DB_PASSWORD— set a strong, unique password for the database. Don't leave the example value.
Leave the other defaults unless you have a reason to change them. Then start it:
docker compose up -d
The first up pulls several images and can take a few minutes. What you're
bringing online is four cooperating containers:
immich-server— the API and web UI (port 2283),immich-machine-learning— the model worker for face recognition and smart search,- a Redis/Valkey cache — job queues and session state,
- a Postgres image with a vector extension — and this one is not optional.
Why the special database image matters: Immich stores ML embeddings —
numerical fingerprints of faces and image content — and searches them with vector
similarity. That needs a Postgres built with a vector extension (pgvecto.rs /
VectorChord). You must use the Postgres image Immich ships in its compose file,
not a plain postgres:16. Swapping in vanilla Postgres is a common mistake that
leaves the server unable to start or search broken — the extension simply isn't
there. Leave that service exactly as the release compose file defines it.
Confirm everything came up healthy:
docker compose ps
docker compose logs -f immich-server
When the server log settles, browse to http://YOUR_SERVER_IP:2283 for a first
look — but don't stop here. You want HTTPS before you point a phone at it.
HTTPS + domain
Serving over http://IP:2283 is fine for a first test, but you need real HTTPS
before going further — and not just for tidiness. The mobile apps expect a
secure connection to back up reliably, and you don't want your entire photo
library traveling over plain HTTP. A reverse proxy in front of Immich terminates
TLS and forwards to port 2283 internally.
The clean way to do this is Caddy, which issues and renews Let's Encrypt
certificates automatically. Follow the companion guide,
Automatic HTTPS with Caddy, and proxy your
domain to Immich's port. First point an A record for your hostname (say
photos.example.com) at the server's public IP, then the Caddyfile is essentially:
photos.example.com {
reverse_proxy 127.0.0.1:2283
}
Caddy requests a certificate on first load (ports 80 and 443 must be reachable —
which your firewall already allows), renews it automatically, and forwards traffic
to Immich over localhost. Once DNS resolves and the certificate issues, load
https://photos.example.com and you're ready to onboard.
First-run setup
The first time you load Immich over your domain, it shows an onboarding screen.
Create your admin account immediately. As with any self-hosted app whose signup is open until the first user exists, whoever registers first owns the server — so do it the moment the install is reachable, before you walk away. Set a strong password; this account controls every library on the box.
Once you're in, the admin panel lets you set the server URL, storage template, and whether new users can self-register (turn that off unless you're inviting others). Then install the Immich app on your phone from the App Store or Play Store:
- Open the app and enter your server URL — the full
https://photos.example.com, the same address you use in the browser. This is why the reverse proxy and a valid certificate matter; the app connects to that HTTPS endpoint. - Log in with the account you just created.
- Grant photo access and enable background backup. The app uploads your camera roll and keeps new photos flowing to the server automatically.
The first backup of a large existing library takes a while — it's uploading everything — and the ML container will churn through face detection and search indexing in the background afterward. That initial indexing pass is CPU- and memory-hungry; it's normal, and it settles once caught up.
Backups
There are two things to back up, and skipping either one loses data you can't reconstruct.
1. The Postgres database. This holds all your metadata — albums, shared links,
detected faces and people, search indexes, users. The originals on disk are
useless without it; you'd have a folder of files and no timeline, no faces, no
albums. Immich documents a database dump using pg_dumpall against its database
container. A dump looks like:
docker exec -t immich_postgres pg_dumpall --clean --if-exists --username=postgres \
| gzip > "immich-db-$(date +%F).sql.gz"
Run that on a schedule (a cron job on the host) and — critically — copy the dump off the box to S3-compatible storage or another machine. A backup that only lives on the same server dies with the server. Follow Immich's current documentation for the exact dump/restore commands, since the database image and recommended flags can change between releases.
2. The UPLOAD_LOCATION folder. This is the actual originals — the photos and
videos themselves. Back up that entire directory (an rsync to off-box storage,
or your provider's block-storage snapshots) on its own schedule. The database
points at these files; both halves have to survive together.
One habit worth building: test a restore. Spin the dump and a copy of the upload folder up somewhere and confirm the library actually comes back. An untested backup is a guess, and photos are the kind of data you only discover was un-restorable at the worst possible moment.
Upgrades
Because Immich moves quickly, upgrades are a deliberate step, not a reflex.
Pin your version. Rather than tracking :latest, set an explicit release tag
so you control exactly when you move. Immich's compose file uses the
IMMICH_VERSION variable in .env for this — set it to a specific release (for
example IMMICH_VERSION=v1.xxx.x) instead of release/latest. That way a
docker compose pull doesn't silently jump you across a breaking change.
To upgrade, when you've decided to:
cd ~/immich
# read the release notes for every version between yours and the target first
docker compose pull
docker compose up -d
Read the release notes before every upgrade, and back up first. Immich has
occasionally shipped changes that require a migration step or a specific upgrade
path — jumping several versions blind is how people get stuck. Move one release
line at a time when a version calls for it, confirm the server comes back healthy
(docker compose logs -f immich-server), and only then delete the old backup. If
an upgrade goes sideways, your pre-upgrade database dump plus the untouched
UPLOAD_LOCATION folder are your way back.
Troubleshooting
Search or face recognition silently does nothing / the ML container keeps
restarting. Almost always memory. The immich-machine-learning container needs
room to load its models, and on an undersized box it gets OOM-killed the moment a
job runs. Check with docker compose logs immich-machine-learning and
docker stats — if it's dying under memory pressure, this is the single best
argument for the 4 GB (6 GB comfortable) sizing above. Size up.
Server won't start after swapping the database image. If you replaced Immich's
provided Postgres with a plain postgres image, the vector extension isn't
present and the server can't initialize its schema. Restore the exact database
service from the release docker-compose.yml. Immich needs its own vector-enabled
Postgres — this is not interchangeable with stock Postgres.
no space left on device / backups and uploads start failing. The library
filled the disk. Check df -h and confirm UPLOAD_LOCATION is pointing at your
large volume, not the root filesystem. This is the most common Immich failure over
time and the reason sizing leans on expandable storage — extend the block volume
(trivial on Kamatera or via a Hetzner Volume) and Immich keeps going.
Mobile app won't connect or backup stalls. Confirm you entered the full
https:// server URL and that the certificate is valid — a self-signed or expired
cert makes the app refuse to sync. Load the same URL in a browser; if it warns
about the certificate, fix the reverse proxy (see the Caddy guide) before blaming
the app.
Verification + next steps
You're done when you can: load Immich over HTTPS on your own domain, log in as
admin, sign in from the mobile app and see a photo you just took auto-upload to
the timeline, and confirm both a database dump and a copy of UPLOAD_LOCATION
are landing in off-box storage.
From here, invite family members, build shared albums, and let the ML worker index faces in the background. The one thing that makes or breaks Immich over the long haul is the box underneath it — specifically the disk. Hetzner is the value pick when you can size a large disk up front; reach for Kamatera when you want to dial RAM precisely and grow block storage on demand as the library expands, which for a photo server it always will. See Best VPS for self-hosting for the ranked picks.