Immich Docker Setup: The Ultimate Google Photos Self-Hosted Alternative

Tired of Google Photos storage limits? Set up Immich on your VPS using Docker Compose. Get fast, high-performance photo backup with machine learning features.

Self-Hosting Immich on a VPS: Production Deployment Guide

This technical guide covers the production deployment of Immichโ€”a high-performance, self-hosted photo and video management solutionโ€”on a Virtual Private Server (VPS) using Docker Compose.

Hardware and System Requirements

Immich relies on several containerized services including a database (PostgreSQL with pgvector), a cache (Redis), a machine learning backend (python-based image/video processing), and the main application server.

CPU & RAM Targets

VPS Size CPU Cores RAM ML Features Target Library Size
Minimum 2 vCPUs 4 GB Disabled / Thread-limited < 50,000 assets
Recommended 4 vCPUs 8 GB Enabled (CPU) 50,000 - 250,000 assets
High Performance 8+ vCPUs 16+ GB Enabled (GPU/CPU) 250,000+ assets
[!IMPORTANT] Running Immich on a VPS with less than 4 GB of RAM is highly discouraged and will lead to Out-Of-Memory (OOM) crashes during machine learning tasks (clip encoding, facial recognition). If you run on a 4 GB RAM VPS, you must configure a swap file of at least 4 GB.

Swap File Allocation (Ubuntu/Debian)

If your VPS has limited RAM, run the following commands to provision swap space:

# Create a 4GB swap file
sudo fallocate -l 4G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile

# Persist across reboots
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

The Docker Compose Architecture

The deployment consists of four primary services: 1. immich-server: The primary application server handling web/API requests, background jobs, metadata extraction, and transcoding. 2. immich-machine-learning: Handles object detection, facial recognition, and CLIP semantic search. 3. postgres: PostgreSQL database with the pgvector extension for image vector storage. 4. redis: Caches session tokens, job queues, and websocket events.

docker-compose.yml

Create a deployment directory and save the following compose file:

# Save to: /opt/immich/docker-compose.yml
name: immich

services:
  immich-server:
    container_name: immich_server
    image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
    # extends:
    #   file: hwaccel.transcoding.yml
    #   service: cpu # Change to nvidiagpu or quicksync if VPS supports it
    volumes:
      - ${UPLOAD_LOCATION}:/usr/src/app/upload
      - /etc/localtime:/etc/localtime:ro
    env_file:
      - .env
    ports:
      - "127.0.0.1:2283:2283"
    depends_on:
      - database
      - redis
    restart: always

  immich-machine-learning:
    container_name: immich_machine_learning
    image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}
    volumes:
      - model-cache:/cache
    env_file:
      - .env
    restart: always

  redis:
    container_name: immich_redis
    image: docker.io/redis:7-alpine
    healthcheck:
      test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
      start_period: 20s
      interval: 10s
    restart: always

  database:
    container_name: immich_postgres
    image: docker.io/tensorchord/pgvecto-rs:pg16-v0.2.0@sha256:e84d3dceae36d51190344b1898afe8a7e961cfce1cb1b5ec2ec2053a0ddcc10c
    environment:
      POSTGRES_DB: ${DB_DATABASE_NAME}
      POSTGRES_USER: ${DB_USERNAME}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_INITDB_ARGS: '--data-checksums'
    volumes:
      - ${DB_DATA_LOCATION}:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready --dbname=${DB_DATABASE_NAME} --username=${DB_USERNAME}"]
      start_period: 20s
      interval: 10s
    restart: always

volumes:
  model-cache:

Environment Configuration

Configure environmental variables in a .env file adjacent to your docker-compose.yml.

.env

# Save to: /opt/immich/.env

# Immich version tag (use 'release' or a specific tag like 'v1.106.1')
IMMICH_VERSION=release

# Database Settings
DB_DATABASE_NAME=immich
DB_USERNAME=postgres
# Generate a secure 32+ character password
DB_PASSWORD=a_very_secure_random_db_password_here

# Storage Paths
UPLOAD_LOCATION=/opt/immich/upload
DB_DATA_LOCATION=/opt/immich/postgres

# Machine Learning Tuning
# Reduce ML worker concurrency on lower-spec VPS (defaults to number of CPU cores)
IMMICH_ML_WORKERS=1
# Limit CPU threads used by ONNX runtime inside the ML container
IMMICH_ML_THREAD_LIMIT=2
[!WARNING] Do not leave DB_PASSWORD at default values. If you are modifying an existing installation, updating the database password requires modifying both the .env configuration and the PostgreSQL authentication settings.

Tuning Machine Learning for VPS Environments

The default Immich machine learning settings consume significant memory during face scanning and CLIP encoding. On standard virtual CPUs, you should tune these settings in the Admin Console or limit container resource consumption:

Docker CPU and Memory Limits

To prevent the ML container from exhausting host resources, apply limits in docker-compose.yml:

  immich-machine-learning:
    container_name: immich_machine_learning
    image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}
    volumes:
      - model-cache:/cache
    env_file:
      - .env
    deploy:
      resources:
        limits:
          cpus: '2'
          memory: 3000M
        reservations:
          memory: 1000M
    restart: always

Reverse Proxy and SSL Configuration

To secure communication between mobile devices, web clients, and Immich, deploy a reverse proxy handling TLS termination.

Caddy provides automated certificate management via Let's Encrypt.

# /etc/caddy/Caddyfile
photos.yourdomain.com {
    reverse_proxy 127.0.0.1:2283 {
        # Enable large client headers for video transfers
        header_up Host {host}
        header_up X-Real-IP {remote_host}
        header_up X-Forwarded-For {remote_host}
        header_up X-Forwarded-Proto {scheme}
    }

    # Configure max payload size for uploads (e.g., 500MB)
    request_body_limit 524288000
}

Nginx Configuration

# /etc/nginx/sites-available/immich
server {
    listen 80;
    server_name photos.yourdomain.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name photos.yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/photos.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/photos.yourdomain.com/privkey.pem;

    client_max_body_size 500M;

    location / {
        proxy_pass http://127.0.0.1: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
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        # Disable buffering for smoother file uploads
        proxy_buffering off;
        proxy_read_timeout 600s;
        proxy_send_timeout 600s;
        send_timeout 600s;
    }
}

Backup Strategy

A production-ready self-hosted instance must follow a 3-2-1 backup strategy. Two main components must be backed up: 1. The database schema and raw table data (PostgreSQL dump). 2. The raw files, uploads, and metadata stored in the UPLOAD_LOCATION path.

Automated Postgres Dump Script

Create a cron job that outputs database states daily:

#!/usr/bin/env bash
# Save to: /opt/immich/backup.sh
set -eo pipefail

BACKUP_DIR="/opt/immich/backups"
TIMESTAMP=$(date +%F_%H%M%S)
DATABASE_CONTAINER="immich_postgres"
DB_USER="postgres"
DB_NAME="immich"

mkdir -p "$BACKUP_DIR"

# Perform database dump
docker exec -t "$DATABASE_CONTAINER" pg_dumpall --clean -U "$DB_USER" | gzip > "$BACKUP_DIR/db_dump_$TIMESTAMP.sql.gz"

# Prune backups older than 7 days
find "$BACKUP_DIR" -name "db_dump_*.sql.gz" -mtime +7 -delete