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 leaveDB_PASSWORDat default values. If you are modifying an existing installation, updating the database password requires modifying both the.envconfiguration 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 Configuration (Recommended)
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