From 13839c9dfd07a594c821c4886d44bc6a1a3b8573 Mon Sep 17 00:00:00 2001 From: il Date: Sat, 21 Mar 2026 13:32:51 +0900 Subject: [PATCH] 1.4.0 Release immich --- ansible/inventory/group_vars/all.yaml | 1 + ansible/inventory/host_vars/app.yaml | 3 + ansible/playbooks/app/site.yaml | 8 ++ ansible/roles/app/handlers/main.yaml | 22 ++++ .../roles/app/tasks/services/set_immich.yaml | 120 ++++++++++++++++++ .../infra/tasks/services/set_postgresql.yaml | 3 +- config/secrets/secrets.yaml | 15 ++- .../app/immich/immich-ml.container.j2 | 32 +++++ .../containers/app/immich/immich.container.j2 | 49 +++++++ .../auth/authelia/config/authelia.yaml.j2 | 23 ++++ .../common/caddy/etc/app/Caddyfile.j2 | 6 + .../common/caddy/etc/auth/Caddyfile.j2 | 9 ++ docs/services/app/immich.md | 86 +++++++++++++ 13 files changed, 374 insertions(+), 3 deletions(-) create mode 100644 ansible/roles/app/tasks/services/set_immich.yaml create mode 100644 config/services/containers/app/immich/immich-ml.container.j2 create mode 100644 config/services/containers/app/immich/immich.container.j2 create mode 100644 docs/services/app/immich.md diff --git a/ansible/inventory/group_vars/all.yaml b/ansible/inventory/group_vars/all.yaml index ae5650c..eee8470 100644 --- a/ansible/inventory/group_vars/all.yaml +++ b/ansible/inventory/group_vars/all.yaml @@ -76,3 +76,4 @@ version: vaultwarden: "1.35.4" gitea: "1.25.5" redis: "8.6.1" + immich: "v2.6.1" diff --git a/ansible/inventory/host_vars/app.yaml b/ansible/inventory/host_vars/app.yaml index 13e4f51..ed29b97 100644 --- a/ansible/inventory/host_vars/app.yaml +++ b/ansible/inventory/host_vars/app.yaml @@ -39,3 +39,6 @@ storage: label: "APP_DATA" level: "raid10" mount_point: "/home/app/data" + +redis: + immich: "6379" diff --git a/ansible/playbooks/app/site.yaml b/ansible/playbooks/app/site.yaml index 9b7e9e6..1d2a3dc 100644 --- a/ansible/playbooks/app/site.yaml +++ b/ansible/playbooks/app/site.yaml @@ -177,6 +177,14 @@ tags: ["site", "gitea"] tags: ["site", "gitea"] + - name: Set immich + ansible.builtin.include_role: + name: "app" + tasks_from: "services/set_immich" + apply: + tags: ["site", "immich"] + tags: ["site", "immich"] + - name: Flush handlers right now ansible.builtin.meta: "flush_handlers" diff --git a/ansible/roles/app/handlers/main.yaml b/ansible/roles/app/handlers/main.yaml index acede68..b8eadea 100644 --- a/ansible/roles/app/handlers/main.yaml +++ b/ansible/roles/app/handlers/main.yaml @@ -20,3 +20,25 @@ changed_when: false listen: "notification_restart_gitea" ignore_errors: true # noqa: ignore-errors + +- name: Restart immich + ansible.builtin.systemd: + name: "immich.service" + state: "restarted" + enabled: true + scope: "user" + daemon_reload: true + changed_when: false + listen: "notification_restart_immich" + ignore_errors: true # noqa: ignore-errors + +- name: Restart immich-ml + ansible.builtin.systemd: + name: "immich-ml.service" + state: "restarted" + enabled: true + scope: "user" + daemon_reload: true + changed_when: false + listen: "notification_restart_immich-ml" + ignore_errors: true # noqa: ignore-errors diff --git a/ansible/roles/app/tasks/services/set_immich.yaml b/ansible/roles/app/tasks/services/set_immich.yaml new file mode 100644 index 0000000..376d180 --- /dev/null +++ b/ansible/roles/app/tasks/services/set_immich.yaml @@ -0,0 +1,120 @@ +--- +- name: Set redis service name + ansible.builtin.set_fact: + redis_service: "immich" + redis_subuid: "100998" + +- name: Create redis_immich directory + ansible.builtin.file: + path: "{{ node['home_path'] }}/{{ item }}" + state: "directory" + owner: "{{ redis_subuid }}" + group: "svadmins" + mode: "0770" + loop: + - "containers/redis" + - "containers/redis/{{ redis_service }}" + - "containers/redis/{{ redis_service }}/data" + become: true + +- name: Deploy redis config file + ansible.builtin.template: + src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/redis/redis.conf.j2" + dest: "{{ node['home_path'] }}/containers/redis/{{ redis_service }}/redis.conf" + owner: "{{ ansible_user }}" + group: "svadmins" + mode: "0644" + +- name: Deploy redis container file + ansible.builtin.template: + src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/redis/redis.container.j2" + dest: "{{ node['home_path'] }}/.config/containers/systemd/redis_{{ redis_service }}.container" + owner: "{{ ansible_user }}" + group: "svadmins" + mode: "0644" + register: "is_redis_conf" + +- name: Enable (Restart) redis service + ansible.builtin.systemd: + name: "redis_{{ redis_service }}.service" + state: "restarted" + enabled: true + daemon_reload: true + scope: "user" + when: is_redis_conf.changed # noqa: no-handler + +- name: Add user in video, render group + ansible.builtin.user: + name: "{{ ansible_user }}" + state: "present" + groups: "video, render" + append: true + become: true + +- name: Create immich directory + ansible.builtin.file: + path: "{{ node['home_path'] }}/{{ item }}" + state: "directory" + owner: "{{ ansible_user }}" + group: "svadmins" + mode: "0770" + loop: + - "data/containers/immich" + - "containers/immich" + - "containers/immich/ssl" + - "containers/immich/ml" + - "containers/immich/ml/cache" + +- name: Deploy root certificate + ansible.builtin.copy: + content: | + {{ hostvars['console']['ca']['root']['crt'] }} + dest: "{{ node['home_path'] }}/containers/immich/ssl/ilnmors_root_ca.crt" + owner: "{{ ansible_user }}" + group: "svadmins" + mode: "0440" + notify: "notification_restart_immich" + no_log: true + +- name: Register secret value to podman secret + containers.podman.podman_secret: + name: "IMMICH_DB_PASSWORD" + data: "{{ hostvars['console']['postgresql']['password']['immich'] }}" + state: "present" + force: true + notify: "notification_restart_immich" + no_log: true + +- name: Deploy immich.container file + ansible.builtin.template: + src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/immich/immich.container.j2" + dest: "{{ node['home_path'] }}/.config/containers/systemd/immich.container" + owner: "{{ ansible_user }}" + group: "svadmins" + mode: "0644" + notify: "notification_restart_immich" + +- name: Deploy immich-ml.container file + ansible.builtin.template: + src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/immich/immich-ml.container.j2" + dest: "{{ node['home_path'] }}/.config/containers/systemd/immich-ml.container" + owner: "{{ ansible_user }}" + group: "svadmins" + mode: "0644" + notify: "notification_restart_immich-ml" + +- name: Enable immich.service + ansible.builtin.systemd: + name: "immich.service" + state: "started" + enabled: true + daemon_reload: true + scope: "user" + +- name: Enable immich-ml.service + ansible.builtin.systemd: + name: "immich-ml.service" + state: "started" + enabled: true + daemon_reload: true + scope: "user" diff --git a/ansible/roles/infra/tasks/services/set_postgresql.yaml b/ansible/roles/infra/tasks/services/set_postgresql.yaml index 11cc0da..d5e8fe5 100644 --- a/ansible/roles/infra/tasks/services/set_postgresql.yaml +++ b/ansible/roles/infra/tasks/services/set_postgresql.yaml @@ -11,6 +11,7 @@ - "grafana" - "vaultwarden" - "gitea" + - "immich" - name: Create postgresql directory ansible.builtin.file: @@ -108,7 +109,7 @@ group: "svadmins" mode: "0600" - - name: Deploy resoring data sql files + - name: Deploy restoring data sql files ansible.builtin.copy: src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/infra/postgresql/init/pg_{{ item }}.sql" dest: "{{ node['home_path'] }}/containers/postgresql/init/{{ index_num + 1 }}_pg_{{ item }}.sql" diff --git a/config/secrets/secrets.yaml b/config/secrets/secrets.yaml index 089f6f0..dad34e2 100644 --- a/config/secrets/secrets.yaml +++ b/config/secrets/secrets.yaml @@ -115,6 +115,7 @@ postgresql: authelia: ENC[AES256_GCM,data:OqyloAADO6KKEaBjGLsJc9GTe77wn6IvA1VCD2dfCWxx+zgzUYh87fK1XX8=,iv:QIOHNTdNnzcY/f3Co8dPdNHykhBnYRhm43nt35hbALM=,tag:DLQq58GrZd+Ul7MSn6s9uQ==,type:str] vaultwarden: ENC[AES256_GCM,data:BPj5eFo54DTZ82n3yTIqEbm7kb/jWT0n2kZY//oV5q48eRch3C2RBuxn/Ko=,iv:DGC4ipHMyVs25gc4sNMt8LN1RsHjiR/b303vgiFoxMY=,tag:k1eb4DoRPLKvvMstSI1faQ==,type:str] gitea: ENC[AES256_GCM,data:l+pBCzyQa3000SE9z1R4htD0V0ONsBtKy92dfgsVYsZ3XlEyVJDIBOsugwM=,iv:5t/oHW1vFAmV/s2Ze/cV9Vuqo96Qu6QvZeRbio7VX2s=,tag:4zeQaXiXIzBpy+tXsxmN7Q==,type:str] + immich: ENC[AES256_GCM,data:11jvxTKA/RL0DGL6y2/X092hnDohj6yTrYGK4IVojqBd1gCOBnDvUjgmx14=,iv:oBfHxsx9nxhyKY/WOuWfybxEX2bf+lHEtsaifFRS9lg=,tag:tAfkBdgQ8ZEkLIFcDICKDw==,type:str] #ENC[AES256_GCM,data:ODXFUxxxdQ==,iv:s9zJVx6wo6x517tbNvC+FZ0dFzqbjqeLI6rXBq72hQA=,tag:bXoV2I3LbpmQyddJrtS3Qg==,type:comment] # # @@ -195,6 +196,16 @@ gitea: #ENC[AES256_GCM,data:ODXFUxxxdQ==,iv:s9zJVx6wo6x517tbNvC+FZ0dFzqbjqeLI6rXBq72hQA=,tag:bXoV2I3LbpmQyddJrtS3Qg==,type:comment] # # +#ENC[AES256_GCM,data:HvMeGuC8JwK50pO1E/nm,iv:5NFTyjesMX0ZnBpH+hEv8jQ0G2NvrDtT23CUyLbQcUo=,tag:qWmPvQpADTeD+W8nWXRQvA==,type:comment] +immich: + il: + password: ENC[AES256_GCM,data:PbDFc4m7rNPPN1mCjcvhbKwf/EbiJxdvO/iMspf9jMuCqQyGv7h6VrZqk98=,iv:hlMAp5wXXkFO9+ekq3A2/ioF/EX8Uau0puhb4TAHkRQ=,tag:pfS0W2JNaFNMpBlKgZ3Pjw==,type:str] + oidc: + secret: ENC[AES256_GCM,data:9ENcp2Ns21OLXDY05zRoSdP+93EiwSH8MGzZZpxK7sToe4QLUXWt9w6xQIE=,iv:Q/VcnArZHs/J2YLRVFXt3Mp+LYfuq4PD/trqO8Simig=,tag:BY/mOyZpYmoAc7NrASlmSA==,type:str] + hash: ENC[AES256_GCM,data:mrML2CWFFtGjq8wfWipVpv+pjJRSHe74VGC7Eoa6588R5C/sCnC3W5aI+dsRCZN3LRCjHAkOJJgjeYrwcXYdKRauXsAYR51dNSsHqqSN3WebLxapRDwcYu5e4j5RN1aPHsysr7GaQ4hhe5rKW4ORCGC3Cp3Ob+LChPy4bdCAZG3bN9k=,iv:Q4hmqhq+dvIr7DxCpcqP4E0NKyFZkOeTnDpGctmCxXM=,tag:/gji6AFkHnYwkQf3FSQUxA==,type:str] +#ENC[AES256_GCM,data:ODXFUxxxdQ==,iv:s9zJVx6wo6x517tbNvC+FZ0dFzqbjqeLI6rXBq72hQA=,tag:bXoV2I3LbpmQyddJrtS3Qg==,type:comment] +# +# #ENC[AES256_GCM,data:T4Wtn49AAxPd2QUFTR+q,iv:bH5goGWBDqumAat9dUv2OwfCUJUpuVqncTMqMBZUXhI=,tag:G+W6hHA+yftQ+4RJpXrxHg==,type:comment] switch: password: ENC[AES256_GCM,data:qu0f9L7A0eFq/UCpaRs=,iv:W8LLOp3MSfd/+EfNEZNf91K8GgI5eUfVPoWTRES2C0Y=,tag:Q5FlAOfwqwJwPvd7k6i+0g==,type:str] @@ -224,7 +235,7 @@ sops: UmliaFNxVTBqRkI1QWJpWGpTRWxETW8KEY/8AfU73UOzCGhny1cNnd5dCNv7bHXt k+uyWPPi+enFkVaceSwMFrA66uaWWrwAj11sXEB7yzvGFPrnAGezjQ== -----END AGE ENCRYPTED FILE----- - lastmodified: "2026-03-14T21:11:56Z" - mac: ENC[AES256_GCM,data:cTm/Vrukt1cnq5xWmfF7J3xhDrVH94ampOK1IqZgsBICuJgDm6VYbntElCcnrmPE4BrIAFr8SyDQa28QgLf+e2UBn36yVUNZTor1kx24WkMsjj4MIRJRlYSQoFAg1iJSGTshqiwgQQMfj1nOCctLXdNAyQFWHt+E+9zqeXFB/l8=,iv:/dDIeqEW8/T5V8glumE3cTTy9e3bFAvg3GaMHAYkFi0=,tag:7eUKYIkju8wniz+/xmGfgg==,type:str] + lastmodified: "2026-03-21T00:50:58Z" + mac: ENC[AES256_GCM,data:xLYH+20TGYk0piWG7OaJqysuEc2xlqpKZcvCIKj9xOIgQIXVS8l5YXXAv6c4uen2cNe/0DIWs04Cb6cdItRWWq46jV3F6+u1BOy7mSmQ40nmbKCy+qls8TaYBeqhlrffpy5yYolDdztYqBjZYQDjY6rNXUIp7UU1VJEZkpY96lI=,iv:EZUezHsy6pghcWa/rq5CObaw3CRW7JJB5zdmCp6mIAA=,tag:JJVI0EDuA90uafbLwdn4ww==,type:str] unencrypted_suffix: _unencrypted version: 3.12.1 diff --git a/config/services/containers/app/immich/immich-ml.container.j2 b/config/services/containers/app/immich/immich-ml.container.j2 new file mode 100644 index 0000000..f41c09b --- /dev/null +++ b/config/services/containers/app/immich/immich-ml.container.j2 @@ -0,0 +1,32 @@ +[Quadlet] +DefaultDependencies=false + +[Unit] +Description=Immich Machine Learning + +After=immich.service +Wants=immich.service + +[Container] +Image=ghcr.io/immich-app/immich-machine-learning:{{ version['containers']['immich'] }}-openvino + +ContainerName=immich-ml +HostName=immich-ml + +PublishPort=3003:3003 + +# iGPU access for OpenVINO +AddDevice=/dev/dri:/dev/dri +PodmanArgs=--group-add keep-groups + +Volume=%h/containers/immich/ml/cache:/cache:rw + +Environment="TZ=Asia/Seoul" + +[Service] +Restart=always +RestartSec=10s +TimeoutStopSec=120 + +[Install] +WantedBy=default.target diff --git a/config/services/containers/app/immich/immich.container.j2 b/config/services/containers/app/immich/immich.container.j2 new file mode 100644 index 0000000..5a228c3 --- /dev/null +++ b/config/services/containers/app/immich/immich.container.j2 @@ -0,0 +1,49 @@ +[Quadlet] +DefaultDependencies=false + +[Unit] +Description=Immich + +After=redis_immich.service +Wants=redis_immich.service + +[Container] +Image=ghcr.io/immich-app/immich-server:{{ version['containers']['immich'] }} + +ContainerName=immich +HostName=immich + +PublishPort=2283:2283 + +# iGPU access +AddDevice=/dev/dri:/dev/dri +PodmanArgs=--group-add keep-groups + +# Volumes +Volume=%h/data/containers/immich:/data:rw +Volume=%h/containers/immich/ssl:/etc/ssl/immich:ro + +# Environment +Environment="TZ=Asia/Seoul" +Environment="REDIS_HOSTNAME=host.containers.internal" +Environment="REDIS_PORT={{ hostvars['app']['redis']['immich'] }}" +Environment="REDIS_DBINDEX=0" + +# Database +Environment="DB_HOSTNAME={{ infra_uri['postgresql']['domain'] }}" +Environment="DB_PORT={{ infra_uri['postgresql']['ports']['tcp'] }}" +Environment="DB_USERNAME=immich" +Environment="DB_DATABASE_NAME=immich_db" +Environment="DB_PASSWORD_FILE=/run/secrets/DB_PASSWORD" +Environment="DB_SSL_MODE=verify-full" +Environment="NODE_EXTRA_CA_CERTS=/etc/ssl/immich/ilnmors_root_ca.crt" +Secret=IMMICH_DB_PASSWORD,target=/run/secrets/DB_PASSWORD + +[Service] +ExecStartPre=/usr/bin/nc -zv {{ infra_uri['postgresql']['domain'] }} {{ infra_uri['postgresql']['ports']['tcp'] }} +Restart=always +RestartSec=10s +TimeoutStopSec=120 + +[Install] +WantedBy=default.target diff --git a/config/services/containers/auth/authelia/config/authelia.yaml.j2 b/config/services/containers/auth/authelia/config/authelia.yaml.j2 index 5247cfb..6a9c649 100644 --- a/config/services/containers/auth/authelia/config/authelia.yaml.j2 +++ b/config/services/containers/auth/authelia/config/authelia.yaml.j2 @@ -152,3 +152,26 @@ identity_providers: access_token_signed_response_alg: 'none' userinfo_signed_response_alg: 'none' token_endpoint_auth_method: 'client_secret_basic' + # https://www.authelia.com/integration/openid-connect/clients/immich/ + - client_id: 'immich' + client_name: 'immich' + client_secret: '{{ hostvars['console']['immich']['oidc']['hash'] }}' + public: false + authorization_policy: 'one_factor' + require_pkce: false + pkce_challenge_method: '' + redirect_uris: + - 'https://immich.ilnmors.com/auth/login' + - 'https://immich.ilnmors.com/user-settings' + - 'app.immich:///oauth-callback' + scopes: + - 'openid' + - 'profile' + - 'email' + response_types: + - 'code' + grant_types: + - 'authorization_code' + access_token_signed_response_alg: 'none' + userinfo_signed_response_alg: 'none' + token_endpoint_auth_method: 'client_secret_post' diff --git a/config/services/containers/common/caddy/etc/app/Caddyfile.j2 b/config/services/containers/common/caddy/etc/app/Caddyfile.j2 index efaca6b..6a99d8b 100644 --- a/config/services/containers/common/caddy/etc/app/Caddyfile.j2 +++ b/config/services/containers/common/caddy/etc/app/Caddyfile.j2 @@ -40,3 +40,9 @@ gitea.app.ilnmors.internal { header_up Host {http.request.header.X-Forwarded-Host} } } +immich.app.ilnmors.internal { + import private_tls + reverse_proxy host.containers.internal:2283 { + header_up Host {http.request.header.X-Forwarded-Host} + } +} diff --git a/config/services/containers/common/caddy/etc/auth/Caddyfile.j2 b/config/services/containers/common/caddy/etc/auth/Caddyfile.j2 index 90faa15..64daeab 100644 --- a/config/services/containers/common/caddy/etc/auth/Caddyfile.j2 +++ b/config/services/containers/common/caddy/etc/auth/Caddyfile.j2 @@ -81,6 +81,15 @@ gitea.ilnmors.com { } } } +immich.ilnmors.com { + import crowdsec_log + route { + crowdsec + reverse_proxy https://immich.app.ilnmors.internal { + header_up Host {http.reverse_proxy.upstream.host} + } + } +} # Internal domain auth.ilnmors.internal { diff --git a/docs/services/app/immich.md b/docs/services/app/immich.md new file mode 100644 index 0000000..80f04aa --- /dev/null +++ b/docs/services/app/immich.md @@ -0,0 +1,86 @@ +# immich + +## Prerequisite + +### Create database + +- Create the password with `openssl rand -base64 32` + - Save this value in secrets.yaml in `postgresql.password.immich` + - Access infra server to create immich_db with `podman exec -it postgresql psql -U postgres` + +```SQL +CREATE USER immich WITH PASSWORD 'postgresql.password.immich'; +CREATE DATABASE immich_db; +ALTER DATABASE immich_db OWNER TO immich; +\connect immich_db +CREATE EXTENSION IF NOT EXISTS vchord CASCADE; +CREATE EXTENSION IF NOT EXISTS cube CASCADE; +CREATE EXTENSION IF NOT EXISTS earthdistance CASCADE; +\dx +-- Check the extension is activated with `\dx` +-- postgresql image is built with `pgvector` and `vectorchord` already +``` + +### Create oidc secret and hash + +- Create the secret with `openssl rand -base64 32` +- access to auth vm + - `podman exec -it authelia sh` + - `authelia crypto hash generate pbkdf2 --password 'immich.oidc.secret'` +- Save this value in secrets.yaml in `immich.oidc.secret` and `immich.oidc.hash` + +### Create admin password + +- Create the secret with `openssl rand -base64 32` +- Save this value in secrets.yaml in `immich.il.password` +- +### Add postgresql dump backup list + +- [set_postgresql.yaml](../../../ansible/roles/infra/tasks/services/set_postgresql.yaml) + +```yaml +- name: Set connected services list + ansible.builtin.set_fact: + connected_services: + - ... + - "immich" +``` + +## Configuration + +### Access to immich + +- https://immich.ilnmors.com + - Getting started + - admin E-mail + - admin password + - admin name +- Theme +- language +- Server privacy + - map + - version check +- User privacy + - google cast \(disable\) +- Storage template + - `{{y}}/{{MM}}/{{y}}{{MM}}{{dd}}_{{hh}}{{mm}}{{ss}}` +- Backups +- Mobile App +- Done + +### Oauth configuration + +- Administartion: Authentication Settings: OAuth: Enable + - Issuer URL: https://auth.example.com/.well-known/openid-configuration + - Client ID: immich + - Client Secret: immich.oidc.secret + - Scope: openid profile email + - Button Text: Login with Authelia + - Auto Register: Enable if desired + +### Machine learning configuration + +- Administration: Machine Learning Settings: Enable + - URL: http://host.containers.internal:3003 +- **!CAUTION!** + - immich-ml should contain `-openvino` to use GPU for machine learning.