Self-Host Immich on a VPS with Docker Compose

Deploy Immich on a VPS as a self-hosted Google Photos replacement. Covers Docker Compose setup, mobile app backup, storage planning with real numbers, machine learning on CPU, database backup and restore, and a safe update workflow.

Immich is an open-source, self-hosted photo and video management platform. It handles automatic mobile backup, face recognition, smart search, and album sharing. If you want to stop sending your photos to Google, Immich is the closest self-hosted equivalent.

Running Immich on a European VPS means your photos stay on infrastructure you control. No third-party scanning, no training datasets, no surprise policy changes. For GDPR purposes, you are both the data controller and processor, which simplifies compliance when the data never leaves your server.

This guide deploys Immich on a remote VPS using Docker Compose. It assumes you already have Docker Engine with the Compose plugin installed and a reverse proxy running. If you need those first, see Docker in Production on a VPS: What Breaks and How to Fix It and Traefik vs Caddy vs Nginx: Docker Reverse Proxy Compared.

What do you need to run Immich on a VPS?

Immich needs at least 6 GB of RAM and 2 CPU cores for a full deployment with machine learning enabled. With ML disabled, 4 GB works. Storage depends on your photo library: plan for your raw library size plus 10-20% overhead for thumbnails and transcoded video, plus 1-3 GB for the PostgreSQL database. Docker Engine with the Compose plugin is required. Immich runs on Linux only.

Component Minimum Recommended
RAM 4 GB (ML disabled) 8 GB
CPU 2 cores 4 cores
Storage 50 GB (small library) 250 GB+
Software Docker Engine + Compose plugin Same
OS Any Linux with Docker Ubuntu 22.04/24.04, Debian 12

PostgreSQL needs local SSD storage. Never put the database on a network share. On a Virtua VPS, NVMe is the default, so this is already covered.

How do you deploy Immich with Docker Compose?

Download the official Docker Compose file and example environment file from the Immich release page, configure your storage paths and database password, then start the stack.

Create the project directory

mkdir -p /opt/immich && cd /opt/immich

Download the official files

Always pull these from the latest release. Do not copy-paste from blog posts (including this one) because Immich updates its Compose file frequently to match database image changes.

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

Configure the environment

Open the .env file:

nano .env

Set these values:

UPLOAD_LOCATION=/opt/immich/data
DB_DATA_LOCATION=/opt/immich/postgres
IMMICH_VERSION=v2.6.1
DB_PASSWORD=<your-strong-password>
TZ=Europe/Berlin

Generate a strong database password with only alphanumeric characters (A-Za-z0-9). Special characters cause Docker Compose parsing issues in .env files:

openssl rand -base64 32 | tr -dc 'A-Za-z0-9' | head -c 32; echo

Copy the output and paste it as the DB_PASSWORD value.

Pin IMMICH_VERSION to a specific release tag like v2.6.1 rather than using the v2 metatag. This prevents surprise upgrades when you run docker compose pull for other services. You control when Immich updates.

Create the data directories

mkdir -p /opt/immich/data /opt/immich/postgres

Remove the exposed port

The default Compose file exposes port 2283 directly. Since you are running a reverse proxy, remove or comment out the ports section in docker-compose.yml to keep Immich accessible only through the proxy:

sed -i "s/- '2283:2283'/# - '2283:2283'/" docker-compose.yml

Instead, connect Immich to your reverse proxy's Docker network. If your proxy network is called proxy:

cat >> docker-compose.yml << 'EOF'

networks:
  default:
  proxy:
    external: true
EOF

Then add the proxy network to the immich-server service. Edit docker-compose.yml and add under the immich-server service:

    networks:
      - default
      - proxy

This keeps the database and Redis on the internal network while exposing only the server to the reverse proxy.

Start the stack

docker compose up -d

Watch the logs to confirm all services start cleanly:

docker compose logs -f --tail=50

The server logs Immich Server is listening on http://[::1]:2283 once it is ready. The machine learning service loads its models next. Press Ctrl+C to exit the log stream.

Check that all four containers are running:

