2025 Infrastructure Live Demo ↗

Overview

This project is a self-hosted Immich instance deployed at photo.tyfsadik.org that provides Google Photos-equivalent functionality with full data ownership. Immich handles automatic mobile photo backup, timeline browsing, album organization, and search. The ML microservices container runs face recognition, object detection, and CLIP-based semantic search, enabling queries like "beach sunset" without any tagging.

The stack runs on Docker Compose with PostgreSQL as the metadata database (with the pgvecto.rs extension for vector similarity search used by the ML features), Redis for job queuing, and a dedicated microservices container that runs the Python-based ML workloads separately from the main server process. Files are stored on a local volume backed by the Proxmox ZFS dataset. The mobile app enables background backup over Wi-Fi, keeping the photo library current without any manual steps.

Architecture

graph LR subgraph Clients["Clients"] MOB["Mobile App\niOS / Android"] WEB["Web Browser"] end subgraph Services["Docker Services"] NG["Nginx\n:443 SSL"] IS["Immich Server\n:3001"] MS["Immich Microservices\nML worker"] ML["ML Models\nface / CLIP / object"] end subgraph Data["Data Layer"] PG[("PostgreSQL\n+ pgvecto.rs")] RD[("Redis\njob queue")] PV[("Photos Volume\noriginal + thumbs")] end MOB -->|"HTTPS backup"| NG WEB -->|"HTTPS :443"| NG NG -->|"proxy_pass"| IS IS -->|"enqueue ML jobs"| RD RD -->|"dequeue"| MS MS --> ML MS -->|"vectors + labels"| PG IS <-->|"metadata queries"| PG IS -->|"read/write files"| PV MS -->|"read originals"| PV style MOB fill:#1a1a2e,stroke:#00d4ff,color:#e0e0e0 style WEB fill:#1a1a2e,stroke:#00d4ff,color:#e0e0e0 style NG fill:#1a1a2e,stroke:#00ff88,color:#e0e0e0 style IS fill:#1a1a2e,stroke:#00d4ff,color:#e0e0e0 style MS fill:#1a1a2e,stroke:#00d4ff,color:#e0e0e0 style ML fill:#181818,stroke:#1e1e1e,color:#888 style PG fill:#181818,stroke:#1e1e1e,color:#888 style RD fill:#181818,stroke:#1e1e1e,color:#888 style PV fill:#181818,stroke:#1e1e1e,color:#888

Tech Stack

  • Immich — self-hosted Google Photos alternative with mobile backup
  • PostgreSQL + pgvecto.rs — metadata store with vector similarity search for ML
  • Redis — job queue for background ML processing tasks
  • Immich microservices — Python ML container for face recognition and object detection
  • Docker & Docker Compose — service orchestration
  • Nginx — SSL reverse proxy for photo.tyfsadik.org
  • Let's Encrypt — TLS certificate management

Build Process

1

Download Immich docker-compose.yml and .env

Immich provides an official Docker Compose file that defines all required services. The .env file contains the upload location path, database credentials, and version pin. These are the only two files needed to get a working stack.

mkdir immich && cd immich

# Download official compose file and env template
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

# Review what services are defined
cat docker-compose.yml | grep "^  [a-z]" | grep ":"
2

Configure UPLOAD_LOCATION and Credentials in .env

The .env file is edited to set the upload path to the ZFS-backed volume mount point, and strong random passwords are generated for the database. The IMMICH_VERSION is pinned to a specific release for reproducibility.

# .env configuration
UPLOAD_LOCATION=/mnt/photos/immich
DB_DATA_LOCATION=/mnt/photos/postgres

# Generate secure password
DB_PASSWORD=$(openssl rand -hex 32)
echo "DB_PASSWORD=$DB_PASSWORD" >> .env

DB_USERNAME=immich
DB_DATABASE_NAME=immich

# Pin to a specific release (check releases page for latest)
IMMICH_VERSION=v1.106.4

# Redis (default, no password needed for local-only)
REDIS_HOSTNAME=immich_redis
3

Start the Stack

The full stack is brought up with Docker Compose. First startup takes several minutes as Docker pulls the images (server, microservices, PostgreSQL, Redis). The ML models are downloaded on first use when the microservices container starts processing a photo.

docker compose up -d

# Watch startup logs
docker compose logs -f

# Verify all containers are healthy
docker compose ps

# Check the server is responding
curl http://localhost:3001/api/server-info/ping
# Expected: {"res":"pong"}
4

