From 12d9ea51b8b245e042711cb4e24d9d78e1c862fb Mon Sep 17 00:00:00 2001 From: "martin.fencl" Date: Tue, 3 Feb 2026 18:13:24 +0100 Subject: [PATCH] edit immich --- .../docker-compose-immich.override.yml | 5 + docker-compose/docker-compose-immich.yml | 77 ++++ old/update_immich.yml | 313 +++++++++++++++ update_immich.yml | 367 ++++-------------- 4 files changed, 464 insertions(+), 298 deletions(-) create mode 100644 docker-compose/docker-compose-immich.override.yml create mode 100644 docker-compose/docker-compose-immich.yml create mode 100644 old/update_immich.yml diff --git a/docker-compose/docker-compose-immich.override.yml b/docker-compose/docker-compose-immich.override.yml new file mode 100644 index 0000000..8ae9fa7 --- /dev/null +++ b/docker-compose/docker-compose-immich.override.yml @@ -0,0 +1,5 @@ +services: + immich-server: + volumes: + - /mnt/nextcloud-howard-photos:/mnt/nextcloud-howard-photos + - /mnt/nextcloud-kamilkaprdelka-photos:/mnt/nextcloud-kamilkaprdelka-photos diff --git a/docker-compose/docker-compose-immich.yml b/docker-compose/docker-compose-immich.yml new file mode 100644 index 0000000..7d62fc0 --- /dev/null +++ b/docker-compose/docker-compose-immich.yml @@ -0,0 +1,77 @@ +# +# WARNING: To install Immich, follow our guide: https://docs.immich.app/install/docker-compose +# +# Make sure to use the docker-compose.yml of the current release: +# +# https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml +# +# The compose file on main may not be compatible with the latest release. + +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 # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding + volumes: + - ${UPLOAD_LOCATION}:/usr/src/app/upload + #- /mnt/nextcloud-howard-photos:/mnt/nextcloud-howard-photos:ro # read-only external library + #- /mnt/nextcloud-kamilkaprdelka-photos:/mnt/nextcloud-kamilkaprdelka-photos:ro # read-only external library + - /mnt/nextcloud-howard-photos:/mnt/nextcloud-howard-photos + - /mnt/nextcloud-kamilkaprdelka-photos:/mnt/nextcloud-kamilkaprdelka-photos + - /etc/localtime:/etc/localtime:ro + env_file: + - .env + ports: + - '2283:2283' + depends_on: + - redis + - database + restart: always + healthcheck: + disable: false + + immich-machine-learning: + container_name: immich_machine_learning + # For hardware acceleration, add one of -[armnn, cuda, rocm, openvino, rknn] to the image tag. + # Example tag: ${IMMICH_VERSION:-release}-cuda + image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release} + # extends: # uncomment this section for hardware acceleration - see https://docs.immich.app/features/ml-hardware-acceleration + # file: hwaccel.ml.yml + # service: cpu # set to one of [armnn, cuda, rocm, openvino, openvino-wsl, rknn] for accelerated inference - use the `-wsl` version for WSL2 where applicable + volumes: + - model-cache:/cache + env_file: + - .env + restart: always + healthcheck: + disable: false + + redis: + container_name: immich_redis + image: docker.io/valkey/valkey:8@sha256:81db6d39e1bba3b3ff32bd3a1b19a6d69690f94a3954ec131277b9a26b95b3aa + healthcheck: + test: redis-cli ping || exit 1 + restart: always + + database: + container_name: immich_postgres + image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:bcf63357191b76a916ae5eb93464d65c07511da41e3bf7a8416db519b40b1c23 + environment: + POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_USER: ${DB_USERNAME} + POSTGRES_DB: ${DB_DATABASE_NAME} + POSTGRES_INITDB_ARGS: '--data-checksums' + # Uncomment the DB_STORAGE_TYPE: 'HDD' var if your database isn't stored on SSDs + # DB_STORAGE_TYPE: 'HDD' + volumes: + # Do not edit the next line. If you want to change the database storage location on your system, edit the value of DB_DATA_LOCATION in the .env file + - ${DB_DATA_LOCATION}:/var/lib/postgresql/data + shm_size: 128mb + restart: always + +volumes: + model-cache: diff --git a/old/update_immich.yml b/old/update_immich.yml new file mode 100644 index 0000000..a2a10a7 --- /dev/null +++ b/old/update_immich.yml @@ -0,0 +1,313 @@ +# update_immich.yml + +- name: Update Immich on VM via Proxmox + hosts: linux_servers + gather_facts: false + become: true + become_user: root + become_method: sudo + + vars: + # VM connection (provided by Semaphore env vars) + vm_ip: "{{ lookup('env', 'VM_IP') }}" + vm_user: "{{ lookup('env', 'VM_USER') }}" + vm_pass: "{{ lookup('env', 'VM_PASS') }}" + use_sudo: false + + # --- Debug mode (controlled via Semaphore variable) --- + DEBUG: "{{ lookup('env', 'DEBUG') | default(0) | int }}" + RETRIES: "{{ lookup('env', 'RETRIES') | default(25) | int }}" + + # Immich specifics + immich_dir: "/opt/immich" + immich_project: "immich" + immich_compose_url: "https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml" + immich_compose_file: "/opt/immich/docker-compose.yml" + immich_override_file: "/opt/immich/docker-compose.override.yml" + immich_port: 2283 + + # Optional external URL for controller-side readiness check (e.g., https://photos.example.com) + immich_url: "{{ lookup('env', 'IMMICH_URL') | default('', true) }}" + + # Retry policy + immich_retries: "{{ RETRIES }}" + immich_delay: 2 + + # Docker command prefix (consistent behavior) + docker_prefix: "unalias docker 2>/dev/null || true; DOCKER_CLI_HINTS=0; command docker" + + # Compose command (always include override to keep local mounts separate from upstream compose) + immich_compose_cmd: >- + {{ docker_prefix }} compose + -p {{ immich_project }} + -f {{ immich_compose_file }} + -f {{ immich_override_file }} + + # Commands to run on the target VM + immich_commands: + - "cd {{ immich_dir }}" + + - | + cd {{ immich_dir }} + mkdir -p backups + if [ -f docker-compose.yml ]; then + cp -a docker-compose.yml "backups/docker-compose.yml.$(date +%F_%H%M%S).bak" + fi + if [ -f .env ]; then + cp -a .env "backups/.env.$(date +%F_%H%M%S).bak" + fi + if [ -f docker-compose.override.yml ]; then + cp -a docker-compose.override.yml "backups/docker-compose.override.yml.$(date +%F_%H%M%S).bak" + fi + + - | + cd {{ immich_dir }} + # Download latest compose from Immich releases (requires curl or wget) + if command -v curl >/dev/null 2>&1; then + curl -fsSL -o docker-compose.yml "{{ immich_compose_url }}" + elif command -v wget >/dev/null 2>&1; then + wget -qO docker-compose.yml "{{ immich_compose_url }}" + else + echo "Neither curl nor wget is available on the VM." + exit 1 + fi + + - | + cd {{ immich_dir }} + # Ensure override compose exists (create if missing) + if [ ! -f "{{ immich_override_file }}" ]; then + printf '%s\n' \ + 'services:' \ + ' immich-server:' \ + ' volumes:' \ + ' - /mnt/nextcloud-howard-photos:/mnt/nextcloud-howard-photos' \ + ' - /mnt/nextcloud-kamilkaprdelka-photos:/mnt/nextcloud-kamilkaprdelka-photos' \ + > "{{ immich_override_file }}" + fi + # Fail early if override is still missing/empty + test -s "{{ immich_override_file }}" + + + - | + cd {{ immich_dir }} + # Ensure .env exists. If missing, try to reconstruct it from running containers to avoid breaking DB creds. + python3 - <<'PY' + import json + import subprocess + from pathlib import Path + + env_path = Path(".env") + if env_path.exists(): + raise SystemExit(0) + + def run(cmd): + p = subprocess.run(cmd, capture_output=True, text=True) + return p.returncode, p.stdout, p.stderr + + rc, out, err = run(["bash", "-lc", "command docker inspect immich_postgres immich_server"]) + if rc != 0 or not out.strip(): + print("ERROR: .env is missing and cannot inspect running containers (immich_postgres/immich_server).", flush=True) + print("Create /opt/immich/.env manually or ensure the containers exist.", flush=True) + raise SystemExit(1) + + data = json.loads(out) + + by_name = {} + for c in data: + name = (c.get("Name") or "").lstrip("/") + by_name[name] = c + + pg = by_name.get("immich_postgres") + srv = by_name.get("immich_server") + if not pg or not srv: + print("ERROR: Could not find immich_postgres and immich_server in docker inspect output.", flush=True) + raise SystemExit(1) + + def env_map(container): + m = {} + for kv in (container.get("Config", {}).get("Env") or []): + if "=" in kv: + k, v = kv.split("=", 1) + m[k] = v + return m + + def find_mount_source(container, dest): + for m in (container.get("Mounts") or []): + if m.get("Destination") == dest: + return m.get("Source") + return "" + + pg_env = env_map(pg) + db_user = pg_env.get("POSTGRES_USER", "") + db_pass = pg_env.get("POSTGRES_PASSWORD", "") + db_name = pg_env.get("POSTGRES_DB", "") + + db_data = find_mount_source(pg, "/var/lib/postgresql/data") + upload_loc = find_mount_source(srv, "/usr/src/app/upload") + + # Try to preserve the currently used image tag as IMMICH_VERSION (optional but safer) + immich_version = "" + image = (srv.get("Config", {}).get("Image") or "") + if ":" in image and "@" not in image: + immich_version = image.rsplit(":", 1)[-1] + elif ":" in image and "@" in image: + # image like repo:tag@sha256:... + immich_version = image.split("@", 1)[0].rsplit(":", 1)[-1] + + missing = [] + for k, v in [ + ("DB_USERNAME", db_user), + ("DB_PASSWORD", db_pass), + ("DB_DATABASE_NAME", db_name), + ("DB_DATA_LOCATION", db_data), + ("UPLOAD_LOCATION", upload_loc), + ]: + if not v: + missing.append(k) + + if missing: + print("ERROR: Could not reconstruct these .env values from containers: " + ", ".join(missing), flush=True) + raise SystemExit(1) + + lines = [ + f"UPLOAD_LOCATION={upload_loc}", + f"DB_DATA_LOCATION={db_data}", + f"DB_USERNAME={db_user}", + f"DB_PASSWORD={db_pass}", + f"DB_DATABASE_NAME={db_name}", + ] + if immich_version: + lines.append(f"IMMICH_VERSION={immich_version}") + + env_path.write_text("\n".join(lines) + "\n", encoding="utf-8") + print("Created .env from running containers.", flush=True) + PY + + - | + cd {{ immich_dir }} + # Comment out healthcheck.start_interval if present (safe no-op if missing) + sed -i -E 's/^([[:space:]]*)start_interval:/\1# start_interval:/' docker-compose.yml || true + + - "cd {{ immich_dir }} && {{ immich_compose_cmd }} config >/dev/null" + - "cd {{ immich_dir }} && {{ immich_compose_cmd }} pull >/dev/null" + - "cd {{ immich_dir }} && {{ immich_compose_cmd }} up -d --remove-orphans --force-recreate >/dev/null" + + tasks: + - name: Ensure sshpass is installed (for password-based SSH) # English comments + ansible.builtin.apt: + name: sshpass + state: present + update_cache: yes + + - name: Run Immich update commands on VM (via SSH) # use SSHPASS env, hide item label + ansible.builtin.command: + argv: + - sshpass + - -e + - ssh + - -o + - StrictHostKeyChecking=no + - -o + - ConnectTimeout=15 + - "{{ vm_user }}@{{ vm_ip }}" + - bash + - -lc + - "{{ ('sudo ' if use_sudo else '') + item }}" + environment: + SSHPASS: "{{ vm_pass }}" + loop: "{{ immich_commands }}" + loop_control: + index_var: idx + label: "cmd-{{ idx }}" + register: immich_cmds + changed_when: false + no_log: "{{ DEBUG == 0 }}" + run_once: true + + - name: Show outputs for each Immich command + ansible.builtin.debug: + msg: | + CMD: {{ item.item }} + RC: {{ item.rc }} + STDOUT: + {{ (item.stdout | default('')).strip() }} + STDERR: + {{ (item.stderr | default('')).strip() }} + loop: "{{ immich_cmds.results }}" + when: DEBUG == 1 + run_once: true + + - name: Fail play if any Immich command failed + ansible.builtin.assert: + that: "item.rc == 0" + fail_msg: "Immich update failed on VM: {{ item.item }} (rc={{ item.rc }})" + success_msg: "All Immich update commands succeeded." + loop: "{{ immich_cmds.results }}" + loop_control: + index_var: idx + label: "cmd-{{ idx }}" + run_once: true + + # ------------------------- + # Readiness checks (controller first, then VM fallback) + # ------------------------- + + - name: Immich | Wait for API ping (controller first, with retries) + ansible.builtin.uri: + url: "{{ (immich_url | regex_replace('/$','')) + '/api/server/ping' }}" + method: GET + return_content: true + validate_certs: "{{ (immich_url | default('')) is match('^https://') }}" + status_code: 200 + register: immich_controller + delegate_to: localhost + run_once: true + when: immich_url is defined and (immich_url | length) > 0 + retries: "{{ immich_retries }}" + delay: "{{ immich_delay }}" + until: immich_controller.status == 200 and ('pong' in (immich_controller.content | default(''))) + failed_when: false + changed_when: false + + - name: Immich | VM-side ping (JSON via Python, with retries) + ansible.builtin.command: + argv: + - sshpass + - -e + - ssh + - -o + - StrictHostKeyChecking=no + - -o + - ConnectTimeout=15 + - "{{ vm_user }}@{{ vm_ip }}" + - bash + - -lc + - | + python3 - <<'PY' + # Ping Immich API from localhost and print response to stdout + import urllib.request, sys + try: + with urllib.request.urlopen("http://127.0.0.1:{{ immich_port }}/api/server/ping", timeout=15) as r: + sys.stdout.write(r.read().decode(errors='ignore')) + except Exception: + pass + PY + environment: + SSHPASS: "{{ vm_pass }}" + register: immich_vm + changed_when: false + failed_when: false + when: immich_controller.status | default(0) != 200 + retries: "{{ immich_retries }}" + delay: "{{ immich_delay }}" + until: (immich_vm.stdout | default('') | trim | length) > 0 and ('pong' in (immich_vm.stdout | default(''))) + no_log: "{{ DEBUG == 0 }}" + run_once: true + + - name: Immich | Print concise summary + ansible.builtin.debug: + msg: >- + Immich API ping {{ 'OK' if (('pong' in (immich_controller.content|default(''))) or ('pong' in (immich_vm.stdout|default('')))) else 'NOT OK' }}. + Source={{ 'controller' if (immich_controller.status|default(0))==200 else 'vm' if (immich_vm.stdout|default('')|trim|length>0) else 'n/a' }}. + when: DEBUG == 1 + run_once: true diff --git a/update_immich.yml b/update_immich.yml index a2a10a7..1822e8b 100644 --- a/update_immich.yml +++ b/update_immich.yml @@ -1,313 +1,84 @@ # update_immich.yml -- name: Update Immich on VM via Proxmox - hosts: linux_servers +- name: Update Immich + hosts: pve2_vm gather_facts: false - become: true - become_user: root - become_method: sudo vars: - # VM connection (provided by Semaphore env vars) - vm_ip: "{{ lookup('env', 'VM_IP') }}" - vm_user: "{{ lookup('env', 'VM_USER') }}" - vm_pass: "{{ lookup('env', 'VM_PASS') }}" - use_sudo: false + # Compose sync (controller -> target) + compose_local_dir: "{{ playbook_dir }}/docker-compose" + compose_remote_base: "/home/{{ ansible_user }}/.ansible-compose" + compose_remote_dir: "{{ compose_remote_base }}/docker-compose" + compose_remote_archive: "{{ compose_remote_base }}/docker-compose.tar.gz" - # --- Debug mode (controlled via Semaphore variable) --- - DEBUG: "{{ lookup('env', 'DEBUG') | default(0) | int }}" - RETRIES: "{{ lookup('env', 'RETRIES') | default(25) | int }}" - - # Immich specifics - immich_dir: "/opt/immich" - immich_project: "immich" - immich_compose_url: "https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml" - immich_compose_file: "/opt/immich/docker-compose.yml" - immich_override_file: "/opt/immich/docker-compose.override.yml" + # Immich settings + immich_project: immich immich_port: 2283 - # Optional external URL for controller-side readiness check (e.g., https://photos.example.com) - immich_url: "{{ lookup('env', 'IMMICH_URL') | default('', true) }}" - - # Retry policy - immich_retries: "{{ RETRIES }}" - immich_delay: 2 - - # Docker command prefix (consistent behavior) - docker_prefix: "unalias docker 2>/dev/null || true; DOCKER_CLI_HINTS=0; command docker" - - # Compose command (always include override to keep local mounts separate from upstream compose) - immich_compose_cmd: >- - {{ docker_prefix }} compose - -p {{ immich_project }} - -f {{ immich_compose_file }} - -f {{ immich_override_file }} - - # Commands to run on the target VM - immich_commands: - - "cd {{ immich_dir }}" - - - | - cd {{ immich_dir }} - mkdir -p backups - if [ -f docker-compose.yml ]; then - cp -a docker-compose.yml "backups/docker-compose.yml.$(date +%F_%H%M%S).bak" - fi - if [ -f .env ]; then - cp -a .env "backups/.env.$(date +%F_%H%M%S).bak" - fi - if [ -f docker-compose.override.yml ]; then - cp -a docker-compose.override.yml "backups/docker-compose.override.yml.$(date +%F_%H%M%S).bak" - fi - - - | - cd {{ immich_dir }} - # Download latest compose from Immich releases (requires curl or wget) - if command -v curl >/dev/null 2>&1; then - curl -fsSL -o docker-compose.yml "{{ immich_compose_url }}" - elif command -v wget >/dev/null 2>&1; then - wget -qO docker-compose.yml "{{ immich_compose_url }}" - else - echo "Neither curl nor wget is available on the VM." - exit 1 - fi - - - | - cd {{ immich_dir }} - # Ensure override compose exists (create if missing) - if [ ! -f "{{ immich_override_file }}" ]; then - printf '%s\n' \ - 'services:' \ - ' immich-server:' \ - ' volumes:' \ - ' - /mnt/nextcloud-howard-photos:/mnt/nextcloud-howard-photos' \ - ' - /mnt/nextcloud-kamilkaprdelka-photos:/mnt/nextcloud-kamilkaprdelka-photos' \ - > "{{ immich_override_file }}" - fi - # Fail early if override is still missing/empty - test -s "{{ immich_override_file }}" - - - - | - cd {{ immich_dir }} - # Ensure .env exists. If missing, try to reconstruct it from running containers to avoid breaking DB creds. - python3 - <<'PY' - import json - import subprocess - from pathlib import Path - - env_path = Path(".env") - if env_path.exists(): - raise SystemExit(0) - - def run(cmd): - p = subprocess.run(cmd, capture_output=True, text=True) - return p.returncode, p.stdout, p.stderr - - rc, out, err = run(["bash", "-lc", "command docker inspect immich_postgres immich_server"]) - if rc != 0 or not out.strip(): - print("ERROR: .env is missing and cannot inspect running containers (immich_postgres/immich_server).", flush=True) - print("Create /opt/immich/.env manually or ensure the containers exist.", flush=True) - raise SystemExit(1) - - data = json.loads(out) - - by_name = {} - for c in data: - name = (c.get("Name") or "").lstrip("/") - by_name[name] = c - - pg = by_name.get("immich_postgres") - srv = by_name.get("immich_server") - if not pg or not srv: - print("ERROR: Could not find immich_postgres and immich_server in docker inspect output.", flush=True) - raise SystemExit(1) - - def env_map(container): - m = {} - for kv in (container.get("Config", {}).get("Env") or []): - if "=" in kv: - k, v = kv.split("=", 1) - m[k] = v - return m - - def find_mount_source(container, dest): - for m in (container.get("Mounts") or []): - if m.get("Destination") == dest: - return m.get("Source") - return "" - - pg_env = env_map(pg) - db_user = pg_env.get("POSTGRES_USER", "") - db_pass = pg_env.get("POSTGRES_PASSWORD", "") - db_name = pg_env.get("POSTGRES_DB", "") - - db_data = find_mount_source(pg, "/var/lib/postgresql/data") - upload_loc = find_mount_source(srv, "/usr/src/app/upload") - - # Try to preserve the currently used image tag as IMMICH_VERSION (optional but safer) - immich_version = "" - image = (srv.get("Config", {}).get("Image") or "") - if ":" in image and "@" not in image: - immich_version = image.rsplit(":", 1)[-1] - elif ":" in image and "@" in image: - # image like repo:tag@sha256:... - immich_version = image.split("@", 1)[0].rsplit(":", 1)[-1] - - missing = [] - for k, v in [ - ("DB_USERNAME", db_user), - ("DB_PASSWORD", db_pass), - ("DB_DATABASE_NAME", db_name), - ("DB_DATA_LOCATION", db_data), - ("UPLOAD_LOCATION", upload_loc), - ]: - if not v: - missing.append(k) - - if missing: - print("ERROR: Could not reconstruct these .env values from containers: " + ", ".join(missing), flush=True) - raise SystemExit(1) - - lines = [ - f"UPLOAD_LOCATION={upload_loc}", - f"DB_DATA_LOCATION={db_data}", - f"DB_USERNAME={db_user}", - f"DB_PASSWORD={db_pass}", - f"DB_DATABASE_NAME={db_name}", - ] - if immich_version: - lines.append(f"IMMICH_VERSION={immich_version}") - - env_path.write_text("\n".join(lines) + "\n", encoding="utf-8") - print("Created .env from running containers.", flush=True) - PY - - - | - cd {{ immich_dir }} - # Comment out healthcheck.start_interval if present (safe no-op if missing) - sed -i -E 's/^([[:space:]]*)start_interval:/\1# start_interval:/' docker-compose.yml || true - - - "cd {{ immich_dir }} && {{ immich_compose_cmd }} config >/dev/null" - - "cd {{ immich_dir }} && {{ immich_compose_cmd }} pull >/dev/null" - - "cd {{ immich_dir }} && {{ immich_compose_cmd }} up -d --remove-orphans --force-recreate >/dev/null" + immich_compose_files: + - docker-compose-immich.yml + - docker-compose-immich.override.yml tasks: - - name: Ensure sshpass is installed (for password-based SSH) # English comments - ansible.builtin.apt: - name: sshpass - state: present - update_cache: yes + - name: Ensure remote base directory exists + ansible.builtin.file: + path: "{{ compose_remote_base }}" + state: directory + mode: "0755" - - name: Run Immich update commands on VM (via SSH) # use SSHPASS env, hide item label - ansible.builtin.command: - argv: - - sshpass - - -e - - ssh - - -o - - StrictHostKeyChecking=no - - -o - - ConnectTimeout=15 - - "{{ vm_user }}@{{ vm_ip }}" - - bash - - -lc - - "{{ ('sudo ' if use_sudo else '') + item }}" - environment: - SSHPASS: "{{ vm_pass }}" - loop: "{{ immich_commands }}" - loop_control: - index_var: idx - label: "cmd-{{ idx }}" - register: immich_cmds - changed_when: false - no_log: "{{ DEBUG == 0 }}" - run_once: true - - - name: Show outputs for each Immich command - ansible.builtin.debug: - msg: | - CMD: {{ item.item }} - RC: {{ item.rc }} - STDOUT: - {{ (item.stdout | default('')).strip() }} - STDERR: - {{ (item.stderr | default('')).strip() }} - loop: "{{ immich_cmds.results }}" - when: DEBUG == 1 - run_once: true - - - name: Fail play if any Immich command failed - ansible.builtin.assert: - that: "item.rc == 0" - fail_msg: "Immich update failed on VM: {{ item.item }} (rc={{ item.rc }})" - success_msg: "All Immich update commands succeeded." - loop: "{{ immich_cmds.results }}" - loop_control: - index_var: idx - label: "cmd-{{ idx }}" - run_once: true - - # ------------------------- - # Readiness checks (controller first, then VM fallback) - # ------------------------- - - - name: Immich | Wait for API ping (controller first, with retries) - ansible.builtin.uri: - url: "{{ (immich_url | regex_replace('/$','')) + '/api/server/ping' }}" - method: GET - return_content: true - validate_certs: "{{ (immich_url | default('')) is match('^https://') }}" - status_code: 200 - register: immich_controller + - name: Create local archive of docker-compose directory (controller) + ansible.builtin.archive: + path: "{{ compose_local_dir }}/" + dest: "/tmp/docker-compose.tar.gz" + format: gz delegate_to: localhost run_once: true - when: immich_url is defined and (immich_url | length) > 0 - retries: "{{ immich_retries }}" - delay: "{{ immich_delay }}" - until: immich_controller.status == 200 and ('pong' in (immich_controller.content | default(''))) - failed_when: false - changed_when: false - - name: Immich | VM-side ping (JSON via Python, with retries) - ansible.builtin.command: - argv: - - sshpass - - -e - - ssh - - -o - - StrictHostKeyChecking=no - - -o - - ConnectTimeout=15 - - "{{ vm_user }}@{{ vm_ip }}" - - bash - - -lc - - | - python3 - <<'PY' - # Ping Immich API from localhost and print response to stdout - import urllib.request, sys - try: - with urllib.request.urlopen("http://127.0.0.1:{{ immich_port }}/api/server/ping", timeout=15) as r: - sys.stdout.write(r.read().decode(errors='ignore')) - except Exception: - pass - PY - environment: - SSHPASS: "{{ vm_pass }}" - register: immich_vm - changed_when: false - failed_when: false - when: immich_controller.status | default(0) != 200 - retries: "{{ immich_retries }}" - delay: "{{ immich_delay }}" - until: (immich_vm.stdout | default('') | trim | length) > 0 and ('pong' in (immich_vm.stdout | default(''))) - no_log: "{{ DEBUG == 0 }}" - run_once: true + - name: Upload archive to remote host + ansible.builtin.copy: + src: "/tmp/docker-compose.tar.gz" + dest: "{{ compose_remote_archive }}" + mode: "0644" - - name: Immich | Print concise summary - ansible.builtin.debug: - msg: >- - Immich API ping {{ 'OK' if (('pong' in (immich_controller.content|default(''))) or ('pong' in (immich_vm.stdout|default('')))) else 'NOT OK' }}. - Source={{ 'controller' if (immich_controller.status|default(0))==200 else 'vm' if (immich_vm.stdout|default('')|trim|length>0) else 'n/a' }}. - when: DEBUG == 1 - run_once: true + - name: Recreate remote compose directory + ansible.builtin.file: + path: "{{ compose_remote_dir }}" + state: absent + + - name: Ensure remote compose directory exists + ansible.builtin.file: + path: "{{ compose_remote_dir }}" + state: directory + mode: "0755" + + - name: Extract archive on remote host + ansible.builtin.unarchive: + src: "{{ compose_remote_archive }}" + dest: "{{ compose_remote_dir }}" + remote_src: true + + - name: Pull latest Immich images + community.docker.docker_compose_v2: + project_name: "{{ immich_project }}" + project_src: "{{ compose_remote_dir }}" + files: "{{ immich_compose_files }}" + pull: always + + - name: Recreate Immich stack + community.docker.docker_compose_v2: + project_name: "{{ immich_project }}" + project_src: "{{ compose_remote_dir }}" + files: "{{ immich_compose_files }}" + state: present + recreate: always + + - name: Wait for Immich port + ansible.builtin.wait_for: + host: 127.0.0.1 + port: "{{ immich_port }}" + timeout: 120 + + - name: Check Immich HTTP endpoint + ansible.builtin.uri: + url: "http://127.0.0.1:{{ immich_port }}/" + status_code: 200