docker compose ps
NAME                    STATUS
immich_server           Up (healthy)
immich_machine_learning Up (healthy)
immich_redis            Up (healthy)
immich_postgres         Up (healthy)

All four should show Up (healthy) within a couple of minutes. The ML container takes the longest because it downloads models on first start.

Restrict file permissions

The .env file contains your database password. Lock it down:

chmod 600 .env
ls -la .env
-rw------- 1 root root 245 Mar 20 10:00 .env

How do you set up a reverse proxy for Immich?

Immich needs a reverse proxy for HTTPS and to handle large file uploads from mobile devices. You must set the request body size limit high enough for video uploads. Without this, uploads over the default limit (typically 1 MB in Nginx) silently fail.

For a full reverse proxy setup, see Traefik vs Caddy vs Nginx: Docker Reverse Proxy Compared. Here are the Immich-specific snippets.

Caddy

In your Caddyfile:

photos.example.com {
    reverse_proxy immich_server:2283
}

Caddy handles TLS automatically and has no default body size limit, so video uploads work out of the box.

Traefik

Add labels to the immich-server service in docker-compose.yml:

    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.immich.rule=Host(`photos.example.com`)"
      - "traefik.http.routers.immich.entrypoints=websecure"
      - "traefik.http.routers.immich.tls.certresolver=letsencrypt"
      - "traefik.http.services.immich.loadbalancer.server.port=2283"

For large uploads with Traefik, add a middleware to increase the buffering limit or set maxRequestBodyBytes in your Traefik static configuration.

Nginx

If you use Nginx as your reverse proxy, the key setting is client_max_body_size. Without it, video uploads fail:

