Immich is a self-hosted photo and video library with mobile backup, timeline, face recognition, search, thumbnails, and video transcoding. It is not merely a file browser: initial imports run CPU-heavy machine-learning and media jobs, and the generated assets consume additional disk.
The Immich project explicitly warns against using it as the only copy of your photos and videos. Keep the originals or a separate backup even after this server is working.
VPS size, CPU, storage, and price
Immich's current upstream requirements are 6 GB minimum RAM and 2 CPU cores, with 8 GB RAM and 4 cores recommended.
| Item | Planning value |
|---|---|
| Minimum RAM | 6 GB with machine learning enabled; upstream notes that 4 GB can work only with machine learning disabled |
| Recommended RAM | 8 GB |
| Expected CPU usage | About 0.1–0.5 vCPU idle; an initial import, face detection, smart search, thumbnails, or software video transcodes can hold all 4 vCPU |
| Storage requirement | Library size plus roughly 10–20% for thumbnails/transcodes, 1–3 GB typical PostgreSQL data, images, logs, and free upgrade/backup working space |
| Exact LayerOne SKU | gc.medium — slug layerone-pro, 4 vCPU, 8 GB RAM, 160 GB disk |
| Expected monthly cost | $0.0205 per active hour; about $14.97 per 730 hours against the advertised $15.00 730-hour equivalent |
A 744-hour month is about $0.0205 × 744 = $15.25. On a 160 GB system disk,
a prudent starting library is much smaller than 160 GB after allowing for the
OS, containers, generated media, database, and safe free-space margin. Measure
your source library before ordering; a library already near 100 GB is a strong
signal to choose more disk.
Media mix, transcoding, machine-learning models, users, transfer, and storage growth vary. LayerOne bills the VPS hourly, and the CPU figures above are planning estimates rather than throughput guarantees.
Assumptions
- Ubuntu 22.04 or 24.04 on x86-64 with at least the x86-64-v2 CPU level required by current Immich machine-learning images
- Docker Engine and the Compose v2 plugin; Immich does not support the old
docker-composecommand. Follow Running Docker - A public IPv4 and DNS
Arecord forphotos.example.com - An SSH-key sudo user; replace
ADMIN_IPwith its source/32
1. Configure both firewalls
Allow TCP 22 from ADMIN_IP/32 and TCP 80/443 from 0.0.0.0/0 in the
LayerOne cloud firewall. Do not publish Immich's port 2283.
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow from ADMIN_IP to any port 22 proto tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose
The Compose edit below binds 2283 to loopback because Docker-published ports can bypass UFW. Keep the same outer policy in the LayerOne cloud firewall.
2. Download the official release files
Immich's supported production path is its release Compose file, not the experimental install script.
sudo install -d -m 0750 /opt/immich
sudo chown "$USER":"$USER" /opt/immich
cd /opt/immich
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
chmod 600 .env
Set explicit local paths and generate a database password using only the alphanumeric characters Immich permits in this value:
cd /opt/immich
sed -i 's|^UPLOAD_LOCATION=.*|UPLOAD_LOCATION=/opt/immich/library|' .env
sed -i 's|^DB_DATA_LOCATION=.*|DB_DATA_LOCATION=/opt/immich/postgres|' .env
IMMICH_DB_PASSWORD="$(openssl rand -hex 24)"
sed -i "s|^DB_PASSWORD=.*|DB_PASSWORD=${IMMICH_DB_PASSWORD}|" .env
unset IMMICH_DB_PASSWORD
grep -E '^(UPLOAD_LOCATION|DB_DATA_LOCATION|IMMICH_VERSION)=' .env
Do not print DB_PASSWORD into terminal transcripts. The current example file
uses the moving stable-major tag IMMICH_VERSION=v3: a later
docker compose pull can therefore fetch a newer v3 release. For stricter change control,
replace it with an exact release tag from Immich's release page and advance it
only after reviewing every intervening release note. Always use the Compose
file shipped with a release; the file on the repository's main branch may not
match the released images.
In the immich-server service's ports section, change the host mapping from
2283:2283 to loopback:
services:
immich-server:
ports:
- "127.0.0.1:2283:2283"
Do not publish PostgreSQL or Redis. They should remain reachable only on the Compose network. Validate the complete downloaded file after editing:
docker compose config >/dev/null
3. Install the HTTPS reverse proxy
sudo apt update
sudo apt install -y nginx certbot python3-certbot-nginx
Create /etc/nginx/sites-available/photos.example.com:
server {
listen 80;
server_name photos.example.com;
client_max_body_size 0;
location / {
proxy_pass http://127.0.0.1:2283;
proxy_http_version 1.1;
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;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_request_buffering off;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
}
}
sudo ln -s /etc/nginx/sites-available/photos.example.com /etc/nginx/sites-enabled/photos.example.com
sudo nginx -t
sudo systemctl reload nginx
sudo certbot --nginx -d photos.example.com
sudo certbot renew --dry-run
Unlimited request body size at Nginx avoids rejecting large videos; Immich's own application limits and available disk still apply.
4. Start, claim, and verify Immich
cd /opt/immich
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail=150
curl -I http://127.0.0.1:2283
curl -I https://photos.example.com
Open the HTTPS URL immediately. The first registered user becomes the administrator, so do not leave an unclaimed instance exposed. Create the administrator, then create an ordinary user for daily testing if appropriate.
Upload a small mix of JPEG, HEIC, and short video assets. In Administration, watch the job queues finish thumbnails, metadata, face detection, smart search, and video conversion. Check the host while the jobs run:
docker stats
df -h /opt/immich
du -sh /opt/immich/library /opt/immich/postgres
Verify the mobile app against https://photos.example.com, not the loopback
port. A successful web login alone does not prove background jobs, mobile
uploads, large videos, and generated previews work.
Security and reliability pitfalls
- Never expose PostgreSQL 5432, Redis 6379, or Immich 2283 publicly.
- Protect the first-user registration window and use a unique administrator password.
- Treat face embeddings, locations, EXIF data, and thumbnails as sensitive as the original media.
- Keep the PostgreSQL directory on the local Unix filesystem. Immich explicitly says not to put its database on a network share.
- Set upload and job concurrency conservatively. More parallel jobs can exhaust RAM and do not create more CPU.
- Immich is not a backup. Preserve another independent copy of every original.
Back up and upgrade
Immich's built-in database backups contain metadata and users, not the
photos and videos in UPLOAD_LOCATION. A usable recovery set needs all of:
- A current PostgreSQL backup produced by Immich's documented process.
- An off-server copy of
/opt/immich/library. - The protected
.envand the Compose file used for that version. - A written restore test showing the database and library match.
Do not copy live PostgreSQL files as your only database backup. Transfer media and database backups to another system, encrypt them, and test restoration on an isolated host. LayerOne does not take snapshots or backups; see Backups and snapshots.
Before updating, read every release note between versions and make a fresh recovery set:
cd /opt/immich
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail=150
Do not interrupt the first startup while database migrations are running. Then repeat login, upload, download, thumbnail, search, and mobile-client tests.