diff --git a/immich-ansible/.gitignore b/immich-ansible/.gitignore new file mode 100644 index 0000000..f91f7e7 --- /dev/null +++ b/immich-ansible/.gitignore @@ -0,0 +1,3 @@ +*.retry +.vault_pass +.env diff --git a/immich-ansible/README.md b/immich-ansible/README.md new file mode 100644 index 0000000..19c6c34 --- /dev/null +++ b/immich-ansible/README.md @@ -0,0 +1,137 @@ +# Immich Ansible + +Playbook Ansible pour installer Immich auto-hébergé avec Docker Compose, PostgreSQL, Redis/Valkey, machine-learning et Nginx/Certbot. + +Il reprend le modèle du projet Forgejo fourni : + +- même host Ansible ; +- installation Docker Compose v2 ; +- reverse proxy Nginx + certificat Let's Encrypt ; +- port Docker hôte non standard ; +- données applicatives persistantes bind-mountées sur le partage NFS. + +## Structure + +```text +immich-ansible/ +├── ansible.cfg +├── inventory.ini +├── playbook.yml +├── group_vars/ +│ └── immich.yml +├── templates/ +│ ├── .env.j2 +│ └── docker-compose.yml.j2 +└── roles/ + └── nginx/ +``` + +## Configuration appliquée + +```yaml +immich_domain: "immich.esfs.fr" +immich_dir: "/opt/immich" +nfs_mount_point: "/mnt/nfs-share" +immich_library_path: "/mnt/nfs-share/applications/immich/library" +immich_postgres_data_path: "/opt/immich/postgres" +immich_host_http_port: "32283" +immich_container_http_port: "2283" +``` + +Nginx proxy vers : + +```text +http://127.0.0.1:32283 +``` + +L'accès final sera : + +```text +https://immich.esfs.fr +``` + +## Point important sur NFS + +La bibliothèque Immich est sur NFS : + +```text +/mnt/nfs-share/applications/immich/library +``` + +PostgreSQL est volontairement local : + +```text +/opt/immich/postgres +``` + +C'est fait ainsi parce que le fichier `example.env` officiel Immich précise que les partages réseau ne sont pas supportés pour `DB_DATA_LOCATION`. C'est aussi cohérent avec le projet Forgejo fourni, où les données Forgejo sont sur NFS mais PostgreSQL reste local. + +## Lancement + +Depuis le dossier du projet : + +```bash +ansible-playbook playbook.yml +``` + +Ou explicitement : + +```bash +ansible-playbook -i inventory.ini playbook.yml +``` + +## Après installation + +Ouvre : + +```text +https://immich.esfs.fr +``` + +Puis crée le premier compte administrateur via l'assistant Immich. + +## Variables utiles + +Dans `group_vars/immich.yml` : + +```yaml +immich_db_password: "Yb8qD7vRc4Nz29AhKp6Lx5Tf" +immich_version: "v2" +immich_host_http_port: "32283" +immich_nginx_client_max_body_size: "10G" +``` + +Le mot de passe PostgreSQL est volontairement alphanumérique uniquement. + +## Mise à jour Immich + +Sur le serveur : + +```bash +cd /opt/immich +docker compose pull +docker compose up -d +``` + +Ou relancer le playbook Ansible. + +## Sauvegarde + +À sauvegarder régulièrement : + +```text +/mnt/nfs-share/applications/immich/library +/opt/immich/postgres +/opt/immich/docker-compose.yml +/opt/immich/.env +``` + +Pour une sauvegarde froide : + +```bash +cd /opt/immich +docker compose down +# sauvegarde +docker compose up -d +``` + diff --git a/immich-ansible/ansible.cfg b/immich-ansible/ansible.cfg new file mode 100644 index 0000000..deb25ba --- /dev/null +++ b/immich-ansible/ansible.cfg @@ -0,0 +1,10 @@ +[defaults] +inventory = inventory.ini +host_key_checking = False +retry_files_enabled = False +stdout_callback = default +interpreter_python = auto_silent + +[privilege_escalation] +become = True +become_method = sudo diff --git a/immich-ansible/group_vars/immich.yml b/immich-ansible/group_vars/immich.yml new file mode 100644 index 0000000..7b88482 --- /dev/null +++ b/immich-ansible/group_vars/immich.yml @@ -0,0 +1,55 @@ +--- +# Immich variables (group_vars/immich.yml) +# Reprise du modèle Forgejo fourni : Docker Compose, Nginx/Certbot, host identique, +# données applicatives persistantes sur NFS, base PostgreSQL locale. + +# Réseau / domaine public +nginx_required: true +immich_domain: "immich.esfs.fr" +letsencrypt_email: "admin@esfs.fr" +immich_nginx_site_filename: "immich.esfs.fr.conf" +immich_nginx_client_max_body_size: "10G" + +# Chemins d'installation +immich_dir: "/opt/immich" +nfs_mount_point: "/mnt/nfs-share" +check_nfs_mount: true + +# Données persistantes +# La bibliothèque Immich contient les uploads, thumbnails, encoded-video, profile, backups, etc. +# Elle est bind-mountée sur le partage NFS, comme les données Forgejo dans le projet d'exemple. +immich_library_path: "{{ nfs_mount_point }}/applications/immich/library" + +# Important : Immich déconseille / ne supporte pas DB_DATA_LOCATION sur un partage réseau. +# On garde donc PostgreSQL en local, comme PostgreSQL Forgejo était local dans le projet fourni. +immich_postgres_data_path: "{{ immich_dir }}/postgres" +immich_model_cache_path: "{{ immich_dir }}/model-cache" + +# Ports exposés Docker +# Nginx proxy vers 127.0.0.1:{{ immich_host_http_port }}. +# Port hôte volontairement non standard pour éviter les conflits avec les autres applis web. +immich_host_http_port: "32283" +immich_container_http_port: "2283" + +# Images / version Immich +# v2 suit la branche majeure stable actuelle. Remplacer par une version précise si besoin, ex: v2.1.0. +immich_version: "v2" +immich_server_image: "ghcr.io/immich-app/immich-server" +immich_machine_learning_image: "ghcr.io/immich-app/immich-machine-learning" +immich_redis_image: "docker.io/valkey/valkey:9" +immich_postgres_image: "ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0" + +# PostgreSQL Immich +immich_db_username: "postgres" +immich_db_database: "immich" +# Alphanumérique uniquement, comme recommandé par Immich. +immich_db_password: "Yb8qD7vRc4Nz29AhKp6Lx5Tf" + +# Système +server_timezone: "Europe/Paris" +docker_remove_conflicting_packages: true + +# Permissions +immich_library_mode: "0777" +immich_postgres_uid: "999" +immich_postgres_gid: "999" diff --git a/immich-ansible/inventory.ini b/immich-ansible/inventory.ini new file mode 100644 index 0000000..7ce3789 --- /dev/null +++ b/immich-ansible/inventory.ini @@ -0,0 +1,2 @@ +[immich] +forgejo-server ansible_host=92.222.203.70 ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/ovhkey diff --git a/immich-ansible/playbook.yml b/immich-ansible/playbook.yml new file mode 100644 index 0000000..5b88b1c --- /dev/null +++ b/immich-ansible/playbook.yml @@ -0,0 +1,210 @@ +--- +- name: Installer Immich avec Docker Compose + hosts: immich + become: true + + tasks: + - name: Installer les prérequis système + ansible.builtin.apt: + name: + - ca-certificates + - curl + - gnupg + - lsb-release + - nfs-common + state: present + update_cache: true + + - name: Vérifier que le partage NFS est monté + ansible.builtin.command: + cmd: findmnt -T "{{ nfs_mount_point }}" + register: nfs_mount_check + changed_when: false + failed_when: nfs_mount_check.rc != 0 + when: check_nfs_mount | bool + + - name: Définir l'architecture Docker APT + ansible.builtin.set_fact: + docker_apt_arch: >- + {{ + { + 'x86_64': 'amd64', + 'aarch64': 'arm64', + 'armv7l': 'armhf', + 'armv6l': 'armhf' + }.get(ansible_architecture, ansible_architecture) + }} + docker_apt_distribution: >- + {{ + 'ubuntu' if ansible_distribution == 'Ubuntu' + else 'debian' if ansible_distribution == 'Debian' + else ansible_distribution | lower + }} + + - name: Supprimer les anciens paquets Docker conflictuels + ansible.builtin.apt: + name: + - docker.io + - docker-doc + - docker-compose + - podman-docker + - containerd + - runc + state: absent + when: docker_remove_conflicting_packages | bool + + - name: Créer le dossier des clés APT + ansible.builtin.file: + path: /etc/apt/keyrings + state: directory + owner: root + group: root + mode: "0755" + + - name: Installer la clé GPG du dépôt Docker officiel + ansible.builtin.get_url: + url: "https://download.docker.com/linux/{{ docker_apt_distribution }}/gpg" + dest: /etc/apt/keyrings/docker.asc + owner: root + group: root + mode: "0644" + + - name: Ajouter le dépôt Docker officiel + ansible.builtin.apt_repository: + repo: "deb [arch={{ docker_apt_arch }} signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/{{ docker_apt_distribution }} {{ ansible_distribution_release }} stable" + filename: docker + state: present + update_cache: true + + - name: Installer Docker Engine et Docker Compose v2 + ansible.builtin.apt: + name: + - docker-ce + - docker-ce-cli + - containerd.io + - docker-buildx-plugin + - docker-compose-plugin + state: present + update_cache: true + + - name: Activer et démarrer Docker + ansible.builtin.service: + name: docker + state: started + enabled: true + + - name: Créer le dossier Immich + ansible.builtin.file: + path: "{{ immich_dir }}" + state: directory + owner: root + group: root + mode: "0755" + + - name: Créer la bibliothèque Immich sur le NFS + ansible.builtin.file: + path: "{{ immich_library_path }}" + state: directory + mode: "{{ immich_library_mode }}" + recurse: true + + - name: Forcer les permissions sur la bibliothèque Immich NFS + ansible.builtin.command: + cmd: chmod -R {{ immich_library_mode }} "{{ immich_library_path }}" + changed_when: false + + - name: Créer le dossier PostgreSQL local Immich + ansible.builtin.file: + path: "{{ immich_postgres_data_path }}" + state: directory + owner: "{{ immich_postgres_uid }}" + group: "{{ immich_postgres_gid }}" + mode: "0700" + + - name: Créer le dossier de cache machine-learning Immich + ansible.builtin.file: + path: "{{ immich_model_cache_path }}" + state: directory + owner: root + group: root + mode: "0777" + + - name: Déployer le fichier .env Immich + ansible.builtin.template: + src: templates/.env.j2 + dest: "{{ immich_dir }}/.env" + owner: root + group: root + mode: "0600" + notify: Redémarrer Immich + + - name: Déployer docker-compose.yml Immich + ansible.builtin.template: + src: templates/docker-compose.yml.j2 + dest: "{{ immich_dir }}/docker-compose.yml" + owner: root + group: root + mode: "0644" + notify: Redémarrer Immich + + - name: Supprimer les anciens conteneurs Immich portant les mêmes noms + ansible.builtin.shell: | + old_ids=$(docker ps -aq \ + --filter "name=immich_server" \ + --filter "name=immich_machine_learning" \ + --filter "name=immich_redis" \ + --filter "name=immich_postgres") + if [ -n "$old_ids" ]; then + docker rm -f $old_ids + fi + printf "%s" "$old_ids" + changed_when: removed_immich_containers.stdout != '' + register: removed_immich_containers + + - name: Démarrer Immich + ansible.builtin.command: + cmd: docker compose up -d + chdir: "{{ immich_dir }}" + register: immich_compose_up + changed_when: >- + 'Started' in immich_compose_up.stdout or + 'Created' in immich_compose_up.stdout or + 'Recreated' in immich_compose_up.stdout or + 'Running' in immich_compose_up.stdout + + - name: Attendre que le port HTTP Immich soit ouvert localement + ansible.builtin.wait_for: + host: 127.0.0.1 + port: "{{ immich_host_http_port }}" + delay: 5 + timeout: 240 + state: started + + - name: Attendre que le conteneur Immich Server soit en état running + ansible.builtin.shell: | + cid=$(docker compose ps -q immich-server) + if [ -z "$cid" ]; then + exit 1 + fi + status=$(docker inspect "$cid" | grep -m1 '"Status":' | awk -F'"' '{print $4}') + [ "$status" = "running" ] + args: + chdir: "{{ immich_dir }}" + register: immich_container_status + retries: 30 + delay: 5 + until: immich_container_status.rc == 0 + changed_when: false + + - name: Lancer le rôle Nginx configuration réseau + ansible.builtin.include_role: + name: nginx + when: nginx_required | bool + + handlers: + - name: Redémarrer Immich + ansible.builtin.command: + cmd: docker compose up -d + chdir: "{{ immich_dir }}" + register: immich_restart + changed_when: true diff --git a/immich-ansible/roles/nginx/README.md b/immich-ansible/roles/nginx/README.md new file mode 100644 index 0000000..1682610 --- /dev/null +++ b/immich-ansible/roles/nginx/README.md @@ -0,0 +1,20 @@ +# Rôle Nginx Immich + +Ce rôle configure Nginx comme reverse proxy HTTPS pour Immich. + +Variables obligatoires : + +```yaml +immich_domain: "immich.esfs.fr" +immich_host_http_port: "32283" +letsencrypt_email: "admin@esfs.fr" +``` + +Variable optionnelle : + +```yaml +immich_nginx_client_max_body_size: "10G" +immich_nginx_site_filename: "immich.esfs.fr.conf" +``` + +Le rôle obtient automatiquement un certificat Let's Encrypt avec `certbot --nginx`. diff --git a/immich-ansible/roles/nginx/defaults/main.yml b/immich-ansible/roles/nginx/defaults/main.yml new file mode 100644 index 0000000..b9e2d69 --- /dev/null +++ b/immich-ansible/roles/nginx/defaults/main.yml @@ -0,0 +1,7 @@ +--- +# Nom du fichier dans /etc/nginx/sites-available et sites-enabled. +# Par défaut : "{{ immich_domain }}.conf" +# immich_nginx_site_filename: "{{ immich_domain }}.conf" + +# Uploads photos/vidéos : taille élevée par défaut. +immich_nginx_client_max_body_size: "10G" diff --git a/immich-ansible/roles/nginx/handlers/main.yml b/immich-ansible/roles/nginx/handlers/main.yml new file mode 100644 index 0000000..f51e66a --- /dev/null +++ b/immich-ansible/roles/nginx/handlers/main.yml @@ -0,0 +1,10 @@ +--- +- name: reload nginx + ansible.builtin.command: nginx -t + changed_when: true + notify: do reload nginx + +- name: do reload nginx + ansible.builtin.service: + name: nginx + state: reloaded diff --git a/immich-ansible/roles/nginx/tasks/config.yml b/immich-ansible/roles/nginx/tasks/config.yml new file mode 100644 index 0000000..44cb85b --- /dev/null +++ b/immich-ansible/roles/nginx/tasks/config.yml @@ -0,0 +1,63 @@ +--- +- name: Vérifier les variables obligatoires du rôle Nginx Immich + ansible.builtin.assert: + that: + - immich_domain is defined + - immich_domain | length > 0 + - immich_host_http_port is defined + - immich_host_http_port | string | length > 0 + - letsencrypt_email is defined + - letsencrypt_email | length > 0 + fail_msg: >- + Variables obligatoires manquantes pour le rôle nginx : + immich_domain, immich_host_http_port, letsencrypt_email. + +- name: Définir le nom du fichier de site Nginx Immich + ansible.builtin.set_fact: + immich_nginx_site_filename_resolved: "{{ immich_nginx_site_filename | default(immich_domain ~ '.conf') }}" + +- name: Supprimer le site Nginx par défaut si présent + ansible.builtin.file: + path: /etc/nginx/sites-enabled/default + state: absent + notify: reload nginx + +- name: Déployer la configuration HTTP temporaire Immich + ansible.builtin.template: + src: immich.http-only.conf.j2 + dest: "/etc/nginx/sites-available/{{ immich_nginx_site_filename_resolved }}" + owner: root + group: root + mode: "0644" + notify: reload nginx + +- name: Activer le site Nginx Immich + ansible.builtin.file: + src: "/etc/nginx/sites-available/{{ immich_nginx_site_filename_resolved }}" + dest: "/etc/nginx/sites-enabled/{{ immich_nginx_site_filename_resolved }}" + state: link + force: true + notify: reload nginx + +- name: Appliquer la configuration HTTP temporaire + ansible.builtin.meta: flush_handlers + +- name: Obtenir le certificat Let's Encrypt pour Immich + ansible.builtin.command: >- + certbot certonly --non-interactive --agree-tos + --email {{ letsencrypt_email }} + --nginx -d {{ immich_domain }} + args: + creates: "/etc/letsencrypt/live/{{ immich_domain }}/fullchain.pem" + +- name: Déployer la configuration HTTPS finale Immich + ansible.builtin.template: + src: immich.https.conf.j2 + dest: "/etc/nginx/sites-available/{{ immich_nginx_site_filename_resolved }}" + owner: root + group: root + mode: "0644" + notify: reload nginx + +- name: Appliquer la configuration HTTPS finale + ansible.builtin.meta: flush_handlers diff --git a/immich-ansible/roles/nginx/tasks/install.yml b/immich-ansible/roles/nginx/tasks/install.yml new file mode 100644 index 0000000..5f0a396 --- /dev/null +++ b/immich-ansible/roles/nginx/tasks/install.yml @@ -0,0 +1,15 @@ +--- +- name: Installer Nginx et Certbot + ansible.builtin.apt: + update_cache: true + name: + - nginx + - certbot + - python3-certbot-nginx + state: present + +- name: Activer et démarrer Nginx + ansible.builtin.service: + name: nginx + state: started + enabled: true diff --git a/immich-ansible/roles/nginx/tasks/main.yml b/immich-ansible/roles/nginx/tasks/main.yml new file mode 100644 index 0000000..05c35c9 --- /dev/null +++ b/immich-ansible/roles/nginx/tasks/main.yml @@ -0,0 +1,14 @@ +--- +- name: Installer / mettre à jour Nginx et Certbot + ansible.builtin.include_tasks: + file: install.yml + apply: + tags: [nginx_update, nginx_config] + tags: [nginx_update, nginx_config] + +- name: Configurer Nginx pour Immich + ansible.builtin.include_tasks: + file: config.yml + apply: + tags: [nginx_config] + tags: [nginx_config] diff --git a/immich-ansible/roles/nginx/templates/immich.http-only.conf.j2 b/immich-ansible/roles/nginx/templates/immich.http-only.conf.j2 new file mode 100644 index 0000000..f1c9011 --- /dev/null +++ b/immich-ansible/roles/nginx/templates/immich.http-only.conf.j2 @@ -0,0 +1,25 @@ +# Configuration HTTP temporaire pour obtenir le certificat Let's Encrypt +server { + listen 80; + server_name {{ immich_domain }}; + + client_max_body_size {{ immich_nginx_client_max_body_size | default('10G') }}; + + location / { + proxy_pass http://127.0.0.1:{{ immich_host_http_port }}; + + proxy_http_version 1.1; + 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; + + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + proxy_read_timeout 3600; + proxy_send_timeout 3600; + proxy_request_buffering off; + proxy_buffering off; + } +} diff --git a/immich-ansible/roles/nginx/templates/immich.https.conf.j2 b/immich-ansible/roles/nginx/templates/immich.https.conf.j2 new file mode 100644 index 0000000..7fc11aa --- /dev/null +++ b/immich-ansible/roles/nginx/templates/immich.https.conf.j2 @@ -0,0 +1,41 @@ +# HTTP -> HTTPS +server { + listen 80; + server_name {{ immich_domain }}; + + return 301 https://$host$request_uri; +} + +# Immich HTTPS reverse proxy +server { + listen 443 ssl http2; + server_name {{ immich_domain }}; + + client_max_body_size {{ immich_nginx_client_max_body_size | default('10G') }}; + + ssl_certificate /etc/letsencrypt/live/{{ immich_domain }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{{ immich_domain }}/privkey.pem; + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers off; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 1d; + + location / { + proxy_pass http://127.0.0.1:{{ immich_host_http_port }}; + + proxy_http_version 1.1; + 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 https; + + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + proxy_read_timeout 3600; + proxy_send_timeout 3600; + proxy_request_buffering off; + proxy_buffering off; + } +} diff --git a/immich-ansible/templates/.env.j2 b/immich-ansible/templates/.env.j2 new file mode 100644 index 0000000..b269500 --- /dev/null +++ b/immich-ansible/templates/.env.j2 @@ -0,0 +1,14 @@ +# Fichier généré par Ansible - ne pas modifier directement. + +# Bibliothèque Immich : bind mount vers le partage NFS +UPLOAD_LOCATION={{ immich_library_path }} + +# Base PostgreSQL : volontairement locale, pas sur NFS +DB_DATA_LOCATION={{ immich_postgres_data_path }} + +TZ={{ server_timezone }} +IMMICH_VERSION={{ immich_version }} + +DB_PASSWORD={{ immich_db_password }} +DB_USERNAME={{ immich_db_username }} +DB_DATABASE_NAME={{ immich_db_database }} diff --git a/immich-ansible/templates/docker-compose.yml.j2 b/immich-ansible/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..7984ea6 --- /dev/null +++ b/immich-ansible/templates/docker-compose.yml.j2 @@ -0,0 +1,64 @@ +name: immich + +services: + immich-server: + image: "{{ immich_server_image }}:${IMMICH_VERSION:-{{ immich_version }}}" + container_name: immich_server + restart: always + depends_on: + - redis + - database + env_file: + - .env + volumes: + - "${UPLOAD_LOCATION}:/data" + - /etc/localtime:/etc/localtime:ro + ports: + - "127.0.0.1:{{ immich_host_http_port }}:{{ immich_container_http_port }}" + healthcheck: + disable: false + networks: + - immich + + immich-machine-learning: + image: "{{ immich_machine_learning_image }}:${IMMICH_VERSION:-{{ immich_version }}}" + container_name: immich_machine_learning + restart: always + env_file: + - .env + volumes: + - "{{ immich_model_cache_path }}:/cache" + healthcheck: + disable: false + networks: + - immich + + redis: + image: "{{ immich_redis_image }}" + container_name: immich_redis + restart: always + healthcheck: + test: redis-cli ping || exit 1 + networks: + - immich + + database: + image: "{{ immich_postgres_image }}" + container_name: immich_postgres + restart: always + environment: + POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_USER: ${DB_USERNAME} + POSTGRES_DB: ${DB_DATABASE_NAME} + POSTGRES_INITDB_ARGS: "--data-checksums" + volumes: + - "${DB_DATA_LOCATION}:/var/lib/postgresql/data" + shm_size: 128mb + healthcheck: + disable: false + networks: + - immich + +networks: + immich: + name: immich