Nginx Reverse Proxy Configuration

Nginx is configured to proxy photo.tyfsadik.org to the Immich server container. The client_max_body_size is set generously to support large video file uploads. WebSocket connections are proxied for real-time upload progress updates.

# /etc/nginx/sites-available/photo.tyfsadik.org
server {
    listen 80;
    server_name photo.tyfsadik.org;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name photo.tyfsadik.org;

    ssl_certificate /etc/letsencrypt/live/photo.tyfsadik.org/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/photo.tyfsadik.org/privkey.pem;

    client_max_body_size 50G;

    location / {
        proxy_pass http://localhost:3001;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Proto https;
        proxy_read_timeout 600s;
        proxy_send_timeout 600s;
    }
}

ln -s /etc/nginx/sites-available/photo.tyfsadik.org /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
5

Provision SSL Certificate

Certbot provisions the Let's Encrypt certificate and modifies the Nginx config to redirect HTTP to HTTPS. A systemd timer handles automatic renewal 30 days before expiry.

apt install certbot python3-certbot-nginx -y

certbot --nginx -d photo.tyfsadik.org \
  --agree-tos --email [email protected]

# Verify auto-renewal is configured
systemctl status certbot.timer

# Test renewal without actually renewing
certbot renew --dry-run
6

Install Mobile App and Enable Auto-Backup

The Immich mobile app is installed from the App Store or Play Store. The server URL is set to https://photo.tyfsadik.org. Auto-backup is configured to run over Wi-Fi only, with background sync enabled. For existing photo libraries, a bulk import job is run via the Immich web interface to scan the upload directory.

# For bulk importing an existing photo directory
# Upload files to the UPLOAD_LOCATION directly, then trigger scan:
docker exec -it immich_server \
  node /usr/src/app/dist/cli/index.js upload --yes \
  /usr/src/app/upload/library

# Or trigger library scan via the web UI:
# Administration > Jobs > Library > Scan All

# Monitor ML processing jobs
docker compose logs -f immich_microservices | grep "Running job"

Request Flow

sequenceDiagram participant MOB as Mobile App participant NG as Nginx participant IS as Immich Server participant RD as Redis Queue participant MS as ML Microservices participant PG as PostgreSQL participant PV as Photos Volume MOB->>NG: HTTPS POST /api/asset/upload NG->>IS: proxy upload IS->>PV: write original file IS->>PG: insert asset record (path, hash, metadata) IS->>RD: enqueue ML jobs (face detect, CLIP encode) IS-->>MOB: 201 Created (asset ID) RD-->>MS: dequeue face detection job MS->>PV: read original file MS->>MS: run face recognition model MS->>PG: store face vectors + bounding boxes RD-->>MS: dequeue CLIP encode job MS->>MS: generate semantic embedding MS->>PG: store clip_embeddings vector

Challenges & Solutions

  • ML microservices consuming too much RAM: The face recognition and CLIP models together consumed over 4 GB of RAM on a server with 8 GB total, leaving insufficient memory for PostgreSQL and the main server. Resolved by adding Docker resource constraints (mem_limit: 2g) in the compose file and configuring the ML service to unload models from memory after a period of inactivity using the MODEL_TTL environment variable.
  • Large initial library import taking days: Importing 30,000 existing photos via the mobile app one by one was impractical. Used the Immich CLI bulk import utility to copy files directly to the upload location and trigger a library scan, processing the entire backlog as a batch job overnight.
  • PostgreSQL pgvecto.rs extension not found: The official Immich PostgreSQL image includes pgvecto.rs, but a custom image was initially used that lacked it. Switched back to the official ghcr.io/immich-app/postgres image which bundles all required extensions.
  • Video thumbnails not generating: The microservices container failed to generate video thumbnails because FFmpeg was not installed in a custom image variant. The official Immich server image includes FFmpeg, resolving the issue after switching back to the canonical image tag.

What I Learned

  • Docker Compose multi-service architectures with health checks and dependency ordering
  • PostgreSQL vector extensions (pgvecto.rs) and their use in ML-powered search
  • Redis job queue patterns for decoupling ML processing from the request path
  • Docker resource constraints (mem_limit, cpus) for shared low-resource servers
  • ML model lifecycle management: lazy loading, TTL-based unloading, warm vs cold inference
  • Mobile app background sync behavior and Wi-Fi-only backup configuration
Immich Docker PostgreSQL Machine Learning Photos Privacy Self-Hosted