server {
    server_name photos.example.com;

    client_max_body_size 50000M;

    location / {
        proxy_pass http://immich_server:2283;
        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;

        # WebSocket support for real-time updates
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

Point your DNS A record (and AAAA if you have IPv6) to your VPS IP address. After DNS propagates, access Immich at https://photos.example.com.

How do you secure Immich after the first login?

Open https://photos.example.com in your browser. Immich shows a registration page. Create your admin account with a strong password.

After creating your admin account, disable public registration immediately. Go to Administration > Settings > Server and turn off Allow New Users. Anyone who discovers your URL could create an account and upload files to your storage otherwise.

Admin panel settings to review

  • Administration > Settings > Storage Template: enable it to organize files by date ({{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}) instead of random UUIDs. This makes manual file browsing and selective restores possible.
  • Administration > Settings > Backup: automatic database backups are on by default, running daily at 2:00 AM with 14-day retention. Confirm this is active.
  • Administration > Settings > Server: set the external URL to your domain (https://photos.example.com). The mobile app and shared links use this.

Container-level hardening

Add security options to the immich-server service in docker-compose.yml:

    security_opt:
      - no-new-privileges:true

This prevents privilege escalation inside the container. The no-new-privileges flag blocks setuid binaries from gaining elevated permissions.

For resource limits, add memory constraints to prevent any single container from consuming all VPS RAM and triggering the OOM killer. See Docker Compose Resource Limits, Healthchecks, and Restart Policies for the full syntax. A practical starting point for an 8 GB VPS:

    deploy:
      resources:
        limits:
          memory: 2G

Set this on immich-server and immich-machine-learning. PostgreSQL and Redis rarely need explicit limits on an 8 GB VPS but benefit from them on a 4 GB instance.

Hide version information

Immich exposes its version in API responses by default. While this is not a direct vulnerability, version disclosure helps attackers target known issues. There is no built-in toggle to hide it, but your reverse proxy can strip response headers. In Nginx:

proxy_hide_header X-Powered-By;

How do you connect the Immich mobile app for automatic backup?

Install the Immich app from the App Store (iOS) or Google Play (Android). On first launch, enter your server URL (https://photos.example.com) and log in with your admin credentials. The app prompts you to enable automatic backup.

Configure automatic backup

  1. Open the app, tap your avatar in the top-right corner, then Backup Settings.
  2. Enable Background Backup. On iOS, also enable Background App Refresh in system settings. On Android, disable battery optimization for Immich so the OS does not kill it.
  3. Choose which albums to back up. By default, Immich backs up your camera roll. You can add other albums (screenshots, WhatsApp images) from the album selection screen.
  4. Enable Cellular Backup only if your mobile plan allows it. Large video uploads can consume several GB.

First backup test

Take a photo, open the Immich app, and pull down to refresh. The photo should appear in the timeline within seconds on Wi-Fi. The backup status indicator in the app shows a green checkmark when all photos are synced.

From your VPS, you can confirm uploads arrive:

ls /opt/immich/data/upload/

You will see a directory named with your user ID containing the uploaded files.

How much storage does Immich need per photo?

Storage planning on a VPS matters more than on a NAS because you cannot just slide in another drive. Here are real numbers based on typical smartphone photo libraries.

Storage per photo type

Format Average size Notes
Smartphone JPEG 3-5 MB Most common format
HEIC (iPhone) 2-3 MB Apple default since iPhone 7
RAW (DSLR) 25-50 MB Professional cameras
Smartphone video (1080p) ~150 MB/min Varies by codec
Smartphone video (4K) ~400 MB/min H.265 saves ~40%

Total storage by library size

This table uses smartphone JPEGs at 4 MB average, plus Immich overhead (thumbnails, transcodes, database).

Library size Raw photos Thumbnails (~15%) DB Total
10,000 photos 40 GB 6 GB 1 GB ~47 GB
25,000 photos 100 GB 15 GB 1.5 GB ~117 GB
50,000 photos 200 GB 30 GB 2 GB ~232 GB
100,000 photos 400 GB 60 GB 3 GB ~463 GB
200,000 photos 800 GB 120 GB 4 GB ~924 GB

If your library includes video, multiply storage needs accordingly. A library with 10% video content (by file count) can double the storage requirement because videos are 30-100x larger per file than photos.

When to add more storage

On a Virtua VPS, you can attach additional block storage volumes when your main disk fills up. Mount the volume and point UPLOAD_LOCATION to it. For libraries above 500 GB, consider S3-compatible external storage. Immich supports this through its storage configuration, offloading media to object storage while keeping the database and thumbnails local.

Monitor disk usage with a simple cron check:

df -h /opt/immich/data

Set up an alert when usage crosses 80%. A full disk corrupts the PostgreSQL database and can make your Immich instance unrecoverable without a backup.

The v2.5 "Free Up Space" feature

Immich v2.5+ includes a "Free Up Space" button in the mobile app. After photos are backed up to your server, the app can delete local copies from your phone to reclaim storage. This only removes files that are confirmed uploaded and not in the Immich trash. It works on both iOS and Android.

How does Immich machine learning work without a GPU?

Immich's ML features (face recognition, smart search via CLIP, and object detection) run on CPU by default. No GPU is required. The immich-machine-learning container loads models into RAM and processes photos in the background without blocking uploads or browsing.

On a 4-core VPS with 8 GB RAM, expect these approximate processing times for initial scanning:

Library size Face detection CLIP indexing Total (sequential)
1,000 photos ~15 min ~20 min ~35 min
10,000 photos ~2.5 hours ~3.5 hours ~6 hours
50,000 photos ~12 hours ~17 hours ~29 hours

These are one-time costs. After the initial scan, new uploads process in seconds. The ML container runs at low priority and does not block photo uploads or browsing while it works through the queue.

Model selection

Immich uses two main ML models:

  • Facial recognition: detects and clusters faces across your library. Runs automatically on every upload.
  • CLIP (smart search): indexes photos by content so you can search for "sunset" or "dog on beach" without tags. Uses more RAM than facial recognition.

Both models load into RAM when first needed and unload after 5 minutes of inactivity (MACHINE_LEARNING_MODEL_TTL=300 by default). On a memory-constrained VPS, you can lower this value to free RAM faster:

# In .env
MACHINE_LEARNING_MODEL_TTL=60

Recreate the container after changing environment variables (restarting is not enough):

docker compose up -d --force-recreate immich-machine-learning

RAM allocation by component

Component RAM usage (8 GB VPS) RAM usage (4 GB VPS)
immich-server ~500 MB ~500 MB
immich-machine-learning ~1.5-2 GB disabled
PostgreSQL ~500 MB-1 GB ~500 MB
Redis (Valkey) ~50 MB ~50 MB
OS + Docker overhead ~1 GB ~1 GB
Available headroom ~3-4 GB ~2 GB

How do you disable ML to save resources on a small VPS?

If you run a 4 GB VPS or want to save resources, remove or comment out the immich-machine-learning service from docker-compose.yml:

cd /opt/immich

Edit docker-compose.yml and comment out (or delete) the entire immich-machine-learning service block. Then restart:

docker compose up -d

You lose face recognition, smart search, and object detection. Photo upload, browsing, sharing, and album management all work normally. You can re-enable ML later by uncommenting the service and restarting.

How do you back up Immich's database and photos?

Back up two things: the PostgreSQL database with pg_dump and the upload directory with rsync. Run both on a cron schedule. Do not use rsync --delete on the backup target because a corruption on the source would propagate to your backup. Store at least one copy off-server. Test your restore procedure periodically.

Database backup

docker exec -t immich_postgres pg_dump \
  --clean --if-exists \
  --dbname=immich \
  --username=postgres | gzip > /opt/immich/backups/db-$(date +%Y%m%d-%H%M%S).sql.gz

Upload directory backup

rsync -a --info=progress2 \
  /opt/immich/data/ \
  /mnt/backup/immich-data/

Skip thumbnails and transcoded video to save space. They regenerate automatically:

rsync -a --info=progress2 \
  --exclude='thumbs/' \
  --exclude='encoded-video/' \
  /opt/immich/data/ \
  /mnt/backup/immich-data/

Automate with cron

Create a backup script:

cat > /opt/immich/backup.sh << 'SCRIPT'
#!/bin/bash
set -euo pipefail

BACKUP_DIR="/mnt/backup/immich"
mkdir -p "$BACKUP_DIR/db"

# Database dump
docker exec -t immich_postgres pg_dump \
  --clean --if-exists \
  --dbname=immich \
  --username=postgres | gzip > "$BACKUP_DIR/db/immich-db-$(date +%Y%m%d).sql.gz"

# Keep last 14 database dumps
find "$BACKUP_DIR/db" -name "immich-db-*.sql.gz" -mtime +14 -delete

# Media sync (excludes regeneratable data)
rsync -a \
  --exclude='thumbs/' \
  --exclude='encoded-video/' \
  /opt/immich/data/ \
  "$BACKUP_DIR/media/"

echo "Backup completed: $(date)"
SCRIPT

chmod 700 /opt/immich/backup.sh

Schedule it to run nightly at 3:00 AM (after Immich's own internal backup at 2:00 AM):

(crontab -l 2>/dev/null; echo "0 3 * * * /opt/immich/backup.sh >> /var/log/immich-backup.log 2>&1") | crontab -

Off-site backup

For a true 3-2-1 backup strategy, copy backups off the server using rclone to any S3-compatible storage provider:

rclone sync /mnt/backup/immich remote:immich-backup --transfers 4

See your storage provider's documentation for rclone configuration.

Test the restore

A backup you have not tested is not a backup. Here is the restore procedure:

cd /opt/immich
docker compose down -v
rm -rf /opt/immich/postgres/*

docker compose pull
docker compose create
docker start immich_postgres
sleep 10

gunzip --stdout /mnt/backup/immich/db/immich-db-20260320.sql.gz | \
  sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" | \
  docker exec -i immich_postgres psql \
    --dbname=immich \
    --username=postgres \
    --single-transaction \
    --set ON_ERROR_STOP=on

docker compose up -d

After the restore, log in to the web UI. Browse your timeline and confirm photos load. If thumbnails are missing (because you excluded them from backup), go to Administration > Job Queues and run Generate Thumbnails.

How do you update Immich safely?

Immich follows semantic versioning and releases frequently. Some releases include database migrations. Downgrading after an upgrade is not supported. This means you need a disciplined update workflow.

Step-by-step update procedure

  1. Read the changelog. Check the releases page for breaking changes before pulling anything.

  2. Back up the database. Run the backup script or a manual pg_dump as shown above. Do not skip this.

  3. Update the version pin. Edit /opt/immich/.env and change IMMICH_VERSION to the new version:

# Example: updating from v2.6.1 to v2.7.0
sed -i 's/IMMICH_VERSION=v2.6.1/IMMICH_VERSION=v2.7.0/' /opt/immich/.env
  1. Pull and restart.
cd /opt/immich
docker compose pull
docker compose up -d
  1. Check the logs.
docker compose logs -f --tail=100

Look for migration messages and confirm the server starts without errors. Database migrations run automatically on startup.

  1. Clean up old images.
docker image prune -f

Rollback

If something breaks, you cannot simply change the version back because the database may have migrated forward. Restore from your pre-update database backup instead:

  1. Stop the stack: docker compose down -v
  2. Restore the database from your backup (see the restore section above)
  3. Set IMMICH_VERSION back to the previous version in .env
  4. Start the stack: docker compose up -d

This is why you back up before every update.

How do you import photos from Google Takeout into Immich?

The easiest way to import a Google Photos export is immich-go, a community tool that reads Google Takeout ZIP files directly. It preserves album structure, dates, and GPS metadata from the JSON sidecar files.

Download immich-go on your local machine (not the VPS). Generate an API key in Immich: click your avatar > Account Settings > API Keys > New API Key.

immich-go upload from-google-photos \
  --server=https://photos.example.com \
  --api-key=your-api-key \
  /path/to/takeout-*.zip

Run a dry run first to see what will be imported:

immich-go upload from-google-photos \
  --server=https://photos.example.com \
  --api-key=your-api-key \
  --dry-run \
  /path/to/takeout-*.zip

The tool deduplicates by checksum. If you run the import twice, nothing gets uploaded a second time.

For smaller imports (under a few thousand photos), use the Immich web UI. Drag-and-drop files into the timeline view or click the upload button. The web uploader handles batches but is slower than immich-go for large libraries.

External library mount

If your photos already live on the VPS filesystem (from a previous backup or migration), you can mount them as an external library instead of re-uploading. Add the path as a volume in docker-compose.yml under immich-server:

    volumes:
      - ${UPLOAD_LOCATION}:/data
      - /etc/localtime:/etc/localtime:ro
      - /mnt/photos:/mnt/photos:ro

Then in the Immich web UI, go to Administration > External Libraries and add /mnt/photos as a library path. Immich indexes the files in place without copying them. The :ro flag keeps the mount read-only so Immich cannot modify your originals.

Troubleshooting

Container keeps restarting

Check the logs for the specific container:

docker compose logs immich-server --tail=50
docker compose logs database --tail=50

Common causes: wrong DB_PASSWORD in .env (container and database disagree), insufficient RAM (OOM killer), or a full disk.

Uploads fail silently

Almost always a reverse proxy body size limit. Check your proxy configuration for client_max_body_size (Nginx) or equivalent. Caddy has no default limit. Traefik defaults vary by version.

ML models fail to load

The ML container downloads models on first start. If your VPS has limited bandwidth or the download was interrupted, the models may be corrupt. Remove the container, delete the model cache volume, and recreate it:

docker compose rm -sf immich-machine-learning
docker volume rm immich_model-cache
docker compose up -d immich-machine-learning

Photos show but thumbnails are broken

Regenerate thumbnails from the admin panel: Administration > Job Queues > Generate Thumbnails > Start.

Database connection errors after restore

If you see relation already exists or foreign key constraint violated errors during a restore, the database was not fully clean before importing. Stop all containers, delete the DB_DATA_LOCATION directory, recreate the postgres container, wait 10 seconds for initialization, then run the restore again.

Check logs

All Immich logs go through Docker's logging driver:

docker compose logs -f

For a specific service:

docker compose logs database -f
docker compose logs immich-machine-learning -f

Filter for errors only:

docker compose logs immich-server 2>&1 | grep -i error

Copyright 2026 Virtua.Cloud. All rights reserved. This content is original work by the Virtua.Cloud team. Reproduction, republication, or redistribution without written permission is prohibited.

Self-Host Immich on a VPS with Docker Compose