Table of Contents

๐Ÿ” Key Considerations Before You Start

  • An S3-compatible backend would have been ideal, but at this time there is no official Immich release that supports S3-compatible storage natively. Community workarounds exist but are fragile and not recommended for production.
  • Youโ€™re working with a single VM: keep services isolated but simple. Use Docker Compose. No need for Kubernetes.
  • Youโ€™re self-hosting for your family: prioritize reliability, recoverability, and ease of use.
  • You are using a custom domain (e.g. media.mangomystic.dedyn.io) and want HTTPS enabled.
  1. Check Your Resource Quotas First
    Leafcloud applies default resource quotas that may not fit your needs (e.g., 2 vCPUs, 12GB RAM, 40GB volume limit). Before planning your infrastructure:

    • Request a quota increase via email or support.
    • Use the OpenStack CLI to check your current limits:
      openstack quota show <project_id> --volume
      
    • You can also inspect compute limits like this:
      openstack quota show <project_id> --compute
      
  2. DNS Propagation Check
    Once you point your domain (e.g. media.example.dedyn.io) to your Leafcloud VM, check if DNS changes have propagated globally using:

    • https://www.whatsmydns.net/
  3. Avoid Extending Storage with a Separate Volume After Immich Install
    This led to numerous headaches. Immich does not gracefully recognize additional mounted volumes for UPLOAD_LOCATION. Instead:

    • Define the correct storage size as root volume during Terraform provisioning.
    • Set UPLOAD_LOCATION from the beginning.
  4. Be Mindful of PostgreSQL Version Compatibility
    Immichโ€™s machine learning features rely on the pgvecto.rs extension.

    • This requires a compatible Postgres version.
    • We used Postgres 15 successfully.
    • Always verify whatโ€™s currently supported: https://github.com/immich-app/immich/discussions
  5. Docker Images Will Take Up Space
    Immichโ€™s containers (server, ML, Postgres, Redis) together take 6โ€“8GB on your disk. Plan your VM root volume accordingly.

  6. Localhost in Caddy Reverse Proxy
    Even though your domain points to the public IP (e.g., 45.135.59.126:2283), Caddy runs on the same VM and should proxy to localhost:2283. Thatโ€™s because inside the VM, Caddy only needs to reach the local Immich containerโ€”no need to route via external IP.


โ˜๏ธ Goal: Provision a Media Host on Leafcloud

  • VM: 2 vCPUs, 8GB RAM, 200GB root volume
  • OS: Ubuntu 22.04
  • App: Immich self-hosted photo/video server
  • Infra: Deployed via Terraform on Leafcloud (OpenStack backend)

โœ… Prerequisites

  • Terraform v1.1+
  • OpenStack project credentials (create.leaf.cloud)
  • SSH key pair created and uploaded to Leafcloud
  • DNS domain from https://desec.io

๐Ÿ“ Terraform Structure

leafcloud-terraform/
โ”œโ”€โ”€ main.tf
โ”œโ”€โ”€ provider.tf
โ”œโ”€โ”€ leafcloud.auto.tfvars
โ””โ”€โ”€ .ssh/immich-key (your private key)

๐Ÿ”ง provider.tf

terraform {
  required_providers {
    openstack = {
      source  = "terraform-provider-openstack/openstack"
      version = ">= 1.0.0"
    }
  }
}

provider "openstack" {
  auth_url    = "https://identity.leaf.cloud/v3"
  user_name   = "your@email.com"
  password    = "your_password"
  tenant_name = "your_project"
  domain_name = "Default"
  region      = "RegionOne"
}

๐Ÿ“ฆ main.tf

resource "openstack_compute_instance_v2" "media_host" {
  name            = "media-host"
  image_name      = "Ubuntu-22.04.20240522"
  flavor_name     = "en1.large"
  key_pair        = "immich-key"
  security_groups = ["default"]

  block_device {
    uuid                  = "<image_id>"
    source_type           = "image"
    destination_type      = "volume"
    volume_size           = 200
    boot_index            = 0
    delete_on_termination = true
  }

  network {
    name = "external"
  }
}

๐Ÿ” leafcloud.auto.tfvars

auth_url    = "https://identity.leaf.cloud/v3"
tenant_name = "your_project"
user_name   = "your@email.com"
password    = "your_password"
region      = "RegionOne"
domain_name = "Default"

๐Ÿš€ Deploy

terraform init
terraform plan -out=tfplan
terraform apply "tfplan"

After apply:

openstack server show media-host -f value -c addresses

๐Ÿงฐ Set Up Immich

ssh -i ~/.ssh/immich-key ubuntu@<external_ip>
sudo apt update && sudo apt upgrade -y
sudo apt install -y docker.io docker-compose
sudo usermod -aG docker $USER
newgrp docker
mkdir ~/immich && cd ~/immich

docker-compose.yml

services:
  immich-server:
    container_name: immich_server
    image: ghcr.io/immich-app/immich-server:release
    restart: always
    env_file:
      - .env
    ports:
      - "2283:2283"
    depends_on:
      - database
      - redis
    volumes:
      - /mnt/immich-storage/upload:/usr/src/app/upload

  immich-microservices:
    container_name: immich_microservices
    image: ghcr.io/immich-app/immich-server:release
    restart: always
    env_file:
      - .env
    depends_on:
      - database
      - redis
    volumes:
      - /mnt/immich-storage/upload:/usr/src/app/upload

  immich-machine-learning:
    container_name: immich_machine_learning
    image: ghcr.io/immich-app/immich-machine-learning:release
    restart: always

  redis:
    image: redis:6.2
    container_name: immich_redis
    restart: always

  database:
    container_name: immich_postgres
    image: tensorchord/pgvecto-rs:pg15-v0.2.0
    restart: always
    environment:
      POSTGRES_PASSWORD: postgres
      POSTGRES_USER: postgres
      POSTGRES_DB: immich
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:

.env

UPLOAD_LOCATION=/mnt/immich-storage/upload

DB_HOST=database
DB_PORT=5432
DB_USERNAME=postgres
DB_PASSWORD=postgres
DB_DATABASE_NAME=immich
REDIS_HOSTNAME=redis

๐Ÿ”’ Add HTTPS + Domain (dedyn.io)

sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy

Edit /etc/caddy/Caddyfile:

# Immich instance hosted on this VM
media.mangomystic.dedyn.io {
  reverse_proxy localhost:2283

  # Compress responses for faster loading
  encode gzip

  # Secure HTTP headers
  header {
    Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"  # Enforce HTTPS
    X-Content-Type-Options "nosniff"                                          # Prevent MIME type sniffing
    X-Frame-Options "DENY"                                                    # Prevent clickjacking
    Referrer-Policy "strict-origin-when-cross-origin"                         # Limit referrer info
  }
}
sudo systemctl restart caddy

Optional: Lock Down Further (only if needed)

If your Immich is just for family:

  • Allowlist IPs via Caddy (e.g., restrict to home IP)
  • Add basic auth if extra protection is needed
  • Use a firewall to block unused ports

But if your domain is private and not indexed publicly, and HTTPS is on โ€” this may be enough.


Final Checks

Run:

docker compose down
docker compose pull
docker compose up -d

Then visit:
๐Ÿ”— https://media.mangomystic.dedyn.io

๐ŸŽ‰ Youโ€™re Done!

Self-hosted Immich on Leafcloud with HTTPS and infrastructure-as-code in place!