diff --git a/README.md b/README.md index c91c58b..b0da1de 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,12 @@ -# ansible-role-immich +# PeerTube Ansible role +This is an [Ansible](https://www.ansible.com/) role which installs [Immich](https://immich.app/) to run as a [Docker](https://www.docker.com/) container wrapped in a systemd service. + +This role *implicitly* depends on: + +- [`com.devture.ansible.role.playbook_help`](https://github.com/devture/com.devture.ansible.role.playbook_help) +- [`com.devture.ansible.role.systemd_docker_base`](https://github.com/devture/com.devture.ansible.role.systemd_docker_base) + +Check [defaults/main.yml](defaults/main.yml) for the full list of supported options. + +For an Ansible playbook which integrates this role and makes it easier to use, see the [mash-playbook](https://github.com/mother-of-all-self-hosting/mash-playbook). diff --git a/defaults/main.yml b/defaults/main.yml new file mode 100644 index 0000000..6a8d24c --- /dev/null +++ b/defaults/main.yml @@ -0,0 +1,90 @@ +--- + +# Project source code URL: https://github.com/imagegenius/docker-immich + +immich_enabled: true + +immich_identifier: immich + +# latest - Latest Immich release with an Ubuntu base. +# noml - Latest Immich release with an Ubuntu base. Machine-learning is completely removed, making it still compatible with hardware accelaration. +# alpine - Latest Immich release with an Alpine base. Machine-learning is completely removed, making it a very lightweight image (can have issues with RAW images). +immich_version: 'v1.93.3-ig193' +immich_distro_variant: noml + +immich_uid: '' +immich_gid: '' + +# The hostname at which Immich is served. +immich_hostname: '' + +# The path at which Immich is exposed. +# This value must either be `/` or not end with a slash (e.g. `/immich`). +immich_path_prefix: / + +immich_base_path: "{{ immich_playbook_base_path }}/immich" +immich_config_dir_path: "{{ immich_base_path }}/config" +immich_photos_dir_path: "{{ immich_base_path }}/photos" +immich_import_dir_path: "{{ immich_base_path }}/import" + +immich_systemd_required_services_list: "{{ immich_systemd_required_services_list_default + immich_systemd_required_services_list_auto + immich_systemd_required_services_list_custom }}" +immich_systemd_required_services_list_default: ['docker.service'] +immich_systemd_required_services_list_auto: [] +immich_systemd_required_services_list_custom: [] + +immich_config_database_hostname: '' +immich_config_database_port: 5432 +immich_config_database_name: immich +immich_config_database_username: '' +immich_config_database_password: '' + +immich_config_machine_learning_workers: '' +immich_config_machine_learning_workers_timeout: 120 + +# Controls the IMMICH_REDIS_HOSTNAME environment variable +immich_config_redis_hostname: '' + +# Controls the IMMICH_LOG_LEVEL environment variable. +# Valid values: debug, info, warn, error +immich_config_log_level: info + +immich_container_image: "{{ immich_container_image_registry_prefix }}imagegenius/immich:{{ immich_container_image_tag }}" +immich_container_image_registry_prefix: ghcr.io/ +immich_container_image_tag: "{{ immich_distro_variant }}-{{ immich_version }}" +immich_container_image_force_pull: "{{ immich_container_image_registry_prefix }}imagegenius/immich:latest" + +# The base container network. It will be auto-created by this role if it doesn't exist already. +immich_container_network: "{{ immich_identifier }}" + +# A list of additional container networks that the container would be connected to. +# The playbook does not create these networks, so make sure they already exist. +# Use this to expose the container to another reverse proxy, which runs in a different container network. +immich_container_additional_networks: "{{ immich_container_additional_networks_auto + immich_container_additional_networks_custom }}" +immich_container_additional_networks_auto: [] +immich_container_additional_networks_custom: [] + +# immich_container_labels_traefik_enabled controls whether labels to assist a Traefik reverse-proxy will be attached to the container. +# See `roles/immich/immich/templates/labels.j2` for details. +# +# To inject your own other container labels, see `immich_container_labels_additional_labels`. +immich_container_labels_traefik_enabled: true +immich_container_labels_traefik_docker_network: '' +immich_container_labels_traefik_hostname: "{{ immich_hostname }}" +# The path prefix must either be `/` or not end with a slash (e.g. `/immich`). +immich_container_labels_traefik_path_prefix: "{{ immich_path_prefix }}" +immich_container_labels_traefik_rule: "Host(`{{ immich_container_labels_traefik_hostname }}`){% if immich_container_labels_traefik_path_prefix != '/' %} && PathPrefix(`{{ immich_container_labels_traefik_path_prefix }}`){% endif %}" +immich_container_labels_traefik_priority: 0 +immich_container_labels_traefik_entrypoints: web-secure +immich_container_labels_traefik_tls: "{{ immich_container_labels_traefik_entrypoints != 'web' }}" +immich_container_labels_traefik_tls_certResolver: default # noqa var-naming + +# A list of extra arguments to pass to the container +immich_container_extra_arguments: [] + +# immich_container_additional_environment_variables contains a multiline string with additional environment variables to pass to the container. +# +# Example: +# immich_container_additional_environment_variables: | +# VAR=1 +# ANOTHER=value +immich_container_additional_environment_variables: '' \ No newline at end of file diff --git a/justfile b/justfile new file mode 100644 index 0000000..d938c3d --- /dev/null +++ b/justfile @@ -0,0 +1,6 @@ +# show help by default +default: + @just --list --justfile {{ justfile() }} + +lint: + ansible-lint . diff --git a/meta/main.yml b/meta/main.yml new file mode 100644 index 0000000..53a7666 --- /dev/null +++ b/meta/main.yml @@ -0,0 +1,21 @@ +galaxy_info: + author: virt + company: virt + role_name: immich + namespace: mash + description: immich + platforms: + - name: Debian + versions: + - all + - name: Ubuntu + versions: + - all + - name: Archlinux + versions: + - all + - name: EL + versions: + - 7 + license: GPL-3.0-or-later + min_ansible_version: '2.1' diff --git a/tasks/install.yml b/tasks/install.yml new file mode 100644 index 0000000..936728b --- /dev/null +++ b/tasks/install.yml @@ -0,0 +1,43 @@ +--- + +- name: Ensure Immich paths exist + ansible.builtin.file: + path: "{{ item }}" + state: directory + mode: "0750" + owner: "{{ immich_uid }}" + group: "{{ immich_gid }}" + with_items: + - "{{ immich_base_path }}" + - "{{ immich_config_dir_path }}" + - "{{ immich_photos_dir_path }}" + - "{{ immich_import_dir_path }}" + +- name: Ensure Immich support files created + ansible.builtin.template: + src: "{{ role_path }}/templates/{{ item }}.j2" + dest: "{{ immich_base_path }}/{{ item }}" + owner: "{{ immich_uid }}" + group: "{{ immich_gid }}" + mode: 0640 + with_items: + - env + - labels + +- name: Ensure Immich container network is created + community.general.docker_network: + name: "{{ immich_container_network }}" + driver: bridge + +- name: Ensure Immich container image is pulled + community.docker.docker_image: + name: "{{ immich_container_image }}" + source: "{{ 'pull' if ansible_version.major > 2 or ansible_version.minor > 7 else omit }}" + force_source: "{{ immich_container_image_force_pull if ansible_version.major > 2 or ansible_version.minor >= 8 else omit }}" + force: "{{ omit if ansible_version.major > 2 or ansible_version.minor >= 8 else immich_container_image_force_pull }}" + +- name: Ensure Immich systemd service installed + ansible.builtin.template: + src: "{{ role_path }}/templates/immich.service.j2" + dest: "{{ devture_systemd_docker_base_systemd_path }}/{{ immich_identifier }}.service" + mode: 0644 diff --git a/tasks/main.yml b/tasks/main.yml new file mode 100644 index 0000000..ea1f23b --- /dev/null +++ b/tasks/main.yml @@ -0,0 +1,20 @@ +--- + +- block: + - when: immich_enabled | bool + ansible.builtin.include_tasks: "{{ role_path }}/tasks/validate_config.yml" + + - when: immich_enabled | bool + ansible.builtin.include_tasks: "{{ role_path }}/tasks/install.yml" + tags: + - setup-all + - setup-immich + - install-all + - install-immich + +- block: + - when: not immich_enabled | bool + ansible.builtin.include_tasks: "{{ role_path }}/tasks/uninstall.yml" + tags: + - setup-all + - setup-immich diff --git a/tasks/uninstall.yml b/tasks/uninstall.yml new file mode 100644 index 0000000..37b81d5 --- /dev/null +++ b/tasks/uninstall.yml @@ -0,0 +1,25 @@ +--- + +- name: Check existence of Immich systemd service + ansible.builtin.stat: + path: "{{ devture_systemd_docker_base_systemd_path }}/{{ immich_identifier }}.service" + register: immich_service_stat + +- when: immich_service_stat.stat.exists | bool + block: + - name: Ensure Immich systemd service is stopped + ansible.builtin.service: + name: "{{ immich_identifier }}" + state: stopped + enabled: false + daemon_reload: true + + - name: Ensure Immich systemd service does not exists + ansible.builtin.file: + path: "{{ devture_systemd_docker_base_systemd_path }}/{{ immich_identifier }}.service" + state: absent + + - name: Ensure Immich path doesn't exist + ansible.builtin.file: + path: "{{ immich_base_path }}" + state: absent diff --git a/tasks/validate_config.yml b/tasks/validate_config.yml new file mode 100644 index 0000000..51edb0e --- /dev/null +++ b/tasks/validate_config.yml @@ -0,0 +1,38 @@ +--- + +- name: Fail if required Immich settings not defined + ansible.builtin.fail: + msg: >- + You need to define a required configuration setting (`{{ item }}`) for using this role. + when: "vars[item] | string == ''" + with_items: + - immich_identifier + - immich_uid + - immich_gid + - immich_container_network + - immich_hostname + - immich_path_prefix + - immich_config_database_hostname + - immich_config_database_username + - immich_config_database_password + - immich_config_redis_hostname + +- when: immich_container_labels_traefik_enabled | bool + block: + - name: Fail if required Immich Traefik settings not defined + ansible.builtin.fail: + msg: >- + You need to define a required configuration setting (`{{ item }}`). + when: "vars[item] == ''" + with_items: + - immich_container_labels_traefik_hostname + - immich_container_labels_traefik_path_prefix + + # We ensure it doesn't end with a slash, because we handle both (slash and no-slash). + # Knowing that `immich_container_labels_traefik_path_prefix` does not end with a slash + # ensures we know how to set these routes up without having to do "does it end with a slash" checks elsewhere. + - name: Fail if immich_container_labels_traefik_path_prefix ends with a slash + ansible.builtin.fail: + msg: >- + immich_container_labels_traefik_path_prefix (`{{ immich_container_labels_traefik_path_prefix }}`) must either be `/` or not end with a slash (e.g. `/immich`). + when: "immich_container_labels_traefik_path_prefix != '/' and immich_container_labels_traefik_path_prefix[-1] == '/'" diff --git a/templates/env.j2 b/templates/env.j2 new file mode 100644 index 0000000..462d9b9 --- /dev/null +++ b/templates/env.j2 @@ -0,0 +1,19 @@ +DB_USERNAME={{ immich_config_database_username }} +DB_PASSWORD={{ immich_config_database_password }} +DB_DATABASE_NAME={{ immich_config_database_name }} +IMMICH_DB_SSL=false +DB_HOSTNAME={{ immich_config_database_hostname }} +DB_PORT={{ immich_config_database_port }} + +REDIS_HOSTNAME={{ immich_config_redis_hostname }} + +PUID={{ immich_uid }} +PGID={{ immich_gid }} +TZ=Europe/Bratislava + +MACHINE_LEARNING_WORKERS={{ immich_config_machine_learning_workers }} +MACHINE_LEARNING_WORKER_TIMEOUT={{ immich_config_machine_learning_workers_timeout }} + +IMMICH_LOG_LEVEL={{ immich_config_log_level }} + +{{ immich_container_additional_environment_variables }} diff --git a/templates/immich.service.j2 b/templates/immich.service.j2 new file mode 100644 index 0000000..fe33b48 --- /dev/null +++ b/templates/immich.service.j2 @@ -0,0 +1,47 @@ +[Unit] +Description=Immich ({{ immich_identifier }}) +{% for service in immich_systemd_required_services_list %} +Requires={{ service }} +After={{ service }} +{% endfor %} +DefaultDependencies=no + +[Service] +Type=simple +Environment="HOME={{ devture_systemd_docker_base_systemd_unit_home_path }}" +ExecStartPre=-{{ devture_systemd_docker_base_host_command_sh }} -c '{{ devture_systemd_docker_base_host_command_docker }} stop --time={{ devture_systemd_docker_base_container_stop_grace_time_seconds }} {{ immich_identifier }} 2>/dev/null' +ExecStartPre=-{{ devture_systemd_docker_base_host_command_sh }} -c '{{ devture_systemd_docker_base_host_command_docker }} rm {{ immich_identifier }} 2>/dev/null' + +ExecStartPre={{ devture_systemd_docker_base_host_command_docker }} create \ + --rm \ + --name={{ immich_identifier }} \ + --log-driver=none \ + --network={{ immich_container_network }} \ + --user={{ immich_uid }}:{{ immich_gid }} \ + --cap-drop=ALL \ + --read-only \ + --env-file={{ immich_base_path }}/env \ + --label-file={{ immich_base_path }}/labels \ + --mount type=bind,src={{ immich_config_dir_path }},dst=/config \ + --mount type=bind,src={{ immich_photos_dir_path }},dst=/photos \ + --mount type=bind,src={{ immich_import_dir_path }},dst=/import,ro \ + --tmpfs=/tmp:rw,noexec,nosuid,size=512m \ + {% for arg in immich_container_extra_arguments %} + {{ arg }} \ + {% endfor %} + {{ immich_container_image }} + +{% for network in immich_container_additional_networks %} +ExecStartPre={{ devture_systemd_docker_base_host_command_docker }} network connect {{ network }} {{ immich_identifier }} +{% endfor %} + +ExecStart={{ devture_systemd_docker_base_host_command_docker }} start --attach {{ immich_identifier }} + +ExecStop=-{{ devture_systemd_docker_base_host_command_sh }} -c '{{ devture_systemd_docker_base_host_command_docker }} stop --time={{ devture_systemd_docker_base_container_stop_grace_time_seconds }} {{ immich_identifier }} 2>/dev/null' +ExecStop=-{{ devture_systemd_docker_base_host_command_sh }} -c '{{ devture_systemd_docker_base_host_command_docker }} rm {{ immich_identifier }} 2>/dev/null' +Restart=always +RestartSec=30 +SyslogIdentifier={{ immich_identifier }} + +[Install] +WantedBy=multi-user.target diff --git a/templates/labels.j2 b/templates/labels.j2 new file mode 100644 index 0000000..32abab8 --- /dev/null +++ b/templates/labels.j2 @@ -0,0 +1,38 @@ +{% if immich_container_labels_traefik_enabled %} +traefik.enable=true + +{% if immich_container_labels_traefik_docker_network %} +traefik.docker.network={{ immich_container_labels_traefik_docker_network }} +{% endif %} + +{% set middlewares = [] %} + +{% if immich_container_labels_traefik_path_prefix != '/' %} +traefik.http.middlewares.{{ immich_identifier }}-slashless-redirect.redirectregex.regex=^({{ immich_container_labels_traefik_path_prefix | quote }})$ +traefik.http.middlewares.{{ immich_identifier }}-slashless-redirect.redirectregex.replacement=${1}/ +{% set middlewares = middlewares + [immich_identifier + '-slashless-redirect'] %} +{% endif %} + +{% if immich_container_labels_traefik_path_prefix != '/' %} +traefik.http.middlewares.{{ immich_identifier }}-strip-prefix.stripprefix.prefixes={{ immich_container_labels_traefik_path_prefix }} +{% set middlewares = middlewares + [immich_identifier + '-strip-prefix'] %} +{% endif %} + +traefik.http.routers.{{ immich_identifier }}.rule={{ immich_container_labels_traefik_rule }} +{% if immich_container_labels_traefik_priority | int > 0 %} +traefik.http.routers.{{ immich_identifier }}.priority={{ immich_container_labels_traefik_priority }} +{% endif %} +traefik.http.routers.{{ immich_identifier }}.service={{ immich_identifier }} +{% if middlewares | length > 0 %} +traefik.http.routers.{{ immich_identifier }}.middlewares={{ middlewares | join(',') }} +{% endif %} +traefik.http.routers.{{ immich_identifier }}.entrypoints={{ immich_container_labels_traefik_entrypoints }} +traefik.http.routers.{{ immich_identifier }}.tls={{ immich_container_labels_traefik_tls | to_json }} +{% if immich_container_labels_traefik_tls %} +traefik.http.routers.{{ immich_identifier }}.tls.certResolver={{ immich_container_labels_traefik_tls_certResolver }} +{% endif %} + +traefik.http.services.{{ immich_identifier }}.loadbalancer.server.port=8080 +{% endif %} + +{{ immich_container_labels_additional_labels }}