From e0df8cfba5f6984a7c7ff7c6ac4f2625791c78eb Mon Sep 17 00:00:00 2001 From: il Date: Sun, 5 Jul 2026 23:25:42 +0900 Subject: [PATCH] feat(mastodon): release mastodon --- ansible/inventory/group_vars/all.yaml | 38 +- ansible/inventory/group_vars/hypervisor.yaml | 17 +- ansible/playbooks/app/site.yaml | 8 + ansible/roles/app/handlers/main.yaml | 15 + .../app/tasks/services/set_mastodon.yaml | 276 +++++++++ .../app/tasks/services/set_nextcloud.yaml | 2 +- .../roles/app/tasks/services/set_outline.yaml | 2 +- .../roles/app/tasks/services/set_sure.yaml | 2 +- config/secrets/secrets.yaml | 43 +- .../app/collabora/collabora.container.j2 | 2 +- .../mastodon/mastodon-streaming.container.j2 | 46 ++ .../app/mastodon/mastodon-web.container.j2 | 74 +++ .../app/mastodon/mastodon-worker.container.j2 | 57 ++ .../containers/app/sure/sure-web.container.j2 | 2 +- .../app/sure/sure-worker.container.j2 | 21 +- .../auth/authelia/config/authelia.yaml.j2 | 21 + .../containers/common/caddy/etc/Caddyfile.j2 | 6 + .../infra/prometheus/etc/rules.yaml.j2 | 16 +- .../systemd/common/alloy/config.alloy.j2 | 4 +- .../services/app/bonfire/bonfire.container.j2 | 59 ++ .../services/app/bonfire/group_vars.yaml | 14 + .../archives/services/app/bonfire/runtime.exs | 562 ++++++++++++++++++ .../services/app/bonfire/secret.example.yaml | 12 + .../services/app/bonfire/set_bonfire.yaml | 101 ++++ 24 files changed, 1338 insertions(+), 62 deletions(-) create mode 100644 ansible/roles/app/tasks/services/set_mastodon.yaml create mode 100644 config/services/containers/app/mastodon/mastodon-streaming.container.j2 create mode 100644 config/services/containers/app/mastodon/mastodon-web.container.j2 create mode 100644 config/services/containers/app/mastodon/mastodon-worker.container.j2 create mode 100644 docs/archives/services/app/bonfire/bonfire.container.j2 create mode 100644 docs/archives/services/app/bonfire/group_vars.yaml create mode 100644 docs/archives/services/app/bonfire/runtime.exs create mode 100644 docs/archives/services/app/bonfire/secret.example.yaml create mode 100644 docs/archives/services/app/bonfire/set_bonfire.yaml diff --git a/ansible/inventory/group_vars/all.yaml b/ansible/inventory/group_vars/all.yaml index 7d07fc0..f246a51 100644 --- a/ansible/inventory/group_vars/all.yaml +++ b/ansible/inventory/group_vars/all.yaml @@ -140,9 +140,9 @@ services: dependency: web: "auth-public" db: "postgresql" + # app redis: subuid: "100998" - # app homepage: domain: public: "@" @@ -179,6 +179,18 @@ services: dependency: web: "app-public" db: "postgresql" + mastodon: + domain: + public: "mastodon" + internal: "mastodon.app" + ports: + http: "3006" + streaming: "4000" + redis: "6385" + subuid: "100990" + dependency: + web: "app-public" + db: "postgresql" nextcloud: domain: public: "nextcloud" @@ -277,11 +289,11 @@ services: version: packages: - sops: "3.13.1" + sops: "3.13.2" step: "0.30.2" kopia: "0.23.1" - blocky: "0.32.1" - alloy: "1.17.0" + blocky: "0.33.0" + alloy: "1.17.1" containers: # common caddy: "2.11.4" @@ -290,30 +302,32 @@ version: ## Postgresql postgresql: "18.4" ### For immich - https://github.com/immich-app/base-images/blob/main/postgres/versions.yaml - ### pgvector: "v0.8.1" vectorchord: "1.1.1" + ### For bonfire + postgis: "3" ## lldap ldap: "v0.6.3" ## maddy maddy: "0.9.5" x509-exporter: "4.1.0" - prometheus: "v3.12.0" + prometheus: "v3.13.0" loki: "3.7.3" - grafana: "13.0.2" + grafana: "13.1.0" # Auth authelia: "4.39.20" # App vaultwarden: "1.36.0" gitea: "1.26.4" redis: "8.8.0" - immich: "v2.7.5" + immich: "v3.0.1" paperless: "2.20.15" - nextcloud: "34.0.0" - collabora: "26.04.1.4.1" - sure: "0.7.1-hotfix.1" + nextcloud: "34.0.1" + collabora: "26.04.2.1.1" + sure: "0.7.2" outline: "1.8.1" memos: "0.29.1" - mealie: "v3.19.2" + mealie: "v3.20.1" homebox: "0.26.2" homepage: "v1.13.2" snappymail: "v2.38.2" + mastodon: "v4.6.3" diff --git a/ansible/inventory/group_vars/hypervisor.yaml b/ansible/inventory/group_vars/hypervisor.yaml index 19783e4..c9e5038 100644 --- a/ansible/inventory/group_vars/hypervisor.yaml +++ b/ansible/inventory/group_vars/hypervisor.yaml @@ -22,12 +22,23 @@ hardware: slot: "0x02" function: "0x0" disk: + # NVMe is set when the hypervisor is initiated manually. + nvme: + serials: + - "S5GXNX0T228635E" + fs: + type: "ext4" + label: "" + mount_point: + - "/" + - "/var/lib/libvirt" + mount_opts: "" data: serials: - "WMC6N0E617S8" - - "P6HVTL0W" - - "P2GH354H" - - "P6GJKSMP" + - "PN1134P6HVTL0W" + - "PN1138P2GH354H" + - "PN2131P6GJKSMP" fs: type: "btrfs" label: "DATA" diff --git a/ansible/playbooks/app/site.yaml b/ansible/playbooks/app/site.yaml index 81f9ec1..449b194 100644 --- a/ansible/playbooks/app/site.yaml +++ b/ansible/playbooks/app/site.yaml @@ -266,6 +266,14 @@ tags: ["site", "homebox"] tags: ["site", "homebox"] + - name: Set mastodon + ansible.builtin.include_role: + name: "app" + tasks_from: "services/set_mastodon" + apply: + tags: ["site", "mastodon"] + tags: ["site", "mastodon"] + - name: Set homepage ansible.builtin.include_role: name: "app" diff --git a/ansible/roles/app/handlers/main.yaml b/ansible/roles/app/handlers/main.yaml index 63a2618..bc85f4b 100644 --- a/ansible/roles/app/handlers/main.yaml +++ b/ansible/roles/app/handlers/main.yaml @@ -157,3 +157,18 @@ changed_when: false listen: "notification_restart_snappymail" ignore_errors: true # noqa: ignore-errors + +- name: Restart mastodon + ansible.builtin.systemd: + name: "{{ item }}" + state: "restarted" + enabled: true + scope: "user" + daemon_reload: true + loop: + - "mastodon-web.service" + - "mastodon-worker.service" + - "mastodon-streaming.service" + changed_when: false + listen: "notification_restart_mastodon" + ignore_errors: true # noqa: ignore-errors diff --git a/ansible/roles/app/tasks/services/set_mastodon.yaml b/ansible/roles/app/tasks/services/set_mastodon.yaml new file mode 100644 index 0000000..8de8c7d --- /dev/null +++ b/ansible/roles/app/tasks/services/set_mastodon.yaml @@ -0,0 +1,276 @@ +--- +- name: Set redis service name + ansible.builtin.set_fact: + redis_service: "mastodon" + +- name: Create redis_mastodon directory + ansible.builtin.file: + path: "{{ node['home_path'] }}/{{ item }}" + state: "directory" + owner: "{{ services['redis']['subuid'] }}" + group: "{{ node['group'] }}" + 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: "{{ node['group'] }}" + mode: "0644" + register: "is_redis_conf" + +- 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: "{{ node['group'] }}" + mode: "0644" + register: "is_redis_containerfile" + +- 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 or is_redis_containerfile.changed # noqa: no-handler + +- name: Create mastodon directory + ansible.builtin.file: + path: "{{ node['home_path'] }}/{{ item }}" + state: "directory" + owner: "{{ services['mastodon']['subuid'] }}" + group: "{{ node['group'] }}" + mode: "0770" + loop: + - "data/containers/mastodon" + - "data/containers/mastodon/system" + - "containers/mastodon" + - "containers/mastodon/ssl" + become: true + +- name: Check data directory empty + ansible.builtin.stat: + path: "{{ node['home_path'] }}/data/containers/mastodon/.init" + register: "is_mastodon_init" + +- name: Check migrate file + ansible.builtin.stat: + path: "{{ node['home_path'] }}/data/containers/mastodon/.migrated_{{ version['containers']['mastodon'] }}" + register: "is_mastodon_migrated" + +- name: Deploy root certificate + ansible.builtin.copy: + content: | + {{ hostvars['console']['ca']['root']['crt'] }} + dest: "{{ node['home_path'] }}/containers/mastodon/ssl/{{ root_cert_filename }}" + owner: "{{ services['mastodon']['subuid'] }}" + group: "{{ node['group'] }}" + mode: "0440" + become: true + notify: "notification_restart_mastodon" + no_log: true + +- name: Register secret value to podman secret + containers.podman.podman_secret: + name: "{{ item.name }}" + data: "{{ item.value }}" + state: "present" + force: true + loop: + - name: "MASTODON_SECRET_KEY_BASE" + value: "{{ hostvars['console']['mastodon']['secret_key'] }}" + - name: "MASTODON_ENCRYPTION_PRIMARY_KEY" + value: "{{ hostvars['console']['mastodon']['encryption']['primary_key'] }}" + - name: "MASTODON_ENCRYPTION_DETERMINISTIC_KEY" + value: "{{ hostvars['console']['mastodon']['encryption']['deterministic_key'] }}" + - name: "MASTODON_ENCRYPTION_SALT" + value: "{{ hostvars['console']['mastodon']['encryption']['salt'] }}" + - name: "MASTODON_POSTGRES_PASSWORD" + value: "{{ hostvars['console']['postgresql']['password']['mastodon'] }}" + - name: "MASTODON_OIDC_CLIENT_SECRET" + value: "{{ hostvars['console']['mastodon']['oidc']['secret'] }}" + - name: "MASTODON_VAPID_PRIVATE_KEY" + value: "{{ hostvars['console']['mastodon']['vapid']['private_key'] }}" + - name: "MASTODON_VAPID_PUBLIC_KEY" + value: "{{ hostvars['console']['mastodon']['vapid']['public_key'] }}" + notify: "notification_restart_mastodon" + no_log: true + +- name: Pull image + containers.podman.podman_image: + name: "{{ item }}" + tag: "{{ version['containers']['mastodon'] }}" + state: "present" + pull: true + loop: + - "ghcr.io/mastodon/mastodon" + - "ghcr.io/mastodon/mastodon-streaming" + +- name: Initialize mastodon + when: not is_mastodon_init.stat.exists + block: + - name: Execute init command + containers.podman.podman_container: + name: "mastodon_init" + image: "ghcr.io/mastodon/mastodon:{{ version['containers']['mastodon'] }}" + command: "bundle exec rails db:prepare" + state: "started" + rm: true + detach: false + env: + LOCAL_DOMAIN: "{{ services['mastodon']['domain']['public'] }}.{{ domain['public'] }}" + SAFETY_ASSURED: "1" + DB_HOST: "{{ services['postgresql']['domain']['internal'] }}.{{ domain['internal'] }}" + DB_PORT: "{{ services['postgresql']['ports']['tcp'] }}" + DB_NAME: "mastodon_db" + DB_USER: "mastodon" + DB_PASS: "{{ hostvars['console']['postgresql']['password']['mastodon'] }}" + DB_SSLMODE: "verify-full" + PGSSLROOTCERT: "/etc/ssl/mastodon/{{ root_cert_filename }}" + REDIS_URL: "redis://host.containers.internal:{{ services['mastodon']['ports']['redis'] }}" + SECRET_KEY_BASE: "{{ hostvars['console']['mastodon']['secret_key'] }}" + ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY: "{{ hostvars['console']['mastodon']['encryption']['primary_key'] }}" + ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY: "{{ hostvars['console']['mastodon']['encryption']['deterministic_key'] }}" + ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT: "{{ hostvars['console']['mastodon']['encryption']['salt'] }}" + volume: + - "{{ node['home_path'] }}/containers/mastodon/ssl:/etc/ssl/mastodon:ro" + - "{{ node['home_path'] }}/data/containers/mastodon/system:/mastodon/public/system:rw" + no_log: true + + - name: Create .init file + ansible.builtin.file: + path: "{{ node['home_path'] }}/data/containers/mastodon/.init" + state: "touch" + mode: "0644" + owner: "{{ ansible_user }}" + group: "{{ node['group'] }}" + + - name: Create .migrated file + ansible.builtin.file: + path: "{{ node['home_path'] }}/data/containers/mastodon/.migrated_{{ version['containers']['mastodon'] }}" + state: "touch" + mode: "0644" + owner: "{{ ansible_user }}" + group: "{{ node['group'] }}" + +- name: Migrate DB of mastodon + when: not is_mastodon_migrated.stat.exists and is_mastodon_init.stat.exists + block: + - name: Stop the services + ansible.builtin.systemd: + name: "{{ item }}" + state: "stopped" + enabled: true + daemon_reload: true + scope: "user" + loop: + - "mastodon-web.service" + - "mastodon-worker.service" + - "mastodon-streaming.service" + + - name: Execute migrate command + containers.podman.podman_container: + name: "mastodon_migrate" + image: "ghcr.io/mastodon/mastodon:{{ version['containers']['mastodon'] }}" + command: "bundle exec rails db:migrate" + state: "started" + rm: true + detach: false + env: + LOCAL_DOMAIN: "{{ services['mastodon']['domain']['public'] }}.{{ domain['public'] }}" + DB_HOST: "{{ services['postgresql']['domain']['internal'] }}.{{ domain['internal'] }}" + DB_PORT: "{{ services['postgresql']['ports']['tcp'] }}" + DB_NAME: "mastodon_db" + DB_USER: "mastodon" + DB_PASS: "{{ hostvars['console']['postgresql']['password']['mastodon'] }}" + DB_SSLMODE: "verify-full" + PGSSLROOTCERT: "/etc/ssl/mastodon/{{ root_cert_filename }}" + REDIS_URL: "redis://host.containers.internal:{{ services['mastodon']['ports']['redis'] }}" + SECRET_KEY_BASE: "{{ hostvars['console']['mastodon']['secret_key'] }}" + ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY: "{{ hostvars['console']['mastodon']['encryption']['primary_key'] }}" + ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY: "{{ hostvars['console']['mastodon']['encryption']['deterministic_key'] }}" + ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT: "{{ hostvars['console']['mastodon']['encryption']['salt'] }}" + volume: + - "{{ node['home_path'] }}/containers/mastodon/ssl:/etc/ssl/mastodon:ro" + - "{{ node['home_path'] }}/data/containers/mastodon/system:/mastodon/public/system:rw" + no_log: true + + - name: Create .migrated file + ansible.builtin.file: + path: "{{ node['home_path'] }}/data/containers/mastodon/.migrated_{{ version['containers']['mastodon'] }}" + state: "touch" + mode: "0644" + owner: "{{ ansible_user }}" + group: "{{ node['group'] }}" + +- name: Deploy mastodon.container file + ansible.builtin.template: + src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/mastodon/{{ item }}.j2" + dest: "{{ node['home_path'] }}/.config/containers/systemd/{{ item }}" + owner: "{{ ansible_user }}" + group: "{{ node['group'] }}" + mode: "0644" + loop: + - "mastodon-web.container" + - "mastodon-worker.container" + - "mastodon-streaming.container" + notify: "notification_restart_mastodon" + +- name: Enable mastodon.service + ansible.builtin.systemd: + name: "{{ item }}" + state: "started" + enabled: true + daemon_reload: true + scope: "user" + loop: + - "mastodon-web.service" + - "mastodon-worker.service" + - "mastodon-streaming.service" + +- name: Check container archive images + ansible.builtin.stat: + path: "{{ node['home_path'] }}/archives/containers/{{ item.file }}.tar" + loop: + - name: "redis" + image: "docker.io/library/redis:{{ version['containers']['redis'] }}" + file: "docker.io_library_redis_{{ version['containers']['redis'] }}" + - name: "mastodon" + image: "ghcr.io/mastodon/mastodon:{{ version['containers']['mastodon'] }}" + file: "ghcr.io_mastodon_mastodon_{{ version['containers']['mastodon'] }}" + - name: "mastodon-streaming" + image: "ghcr.io/mastodon/mastodon-streaming:{{ version['containers']['mastodon'] }}" + file: "ghcr.io_mastodon_mastodon-streaming_{{ version['containers']['mastodon'] }}" + loop_control: + label: "{{ item.name }}" + register: container_archive_images + +- name: Save container archive images + containers.podman.podman_save: + image: + - "{{ item.item.image }}" + dest: "{{ node['home_path'] }}/archives/containers/{{ item.item.file }}.tar" + format: "oci-archive" + force: false + loop: "{{ container_archive_images.results }}" + loop_control: + label: "{{ item.item.file }}" + when: not item.stat.exists + +- name: Fetch container archive images + ansible.builtin.fetch: + src: "{{ node['home_path'] }}/archives/containers/{{ item.item.file }}.tar" + dest: "{{ hostvars['console']['node']['data_path'] }}/images/containers/" + flat: true + loop: "{{ container_archive_images.results }}" + loop_control: + label: "{{ item.item.file }}" diff --git a/ansible/roles/app/tasks/services/set_nextcloud.yaml b/ansible/roles/app/tasks/services/set_nextcloud.yaml index 2a4a774..0a992f0 100644 --- a/ansible/roles/app/tasks/services/set_nextcloud.yaml +++ b/ansible/roles/app/tasks/services/set_nextcloud.yaml @@ -78,7 +78,7 @@ - name: Initialize nextcloud when: not is_nextcloud_init.stat.exists block: - - name: Execute init command (Including pulling image) + - name: Execute init command containers.podman.podman_container: name: "nextcloud_init" image: "docker.io/library/nextcloud:{{ version['containers']['nextcloud'] }}" diff --git a/ansible/roles/app/tasks/services/set_outline.yaml b/ansible/roles/app/tasks/services/set_outline.yaml index 4f1975c..a9ebefe 100644 --- a/ansible/roles/app/tasks/services/set_outline.yaml +++ b/ansible/roles/app/tasks/services/set_outline.yaml @@ -91,7 +91,7 @@ force: true loop: - name: "OUTLINE_SECRET_KEY" - value: "{{ hostvars['console']['outline']['session_secret'] }}" + value: "{{ hostvars['console']['outline']['secret_key'] }}" - name: "OUTLINE_UTILS_SECRET" value: "{{ hostvars['console']['outline']['utils_secret'] }}" - name: "OUTLINE_DATABASE_PASSWORD" diff --git a/ansible/roles/app/tasks/services/set_sure.yaml b/ansible/roles/app/tasks/services/set_sure.yaml index 32873cd..dbf70f4 100644 --- a/ansible/roles/app/tasks/services/set_sure.yaml +++ b/ansible/roles/app/tasks/services/set_sure.yaml @@ -78,7 +78,7 @@ force: true loop: - name: "SURE_SECRET_KEY_BASE" - value: "{{ hostvars['console']['sure']['session_secret'] }}" + value: "{{ hostvars['console']['sure']['secret_key'] }}" - name: "SURE_POSTGRES_PASSWORD" value: "{{ hostvars['console']['postgresql']['password']['sure'] }}" - name: "SURE_OIDC_CLIENT_SECRET" diff --git a/config/secrets/secrets.yaml b/config/secrets/secrets.yaml index dc07aec..7c2394c 100644 --- a/config/secrets/secrets.yaml +++ b/config/secrets/secrets.yaml @@ -139,6 +139,7 @@ postgresql: memos: ENC[AES256_GCM,data:BaNTnZFZH4oMTnDkgrf2/fMTkO43rc9bSsgnDWrHJ/Z6+MZdMitpIMkrMN4=,iv:JsG3B0YweZYO96MW9NmoSsIG++gFq+jRUsozwJtWl3M=,tag:orLTUiW0inOJdD4YSp4baQ==,type:str] mealie: ENC[AES256_GCM,data:XbYHQv6HBWCXUBRmcbQZN5Y1Gw+V+Wra28Mp+m+ZGl8VRXsKMu5507vGHQM=,iv:zp4ZarqG9KshiBV8F8UksfI2QXmXHwqPw631r/5bYy8=,tag:jvsvXrUsCvpQGIbLV9jTYQ==,type:str] homebox: ENC[AES256_GCM,data:EhNnltOBwLai5vEMt7Dih5Ug6h3ojrdbwhq4Vr0tg/p/0nqI9L7O/Le0B4w=,iv:14oyl916t0inr+W/lztQCjYiSQ0vIIwC9NmbezDuvvY=,tag:IiXZd3NU+dUvDrHpxJXFew==,type:str] + mastodon: ENC[AES256_GCM,data:hq1ETBK9iUI7kWFO+9pZwMQMbmyLExGk7/VzBwDdLFbzptXL2KEi6oYRCS0=,iv:hF1C8lsnP2i3LxDIuOA6op4qKXQKjJ7gqznDst7hVAU=,tag:maR7Jvsv3DJGrO77EzmBDA==,type:str] #ENC[AES256_GCM,data:ODXFUxxxdQ==,iv:s9zJVx6wo6x517tbNvC+FZ0dFzqbjqeLI6rXBq72hQA=,tag:bXoV2I3LbpmQyddJrtS3Qg==,type:comment] # # @@ -272,7 +273,7 @@ nextcloud: # #ENC[AES256_GCM,data:Fsqc2JDp9dvfgiCjdQ==,iv:3DALKKEXaP8hzXRvxD4CgfFpOiPPsOa16OB94n8WKp8=,tag:K+FF3zGrc0YLXWK/R2L3Ow==,type:comment] sure: - session_secret: ENC[AES256_GCM,data:InHsz/jld8E9TwI8MWpxk9x2I7dxlIsY9R6jtDK2pBA=,iv:HY5yXEC2Dce26e9/vXTIWELvVd9ZjhcCwFD0jhz5pPw=,tag:LLSJovZ0RH3CUK+se7R4Ag==,type:str] + secret_key: ENC[AES256_GCM,data:jZLkIoLG7Y5JaMlfG7XTVnrgmM3JiJUfYEXzi4XHukE=,iv:VkgZbr9GDMQuAA4xvls9eNp479ylus0LHzLqArKt1Bk=,tag:7+g7JOEQUzZaKc1JYeCwmA==,type:str] oidc: secret: ENC[AES256_GCM,data:9BSvpcU9BJctSN9bkPIAsRxg8JNHTWvOKdpJFhm//CUDm/Xc7oC/ANHf5no=,iv:JVQLl/rp65kZSK/4SpVXxtiac3Z35XNkxWm2+lEdq/c=,tag:WgfaORiNlrO+wHSdnl4CWQ==,type:str] hash: ENC[AES256_GCM,data:EjJ+1fP7/9wG2jG0Jv2hxMLtErqxjHBstRjru79dd5ZXhqwT7S+jpLfl9WpZU9qi20ps9YP4qe7G08p6NJNXjYhQj852GQxEORRh/9StAZsPt3p8w+ePZSVbivPQH+FpPKWYxoH0VR7y3TnL66R0tKRLh1fNTc5jRy5rU5r1bfs1jZ0=,iv:0y9FxW4QdD7qHz3bPRWlwHFpvOsvlYhVrOItB6BzaE8=,tag:Wc7MZhP3QRYmvZcjpoEWtQ==,type:str] @@ -281,7 +282,7 @@ sure: # #ENC[AES256_GCM,data:H6BtfLuevnDCEpHSmf6pxQ==,iv:9Jw0UZwVwS2It7c4e1rAN/BEiqDKp9DHD4W9xdpJ/Vs=,tag:Xj8HKvxgCA260CXBVK2S1A==,type:comment] outline: - session_secret: ENC[AES256_GCM,data:A26j8/ghGA9MjDiuq+H84iTFXkHZp3l3LbGKO9w+Cgv+u1dkTF5q4zuBnOSBi+Tnz+2Y4jIoxsac+CS0dguRMg==,iv:NzJiJxVxOvud3e4mP8urnHrqugE8SmjFVnZtwfzrUzs=,tag:YR/jexb3G0HfC2qHpn6fPA==,type:str] + secret_key: ENC[AES256_GCM,data:0kfEulDNslLfm8aNSPaWci2qmWiEHU+RF1pyAddcFJjhV0Rj8WduJjIwEQkp8pmAt2QE76yAi4Flksj0VgMNJA==,iv:XvRgWkhfsLofms5M4Y+3M3ycgNzB8ZmDnDWklB8ZwvM=,tag:ccADOhrpmiDjK64JA3tkmw==,type:str] utils_secret: ENC[AES256_GCM,data:ACI+NZzqHBsYeb4WwTJ1nfJcy6zNwJn2hBqA93aM5cgNWQ4KtQHDXgmS6UTwmSpxrteiaH25J3Np03H3eQoVeA==,iv:nitwxxwTPQvoQXGLVMnk2zsdC7M+JIXkTY4zJdQ3DFo=,tag:IQewG7EE9IgQk+Rfz7M5Iw==,type:str] oidc: secret: ENC[AES256_GCM,data:zR6sN205C51BIiQxLYOwC4ZbTYGr2AveiPeEbdadairmfnNJkGOtm0zT4Gg=,iv:uyYoJYuAuH/Gy2dlLr2SkkLYq+lMdwBgmzUsEj1EAKc=,tag:uhgvE6BagvPPKf7Y3NaGfA==,type:str] @@ -314,26 +315,28 @@ homebox: #ENC[AES256_GCM,data:ODXFUxxxdQ==,iv:s9zJVx6wo6x517tbNvC+FZ0dFzqbjqeLI6rXBq72hQA=,tag:bXoV2I3LbpmQyddJrtS3Qg==,type:comment] # # +#ENC[AES256_GCM,data:3vSrCbjgdJF8gQ429r/xMCE=,iv:b7+xc6VGoCBttqUFb7tk4qTYvqdIT4U2+EjVZgPqB6c=,tag:60uoeIZEkQk+h2MkzGK60g==,type:comment] +mastodon: + secret_key: ENC[AES256_GCM,data:nyymxkVxZK2Lr7QHtX5A/C+18/xHJj8B3EYbqy133reQMxGPyt1ROvnuKSU=,iv:htu8aWqS1ANJeAt6K/wa4x7F7tBqmqLMLPucOZbEaLQ=,tag:jVEsKh7yi9dzXtN58VXKcQ==,type:str] + encryption: + primary_key: ENC[AES256_GCM,data:MJqoZmeQzB7o6XNS5/KK5GZrJp70bXe8m8M/aSVs0RFHn/Mb4Bzk0yfdATU=,iv:7bszcxnK0V40Fm2krbuYe4bpuKzJDwsn5D68HWpzGGk=,tag:n4Ij8DQli4JfyMdxaYGiXA==,type:str] + deterministic_key: ENC[AES256_GCM,data:fREH+pLeWPVzycTHp9raSu5EqNnO/zjBmUCe8Df2/2hjDrf2f1OjQpI2w7Y=,iv:4v/Cwh+lqRLtpO6ymZ0jLUhJIOwukS+Ng4pK3qeZh8A=,tag:ylAhb0GvVIx36dvJTYdjNg==,type:str] + salt: ENC[AES256_GCM,data:NC4iHNcL+M5tl1W4dKqz9TlvYKY4TN83YCxJbbMDJrvBurnJJtFDqRsmK28=,iv:zIiPcgDCY6stDhZsQRhhJstmh+/Z9oSpVRRfuLBxVSE=,tag:2FajPSyZiFRPUWGj/RpvEQ==,type:str] + vapid: + private_key: ENC[AES256_GCM,data:t4weu5P492pFSpTlHGeEaHhssjReSx2jgE2CdlYcHqgembiv30CpYyVj71c=,iv:V5rLMV4dyjfCgIpcZAuijPDuPTEaRyR5kqR1Pico17Y=,tag:cvfQbl8sJynuw8gbZ0/9Fg==,type:str] + public_key: ENC[AES256_GCM,data:DOzTSCIJkwGoFk2lymMwTeZpnV8RIX1xbFff3SdHu6/Rvtyj6GxVPlRo2IADXUBB5vA5BlORxSsQCmkzjPI/6Rq/WKtHEgLi2ZWtvt83j6LHkwqISuNZ/A==,iv:jgCSaAmsoDxtb5Voxx1b5oRyKH2Yb0oje4Mg14lT+is=,tag:YImmQEuzVxfcWe3L3u8KCg==,type:str] + oidc: + secret: ENC[AES256_GCM,data:dAnoQbB0YyS3Uj4AcM9h0RkGjseFWfiGllYGGUnjc8LXsruTqIPjYuM6NR0=,iv:l4fy6uD4NosdKq/JqrQbIIzjB6o9rkyH5OLL50uRUjY=,tag:AXohLv42vQo6Kmo4tDAbhQ==,type:str] + hash: ENC[AES256_GCM,data:1iISp93pXT21Cn+7+aiZDdXs7jb562IjLS926xseKfTZszFeqlXktal1RiPllTvwoVzB7PC4WcdiJHma8yJock8oXMEQa5/IAm+Wr3sfFkOEBiQtNieJXdWdBffqtrg4dxYz6Silc+NB4Sifs2SaSrUJjn5FuItl/bNW9jk8Tzq3Yd8=,iv:oxqHOowZIPJDtIscOLByyY1OpQPiZAEm+4iRwwFUirI=,tag:R4w+qlwUDiWnKeEZwF3ixg==,type:str] +#ENC[AES256_GCM,data:9YYxY6yRWdykN6mhtAtD2XE5HDwMvg9YZzcvP0lzq6ts8zX6usOg1SVgIKG+lezv5fgFgKB3xP7uNqbQRIdvFY1YgSW/ZcNlkcKQ,iv:Qd+p0c7qRw2hYR+i2sCjuY1zd1XW52m1VoglO7VlIzM=,tag:bY4ZXqnVHrfnDcGd0E0xyA==,type:comment] +#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: admin: password: ENC[AES256_GCM,data:cwBJRIO3eUgezyN60dU=,iv:Ab+EpURW+MYT7RTcPHi4FvQiv4m6TDhAb1/xexkIHCE=,tag:JEWZ3cqoAfAPfwXz7KmINQ==,type:str] #ENC[AES256_GCM,data:ODXFUxxxdQ==,iv:s9zJVx6wo6x517tbNvC+FZ0dFzqbjqeLI6rXBq72hQA=,tag:bXoV2I3LbpmQyddJrtS3Qg==,type:comment] -# -# -#ENC[AES256_GCM,data:UfrFHblh4F20kzKZ,iv:+gouH3z5BXLnALq1Fcs0H6SQ8Gnz6noeeRhltWSgzJQ=,tag:JXqIQjRZGtnx4y+/83CFFA==,type:comment] -dsm: - key: ENC[AES256_GCM,data:jxQRPNuioZbznadJa7sdhbntABsi+nc6Y5TJsyMXwKRvVUbQw8p3snp75d9WVmOfq4uj8wBGen0noa/6xtWo4ZiHsIg1Svh/5xAN37zueB6NmK/z+BW2y8HaIkNhjcinusnT2SWE2ahguizMNM90FpXgmTt9Zv7wVwRkifDLWpH+rs5zmyMWPtpT2RN3+k7tVh8XWVevek/DxG4h+jtoYhTVzndMC5oe/A60MHBJCj6kaksGlX3vIDbRo5A0FEKX8uWI6k6U2osFc4SHIvpljYmPagj3EwPaTc3mbl8IJsXFVH4=,iv:7O5oQa1fELjIt6f3nQ+fWDFirAexgioS36/Nozh656o=,tag:t1RHmY8QC6mE9PVPJg426Q==,type:str] - crt: ENC[AES256_GCM,data:N32+8CItiwbW8drpnL65weS35SY8uP8vcw9wBDFOxGFQjs6EyEwsMUVh1ndMpmDSUKWz7c7ji0HisC710RqnZw9l2NMg6R57DeXR/WsLW4LiAT7sFuDBcqorTB+6Nk0K9wLLdnsKpUQgl8oV9ffnWXZdfI5skKCDMNB40WJAYY/3wx9iT3wDQWaunq/1NT0SZHrtXXTj1JYiLbfDaTwTvkuqqUv1cXvOjonL2Cp4aNT/zjwK6c/TRbKdhIs/7tnUUE/lJx74eARfrMQy/3y6h2xK9pgruASuJxMw9Tyaj6Ya0FMNMIezO3GPQbFCE/IRfmCE6vX2RXUjVqX8wd/d4m4KNuMsN3hbwC3vQ6r+DcYdaTUfGIu96xfKSiSax/ZPQ4JhTyqthkGgpE0Ob67aVELrZkyhc/kvvSxafVSvP63C/nHPQnXMgitdRddYFq7487Vbq03frxiyID1WkqeUavc+Tn3dyA8c7aqVvbW6vZCAwUzbKD0nwPuBUtKZQpJKxHvtx2390PW7a214zkPLcw5LH3FnNacopiNfs6dNEeW/UsGysYkoBRXfrAPMX4UL53TvUTrpuCBhthJ4rgqjhgTfq9GtomP7piuf/tnMAO33qSHPM3w2ocLWeLyOiqYESFmXoBAsiu33LVzuVryFVw75pgrGAsc2wwKhcu4JC0cNgUE44ENIIjffVQX/mcvPvHxuI05vtzM0N8kOFPwG7iAVrO1eUEpwUjZwUwkk8QRsuIhZ6W7aYx7TLdPDpjmy6XCYGNHZgHg6jU7v18KkPep1YarAnsVYFzHQwfZIMOzW7DHlHq38NJW8lWq7zkg+kY6JDVy0nttlAUSskWNeCHdI5Xy3Rz3t7V6t1+UxkSy3roQCZOYjiaknwrXSctsgqucKaJCq4w0Tty36kaIXwOQYVm9JqDIztOmer4fW8Es7OEbLwA==,iv:26fGb288JEbtA0UcAmiYcN+UunrUOw2S6fKjGMetgwE=,tag:GmUJb9LJ186HNOF4AhfmLg==,type:str] - admin: - id: ENC[AES256_GCM,data:/Js=,iv:GyEcag6KxEHy6xApfzEo+dZF9NOtkOoYaJZEzPa8ing=,tag:W/4zaDxdw42Ap4DVE9ogAQ==,type:str] - password: ENC[AES256_GCM,data:/rgn1Eogw3F2EqArMJXL8Ri4y1W04hGzxy4w1Ct5/ysBjjHGgRbxd9tXSf0=,iv:PISJ0a113rlEL0z0yo+nBYNQNU6Ht0PgFWZzooxIgi0=,tag:UZl0EiVcaVPq1v0MtNCXFQ==,type:str] - oidc: - secret: ENC[AES256_GCM,data:IXJNy9PmMPn9uowSMMrNPYqaW5PZ13JTeu1t7eNlapxmnV3EAl3I3xbMK5A=,iv:UcG2E6zgoKkPfUiHr0A+gvnApCkBJG2GROfacwD634Q=,tag:nSFn9yaCadJGlGy/aCvc2A==,type:str] - hash: ENC[AES256_GCM,data:Y2/FNYbID0trRS5UpP2MX6UcLWxlRoXu9VLeU9DU9whEwNLbf4gYrxI7Q7etQe6HidBuoIJNxKajf3t0fI0ptbXnJaxzMh/v/CsiDg2p568o3KIyfB1PruGRKUrTuJneREUHg5jRXMYCwcRUQ4C5WgAyjd2spGlTQO0OLEqh0wkTtvs=,iv:YTYoaBy3QwlSrmx/K5woiXAHrh0RkawF+uQ4rQuP7WU=,tag:Ej6ZjTFvKjw6BrXbd0lf0g==,type:str] -#ENC[AES256_GCM,data:ODXFUxxxdQ==,iv:s9zJVx6wo6x517tbNvC+FZ0dFzqbjqeLI6rXBq72hQA=,tag:bXoV2I3LbpmQyddJrtS3Qg==,type:comment] -# -# sops: age: - enc: | @@ -345,7 +348,7 @@ sops: k+uyWPPi+enFkVaceSwMFrA66uaWWrwAj11sXEB7yzvGFPrnAGezjQ== -----END AGE ENCRYPTED FILE----- recipient: age120wuwcmsm845ztsvsz46pswj5je53uc2n35vadklrfqudu6cxuusxetk7y - lastmodified: "2026-06-25T05:44:30Z" - mac: ENC[AES256_GCM,data:eVRWoJ48BjmflUJoXy3hJpf48H4+7zum47xBHZi674UFH4wSWa/01ctq94fXLNeGpovbaPnh2997c33lm26JVXNfSUPzkjALzf5e7pzMLoxnMbpHrAqyctfcx8PxLXWW1DBNOym0Bi0CM8F4BDsuroQPDDtVGzZk2mouzl0iJqo=,iv:ZRqcYFRxae2cnhwAEbHlq66ydV7PbvSdDlu+Nl2dYlw=,tag:bIAhfpzaJhO+STMC34Qxnw==,type:str] + lastmodified: "2026-07-05T12:35:46Z" + mac: ENC[AES256_GCM,data:fTpqEj1RulJgsW1ueIvSaZIabHGjG/bCk7KutUW+mYXjL2OUskFejnWIbw/vCVaqv/eQsDHRQWL7SSt5SVFOUwv9v6vqXdj/ZIEMIcF3hE3T65vt2Rr9kuB5kjaOwWNzKDUY32Gb1v4MmP2aEG4D7Lj0dVVrej85T6Nizpg0uc8=,iv:8+jqnFxmLSTi247Tu1HXzXUPWqnty0QVn1mSSLk4z9E=,tag:vct9Gq7kMwo+TwghBWKhkA==,type:str] unencrypted_suffix: _unencrypted - version: 3.13.1 + version: 3.13.2 diff --git a/config/services/containers/app/collabora/collabora.container.j2 b/config/services/containers/app/collabora/collabora.container.j2 index 187117c..8ce2bf8 100644 --- a/config/services/containers/app/collabora/collabora.container.j2 +++ b/config/services/containers/app/collabora/collabora.container.j2 @@ -22,4 +22,4 @@ RestartSec=10s TimeoutStopSec=120 [Install] -WantedBy=default.target \ No newline at end of file +WantedBy=default.target diff --git a/config/services/containers/app/mastodon/mastodon-streaming.container.j2 b/config/services/containers/app/mastodon/mastodon-streaming.container.j2 new file mode 100644 index 0000000..020c6e8 --- /dev/null +++ b/config/services/containers/app/mastodon/mastodon-streaming.container.j2 @@ -0,0 +1,46 @@ +[Quadlet] +DefaultDependencies=false + +[Unit] +Description=Mastodon Streaming + +After=network-online.target redis_mastodon.service +Wants=network-online.target redis_mastodon.service + +[Container] +Image=ghcr.io/mastodon/mastodon-streaming:{{ version['containers']['mastodon'] }} +ContainerName=mastodon-streaming +HostName=mastodon-streaming + +PublishPort={{ services['mastodon']['ports']['streaming'] }}:4000/tcp + +Volume=%h/containers/mastodon/ssl:/etc/ssl/mastodon:ro + +# General +Environment="TZ={{ timezone }}" +Environment="LOCAL_DOMAIN={{ services['mastodon']['domain']['public'] }}.{{ domain['public'] }}" +Environment="LOG_LEVEL=info" + +# PostgreSQL +Environment="DB_HOST={{ services['postgresql']['domain']['internal'] }}.{{ domain['internal'] }}" +Environment="DB_PORT={{ services['postgresql']['ports']['tcp'] }}" +Environment="DB_NAME=mastodon_db" +Environment="DB_USER=mastodon" +Environment="DB_SSLMODE=verify-full" +Environment="NODE_EXTRA_CA_CERTS=/etc/ssl/mastodon/{{ root_cert_filename }}" +Secret=MASTODON_POSTGRES_PASSWORD,type=env,target=DB_PASS + +# Redis +Environment="REDIS_URL=redis://host.containers.internal:{{ services['mastodon']['ports']['redis'] }}" + +# Process +Exec=node ./streaming/index.js + +[Service] +ExecStartPre=/usr/bin/nc -zv {{ services['postgresql']['domain']['internal'] }}.{{ domain['internal'] }} {{ services['postgresql']['ports']['tcp'] }} +Restart=always +RestartSec=10s +TimeoutStopSec=120 + +[Install] +WantedBy=default.target diff --git a/config/services/containers/app/mastodon/mastodon-web.container.j2 b/config/services/containers/app/mastodon/mastodon-web.container.j2 new file mode 100644 index 0000000..863c651 --- /dev/null +++ b/config/services/containers/app/mastodon/mastodon-web.container.j2 @@ -0,0 +1,74 @@ +[Quadlet] +DefaultDependencies=false + +[Unit] +Description=Mastodon Web + +After=network-online.target redis_mastodon.service +Wants=network-online.target redis_mastodon.service + +[Container] +Image=ghcr.io/mastodon/mastodon:{{ version['containers']['mastodon'] }} +ContainerName=mastodon-web +HostName=mastodon-web + +PublishPort={{ services['mastodon']['ports']['http'] }}:3000/tcp + +Volume=%h/data/containers/mastodon/system:/mastodon/public/system:rw +Volume=%h/containers/mastodon/ssl:/etc/ssl/mastodon:ro + + +# General +Environment="TZ={{ timezone }}" +Environment="LOCAL_DOMAIN={{ services['mastodon']['domain']['public'] }}.{{ domain['public'] }}" +Environment="LIMITED_FEDERATION_MODE=true" +Environment="DISALLOW_UNAUTHENTICATED_API_ACCESS=true" +Environment="ES_ENABLED=false" +Environment="RAILS_LOG_LEVEL=info" + +# PostgreSQL +Environment="DB_HOST={{ services['postgresql']['domain']['internal'] }}.{{ domain['internal'] }}" +Environment="DB_PORT={{ services['postgresql']['ports']['tcp'] }}" +Environment="DB_NAME=mastodon_db" +Environment="DB_USER=mastodon" +Environment="DB_SSLMODE=verify-full" +Environment="PGSSLROOTCERT=/etc/ssl/mastodon/{{ root_cert_filename }}" +Secret=MASTODON_POSTGRES_PASSWORD,type=env,target=DB_PASS + +# Redis +Environment="REDIS_URL=redis://host.containers.internal:{{ services['mastodon']['ports']['redis'] }}" + +# Rails secrets +Secret=MASTODON_SECRET_KEY_BASE,type=env,target=SECRET_KEY_BASE +Secret=MASTODON_ENCRYPTION_PRIMARY_KEY,type=env,target=ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY +Secret=MASTODON_ENCRYPTION_DETERMINISTIC_KEY,type=env,target=ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY +Secret=MASTODON_ENCRYPTION_SALT,type=env,target=ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT +Secret=MASTODON_VAPID_PRIVATE_KEY,type=env,target=VAPID_PRIVATE_KEY +Secret=MASTODON_VAPID_PUBLIC_KEY,type=env,target=VAPID_PUBLIC_KEY + +# OIDC - Authelia +Environment="OIDC_ENABLED=true" +Environment="OIDC_DISPLAY_NAME=Sign in with Authelia" +Environment="OIDC_ISSUER=https://{{ services['authelia']['domain']['public'] }}.{{ domain['public'] }}" +Environment="OIDC_DISCOVERY=true" +Environment="OIDC_SCOPE=openid,groups,profile,email" +Environment="OIDC_UID_FIELD=preferred_username" +Environment="OIDC_CLIENT_ID=mastodon" +Environment="OIDC_REDIRECT_URI=https://{{ services['mastodon']['domain']['public'] }}.{{ domain['public'] }}/auth/auth/openid_connect/callback" +Environment="OIDC_SECURITY_ASSUME_EMAIL_IS_VERIFIED=false" +Environment="OMNIAUTH_ONLY=true" +Environment="ONE_CLICK_SSO_LOGIN=true" +Environment="OIDC_USE_PKCE=true" +Secret=MASTODON_OIDC_CLIENT_SECRET,type=env,target=OIDC_CLIENT_SECRET + +# Process +Exec=bundle exec puma -C config/puma.rb + +[Service] +ExecStartPre=/usr/bin/nc -zv {{ services['postgresql']['domain']['internal'] }}.{{ domain['internal'] }} {{ services['postgresql']['ports']['tcp'] }} +Restart=always +RestartSec=10s +TimeoutStopSec=120 + +[Install] +WantedBy=default.target diff --git a/config/services/containers/app/mastodon/mastodon-worker.container.j2 b/config/services/containers/app/mastodon/mastodon-worker.container.j2 new file mode 100644 index 0000000..8d41618 --- /dev/null +++ b/config/services/containers/app/mastodon/mastodon-worker.container.j2 @@ -0,0 +1,57 @@ +[Quadlet] +DefaultDependencies=false + +[Unit] +Description=Mastodon worker + +After=network-online.target redis_mastodon.service +Wants=network-online.target redis_mastodon.service + +[Container] +Image=ghcr.io/mastodon/mastodon:{{ version['containers']['mastodon'] }} +ContainerName=mastodon-worker +HostName=mastodon-worker + +Volume=%h/data/containers/mastodon/system:/mastodon/public/system:rw +Volume=%h/containers/mastodon/ssl:/etc/ssl/mastodon:ro + + +# General +Environment="TZ={{ timezone }}" +Environment="LOCAL_DOMAIN={{ services['mastodon']['domain']['public'] }}.{{ domain['public'] }}" +Environment="LIMITED_FEDERATION_MODE=true" +Environment="DISALLOW_UNAUTHENTICATED_API_ACCESS=true" +Environment="ES_ENABLED=false" +Environment="RAILS_LOG_LEVEL=info" + +# PostgreSQL +Environment="DB_HOST={{ services['postgresql']['domain']['internal'] }}.{{ domain['internal'] }}" +Environment="DB_PORT={{ services['postgresql']['ports']['tcp'] }}" +Environment="DB_NAME=mastodon_db" +Environment="DB_USER=mastodon" +Environment="DB_SSLMODE=verify-full" +Environment="PGSSLROOTCERT=/etc/ssl/mastodon/{{ root_cert_filename }}" +Secret=MASTODON_POSTGRES_PASSWORD,type=env,target=DB_PASS + +# Redis +Environment="REDIS_URL=redis://host.containers.internal:{{ services['mastodon']['ports']['redis'] }}" + +# Rails secrets +Secret=MASTODON_SECRET_KEY_BASE,type=env,target=SECRET_KEY_BASE +Secret=MASTODON_ENCRYPTION_PRIMARY_KEY,type=env,target=ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY +Secret=MASTODON_ENCRYPTION_DETERMINISTIC_KEY,type=env,target=ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY +Secret=MASTODON_ENCRYPTION_SALT,type=env,target=ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT +Secret=MASTODON_VAPID_PRIVATE_KEY,type=env,target=VAPID_PRIVATE_KEY +Secret=MASTODON_VAPID_PUBLIC_KEY,type=env,target=VAPID_PUBLIC_KEY + +# Process +Exec=bundle exec sidekiq + +[Service] +ExecStartPre=/usr/bin/nc -zv {{ services['postgresql']['domain']['internal'] }}.{{ domain['internal'] }} {{ services['postgresql']['ports']['tcp'] }} +Restart=always +RestartSec=10s +TimeoutStopSec=120 + +[Install] +WantedBy=default.target diff --git a/config/services/containers/app/sure/sure-web.container.j2 b/config/services/containers/app/sure/sure-web.container.j2 index f0a1f1c..7630d6b 100644 --- a/config/services/containers/app/sure/sure-web.container.j2 +++ b/config/services/containers/app/sure/sure-web.container.j2 @@ -42,11 +42,11 @@ Environment="REDIS_URL=redis://host.containers.internal:{{ services['sure']['por Environment="OIDC_CLIENT_ID=sure" Environment="OIDC_ISSUER=https://{{ services['authelia']['domain']['public'] }}.{{ domain['public'] }}" Environment="OIDC_REDIRECT_URI=https://{{ services['sure']['domain']['public'] }}.{{ domain['public'] }}/auth/openid_connect/callback" -Secret=SURE_OIDC_CLIENT_SECRET,type=env,target=OIDC_CLIENT_SECRET Environment="OIDC_BUTTON_LABEL=Sign in with Authelia" Environment="AUTH_JIT_MODE=create_and_link" # email's domain, e.g. ilnmors.internal then only user@ilnmors.internal is allowed to sign-up Environment="ALLOWED_OIDC_DOMAINS=" +Secret=SURE_OIDC_CLIENT_SECRET,type=env,target=OIDC_CLIENT_SECRET # WebAuthn / Passkey Environment="WEBAUTHN_RP_ID={{ domain['public'] }}" diff --git a/config/services/containers/app/sure/sure-worker.container.j2 b/config/services/containers/app/sure/sure-worker.container.j2 index 0d6f958..b0619ec 100644 --- a/config/services/containers/app/sure/sure-worker.container.j2 +++ b/config/services/containers/app/sure/sure-worker.container.j2 @@ -15,14 +15,9 @@ HostName=sure-worker Volume=%h/data/containers/sure/storage:/rails/storage:rw Volume=%h/containers/sure/ssl:/etc/ssl/sure:ro -Exec=bundle exec sidekiq - # General Environment="TZ={{ timezone }}" Environment="SELF_HOSTED=true" -Environment="ONBOARDING_STATE=closed" -Environment="RAILS_FORCE_SSL=false" -Environment="RAILS_ASSUME_SSL=true" Environment="APP_DOMAIN={{ services['sure']['domain']['public'] }}.{{ domain['public'] }}" Secret=SURE_SECRET_KEY_BASE,type=env,target=SECRET_KEY_BASE @@ -38,25 +33,13 @@ Secret=SURE_POSTGRES_PASSWORD,type=env,target=POSTGRES_PASSWORD # Redis Environment="REDIS_URL=redis://host.containers.internal:{{ services['sure']['ports']['redis'] }}/1" -# OIDC - Authelia -Environment="OIDC_CLIENT_ID=sure" -Environment="OIDC_ISSUER=https://{{ services['authelia']['domain']['public'] }}.{{ domain['public'] }}" -Environment="OIDC_REDIRECT_URI=https://{{ services['sure']['domain']['public'] }}.{{ domain['public'] }}/auth/openid_connect/callback" -Secret=SURE_OIDC_CLIENT_SECRET,type=env,target=OIDC_CLIENT_SECRET -Environment="OIDC_BUTTON_LABEL=Sign in with Authelia" -Environment="AUTH_JIT_MODE=create_and_link" -# email's domain, e.g. ilnmors.internal then only user@ilnmors.internal is allowed to sign-up -Environment="ALLOWED_OIDC_DOMAINS=" - -# WebAuthn / Passkey -Environment="WEBAUTHN_RP_ID={{ domain['public'] }}" -Environment="WEBAUTHN_ALLOWED_ORIGINS=https://{{ services['sure']['domain']['public'] }}.{{ domain['public'] }}" - # Provider ## Currency Environment="EXCHANGE_RATE_PROVIDER=yahoo_finance" Environment="SECURITIES_PROVIDER=yahoo_finance" +Exec=bundle exec sidekiq + [Service] ExecStartPre=/usr/bin/nc -zv {{ services['postgresql']['domain']['internal'] }}.{{ domain['internal'] }} {{ services['postgresql']['ports']['tcp'] }} Restart=always diff --git a/config/services/containers/auth/authelia/config/authelia.yaml.j2 b/config/services/containers/auth/authelia/config/authelia.yaml.j2 index d88f9b4..553a1d5 100644 --- a/config/services/containers/auth/authelia/config/authelia.yaml.j2 +++ b/config/services/containers/auth/authelia/config/authelia.yaml.j2 @@ -324,3 +324,24 @@ identity_providers: grant_types: - 'authorization_code' token_endpoint_auth_method: 'client_secret_basic' + # mastodon + - client_id: 'mastodon' + client_name: 'mastodon' + consent_mode: 'implicit' + client_secret: '{{ hostvars['console']['mastodon']['oidc']['hash'] }}' + public: false + authorization_policy: 'one_factor' + require_pkce: true + pkce_challenge_method: 'S256' + redirect_uris: + - 'https://{{ services['mastodon']['domain']['public'] }}.{{ domain['public'] }}/auth/auth/openid_connect/callback' + scopes: + - 'openid' + - 'groups' + - 'email' + - 'profile' + response_types: + - 'code' + grant_types: + - 'authorization_code' + token_endpoint_auth_method: 'client_secret_basic' diff --git a/config/services/containers/common/caddy/etc/Caddyfile.j2 b/config/services/containers/common/caddy/etc/Caddyfile.j2 index 06c0a12..02f74f7 100644 --- a/config/services/containers/common/caddy/etc/Caddyfile.j2 +++ b/config/services/containers/common/caddy/etc/Caddyfile.j2 @@ -84,6 +84,12 @@ {% elif node['name'] == 'app' %} {{ service['domain']['internal'] }}.{{ domain['internal'] }} { import private_tls +{% if service['ports']['streaming'] is defined %} + @streaming path /api/v1/streaming* + reverse_proxy @streaming host.containers.internal:{{ service['ports']['streaming'] }} { + header_up Host {http.request.header.X-Forwarded-Host} + } +{% endif %} reverse_proxy host.containers.internal:{{ service['ports']['http'] }} { header_up Host {http.request.header.X-Forwarded-Host} } diff --git a/config/services/containers/infra/prometheus/etc/rules.yaml.j2 b/config/services/containers/infra/prometheus/etc/rules.yaml.j2 index 992edee..5297b17 100644 --- a/config/services/containers/infra/prometheus/etc/rules.yaml.j2 +++ b/config/services/containers/infra/prometheus/etc/rules.yaml.j2 @@ -28,7 +28,7 @@ groups: {% for filename in ['root', 'intermediate'] + (services | dict2items | selectattr('value.dependency.cert', 'defined') | selectattr('value.dependency.cert', 'equalto', true) | map(attribute='key') | list) %} - alert: {{ filename | replace('.', '_') }}_is_expired_soon expr: | - max(x509_cert_not_after{filename="{{ filename }}.crt"}) - time() < 2592000 + min(x509_cert_not_after{filename="{{ filename }}.crt"}) - time() < 2592000 for: 1d labels: severity: critical @@ -58,6 +58,20 @@ groups: summary: "No SMART devices discovered on vmm" description: "smartctl-generator is running, but it reports fewer than one SMART device. Check USB filtering, /sys/class/block discovery, and smartctl output." +{% set devices_count = + (hostvars['vmm']['hardware']['disk']['data']['serials'] | default([]) | length) + + (hostvars['vmm']['hardware']['disk']['backup']['serials'] | default([]) | length) + + (hostvars['vmm']['hardware']['disk']['nvme']['serials'] | default([]) | length) %} + - alert: SmartctlDeviceCountLow + expr: | + (max_over_time(smartctl_devices{job="smartctl", instance="vmm"}[5m]) or on() vector(0)) < {{ devices_count }} + for: 10m + labels: + severity: critical + annotations: + summary: "SMART device count is low on vmm" + description: "Expected {{ devices_count }} SMART devices on vmm, but fewer than {{ devices_count }} are currently reported. Check missing HDD/NVMe devices, smartctl output, and device discovery." + - alert: SmartctlDeviceHealthFailed expr: | ( diff --git a/config/services/systemd/common/alloy/config.alloy.j2 b/config/services/systemd/common/alloy/config.alloy.j2 index eb13f8b..c9ab450 100644 --- a/config/services/systemd/common/alloy/config.alloy.j2 +++ b/config/services/systemd/common/alloy/config.alloy.j2 @@ -338,7 +338,7 @@ loki.process "journal_parser" { } } stage.match { - selector = "{level!=\"fatal\", job!~\"loki|prometheus|grafana\"} |~ \"(?i)(fatal|critical|panic|uncaught|unhandled|assertion failed|out of memory|\\bboom\\b|killed process)\"" + selector = "{level!=\"fatal\", job!~\"loki|prometheus|grafana\"} |~ \"(?i)(panic|assertion failed|out of memory|segfault|killed process)\"" stage.static_labels { values = { level = "fatal", @@ -346,7 +346,7 @@ loki.process "journal_parser" { } } stage.match { - selector = "{level=~\"info|debug\", job!~\"loki|prometheus|grafana\"} |~ \"(?i)(\\berror\\b|\\berr\\b|\\bfailed\\b|\\bfailure\\b|\\bexception\\b|traceback|segfault|exit-code|container died|permission denied|access denied|connection refused|connection reset|timed out|timeout exceeded|no such file|authentication failed|sqlstate|duplicate key|relation .* does not exist|column .* does not exist|database .* does not exist|relation .* already exists|column .* already exists|x509|unknown authority|certificate .*expired|tls handshake|bad gateway|upstream .*failed|upstream .*error)\"" + selector = "{level=~\"info|debug\", job!~\"loki|prometheus|grafana\"} |~ \"(?i)(\\berror\\b|\\berr\\b|\\bfailed\\b|\\bfailure\\b|\\bexception\\b|traceback|exit-code|container died|permission denied|access denied|connection refused|connection reset|timed out|timeout exceeded|no such file|authentication failed|sqlstate|duplicate key|relation .* does not exist|column .* does not exist|database .* does not exist|relation .* already exists|column .* already exists|x509|unknown authority|certificate .*expired|tls handshake|bad gateway|upstream .*failed|upstream .*error)\"" stage.static_labels { values = { level = "error", diff --git a/docs/archives/services/app/bonfire/bonfire.container.j2 b/docs/archives/services/app/bonfire/bonfire.container.j2 new file mode 100644 index 0000000..5243959 --- /dev/null +++ b/docs/archives/services/app/bonfire/bonfire.container.j2 @@ -0,0 +1,59 @@ +[Quadlet] +DefaultDependencies=false + +[Unit] +Description=Bonfire + +[Container] +Image=docker.io/bonfirenetworks/bonfire:{{ version['containers']['bonfire'] }} + +ContainerName=bonfire +HostName=bonfire + +PublishPort={{ services['bonfire']['ports']['http'] }}:4000 + +# Volumes +Volume=%h/data/containers/bonfire/uploads:/opt/app/data/uploads:rw +Volume=%h/containers/bonfire/ssl:/etc/ssl/bonfire:ro +Volume=%h/containers/bonfire/config/runtime.exs:/opt/app/releases/{{ version['containers']['bonfire'] | replace('-amd64', '') }}/runtime.exs:ro + +# Environment +Environment="TZ={{ timezone }}" +Environment="MIX_ENV=prod" +Environment="FLAVOUR=social" +Environment="WITH_DOCKER=true" +Environment="HOSTNAME={{ services['bonfire']['domain']['public'] }}.{{ domain['public'] }}" +Environment="PUBLIC_PORT=443" +Environment="SERVER_PORT={{ services['bonfire']['ports']['http'] }}" + +# Secrets +Secret=BONFIRE_SESSION_SECRET,type=env,target=SECRET_KEY_BASE +Secret=BONFIRE_SESSION_SALT,type=env,target=SIGNING_SALT +Secret=BONFIRE_ENCRYPT_SALT,type=env,target=ENCRYPTION_SALT +Secret=BONFIRE_RELEASE_COOKIE,type=env,target=RELEASE_COOKIE + +# Database +Environment="POSTGRES_HOST={{ services['postgresql']['domain']['internal'] }}.{{ domain['internal'] }}" +Environment="POSTGRES_PORT={{ services['postgresql']['ports']['tcp'] }}" +Environment="POSTGRES_USER=bonfire" +Environment="POSTGRES_DB=bonfire_db" +Environment="DB_SSL=true" +Environment="DB_CA_CERT_FILE=/etc/ssl/bonfire/{{ root_cert_filename }}" +Secret=BONFIRE_DB_PASSWORD,type=env,target=POSTGRES_PASSWORD + +# OIDC +Environment="OPENID_1_DISCOVERY=https://{{ services['authelia']['domain']['public'] }}.{{ domain['public'] }}/.well-known/openid-configuration" +Environment="OPENID_1_CLIENT_ID=bonfire" +Environment="OPENID_1_DISPLAY_NAME=Authelia" +Environment="OPENID_1_SCOPE=openid email profile groups" +Environment="OPENID_1_ENABLE_SIGNUP=true" +Secret=BONFIRE_OIDC_CLIENT_SECRET,type=env,target=OPENID_1_CLIENT_SECRET + +[Service] +ExecStartPre=/usr/bin/nc -zv {{ services['postgresql']['domain']['internal'] }}.{{ domain['internal'] }} {{ services['postgresql']['ports']['tcp'] }} +Restart=always +RestartSec=10s +TimeoutStopSec=120 + +[Install] +WantedBy=default.target diff --git a/docs/archives/services/app/bonfire/group_vars.yaml b/docs/archives/services/app/bonfire/group_vars.yaml new file mode 100644 index 0000000..5228587 --- /dev/null +++ b/docs/archives/services/app/bonfire/group_vars.yaml @@ -0,0 +1,14 @@ +--- +services: + bonfire: + domain: + public: "" + internal: "" + ports: + http: "" + dependency: + web: "" + db: "" + +version: + bonfire: "1.0.5-social-amd64" diff --git a/docs/archives/services/app/bonfire/runtime.exs b/docs/archives/services/app/bonfire/runtime.exs new file mode 100644 index 0000000..d33683f --- /dev/null +++ b/docs/archives/services/app/bonfire/runtime.exs @@ -0,0 +1,562 @@ +# In this file, we load production configuration and secrets +# from environment variables. You can also hardcode secrets, +# although such is generally not recommended and you have to +# remember to add this file to your .gitignore. +import Config + +# Allow any secret to be provided via a file (`_FILE`, e.g. Docker secrets / systemd +# LoadCredential) instead of a plain env var. See bonfire-app#1663. +alias Bonfire.Common.EnvSecrets + +IO.puts("Preparing runtime config...") + +System.get_env("MIX_QUIET") || IO.puts("🔥 Welcome to Bonfire!") + +yes? = ~w(true yes 1) +no? = ~w(false no none 0) + +# flavour = System.get_env("FLAVOUR", "classic") +host = System.get_env("HOSTNAME", "localhost") +server_port = String.to_integer(System.get_env("SERVER_PORT", "4000")) +public_port = String.to_integer(System.get_env("PUBLIC_PORT", "4000")) +test_instance? = System.get_env("TEST_INSTANCE") in yes? +federate? = test_instance? or System.get_env("FEDERATE") in yes? + +hosts = + "#{host},#{System.get_env("EXTRA_DOMAINS")}" + |> String.replace(["`", " "], "") + |> String.split(",") + |> Enum.map(&"//#{&1}") + +EnvSecrets.env_or_file("DATABASE_URL") || EnvSecrets.env_or_file("CLOUDRON_POSTGRESQL_URL") || + EnvSecrets.env_or_file("POSTGRES_PASSWORD") || + EnvSecrets.env_or_file("CLOUDRON_POSTGRESQL_PASSWORD") || + System.get_env("MIX_QUIET") || System.get_env("CI") || + raise """ + Environment variables for database are missing. + For example: DATABASE_URL=ecto://USER:PASS@HOST/DATABASE + You can also set POSTGRES_PASSWORD (required), + and POSTGRES_USER (default: postgres) and POSTGRES_HOST (default: localhost) + """ + +## load extensions' runtime configs (and behaviours) directly via extension-provided modules +Bonfire.Common.Config.LoadExtensionsConfig.load_configs([Bonfire.RuntimeConfig]) +## + +secret_key_base = + EnvSecrets.env_or_file("SECRET_KEY_BASE") || System.get_env("MIX_QUIET") || + System.get_env("CI") || + raise """ + environment variable SECRET_KEY_BASE is missing. + You can generate one by calling: mix phx.gen.secret + """ + +signing_salt = + EnvSecrets.env_or_file("SIGNING_SALT") || System.get_env("MIX_QUIET") || System.get_env("CI") || + raise """ + environment variable SIGNING_SALT is missing. + """ + +encryption_salt = + EnvSecrets.env_or_file("ENCRYPTION_SALT") || System.get_env("MIX_QUIET") || + System.get_env("CI") || + raise """ + environment variable ENCRYPTION_SALT is missing. + """ + +cute_gifs_dir = System.get_env("CUTE_GIFS_DIR", "data/uploads/cute-gifs/") + +config :bonfire, + # how many nested replies to show + thread_default_max_depth: String.to_integer(System.get_env("THREAD_DEPTH", "3")), + feed_live_update_many_preload_mode: :async_actions, + host: host, + default_cache_hours: String.to_integer(System.get_env("BONFIRE_CACHE_HOURS", "3")), + app_name: System.get_env("APP_NAME", "Bonfire"), + ap_base_path: System.get_env("AP_BASE_PATH", "/pub"), + # github_app_client_id: System.get_env("GITHUB_APP_CLIENT_ID", "Iv1.8d612e6e5a2149c9"), + github_token: EnvSecrets.env_or_file("GITHUB_TOKEN"), + show_debug_errors_in_dev: System.get_env("SHOW_DEBUG_IN_DEV"), + encryption_salt: encryption_salt, + signing_salt: signing_salt, + root_path: File.cwd!(), + cute_gifs: [ + num: length(Path.wildcard(cute_gifs_dir <> "*.gif")), + dir: cute_gifs_dir + ] + +if System.get_env("ENABLE_STATIC_CACHING") not in yes? do + config :bonfire_ui_common, Bonfire.UI.Common.StaticGenerator, modularity: :disabled +end + +phx_server = System.get_env("PHX_SERVER") +plug_server = System.get_env("PLUG_SERVER") +use_cowboy? = plug_server == "cowboy" +use_socket_file? = plug_server == "cowboy_socket" +socket_file_path = System.get_env("SOCKET_FILE_PATH", "/tmp/bonfire_socket") +phx_compress? = System.get_env("PHX_COMPRESS_HTTP") not in no? + +if System.get_env("DISABLE_LOG") in yes? do + # to suppress non-captured logs in tests (eg. in setup_all) + config :logger, backends: [] +end + +http_options = + cond do + use_socket_file? -> + [ + ip: {:local, socket_file_path}, + port: 0, + compress: phx_compress?, + transport_options: [ + post_listen_callback: fn _ -> + File.touch!(socket_file_path) + File.chmod!(socket_file_path, 0o660) + end, + socket_opts: [] + ] + ] + + use_cowboy? -> + [ + # only bind the app to localhost when serving behind a proxy + # ip: (if public_port != server_port, do: {127, 0, 0, 1}), + port: server_port, + compress: phx_compress?, + transport_options: [ + max_connections: 16_384, + socket_opts: [:inet6] + ] + ] + + true -> + # for bandit + [ + port: server_port, + http_options: [ + compress: phx_compress? + ] + ] + end + +config :bonfire, Bonfire.Web.Endpoint, + server: + phx_server not in no? and + (config_env() != :test or test_instance? or phx_server in yes?), + url: [ + host: host, + port: public_port + ], + check_origin: hosts, + # check_origin: false, + adapter: + if(use_cowboy?, + do: Phoenix.Endpoint.Cowboy2Adapter, + else: Bandit.PhoenixAdapter + ), + http: http_options, + thousand_island: [transport_ports: [hibernate_after: to_timeout(second: 15)]], + secret_key_base: secret_key_base, + live_view: [signing_salt: signing_salt] + +if test_instance? do + test_instance_hostname = System.get_env("TEST_INSTANCE_HOSTNAME", "localhost") + + test_instance_server_port = + String.to_integer(System.get_env("TEST_INSTANCE_SERVER_PORT", "4002")) + + config :bonfire, Bonfire.Web.FakeRemoteEndpoint, + url: [ + host: test_instance_hostname, + port: + if(test_instance_hostname != "localhost", + do: public_port, + else: test_instance_server_port + ) + ], + http: [ + port: test_instance_server_port + ], + secret_key_base: secret_key_base +end + +# HTTP client(s) configuration + +finch_conn_opts = [case_sensitive_headers: true] + +finch_conn_opts = + finch_conn_opts ++ + case System.get_env("HTTP_PROXY_URL") do + nil -> + [] + + uri -> + uri = + uri + |> URI.parse() + + [proxy: {String.to_existing_atom(uri.scheme), uri.host, uri.port, []}] + end + +finch_pools = %{ + :default => [ + # Number of connections to maintain in each pool (HTTP1) + size: String.to_integer(System.get_env("FINCH_POOL_SIZE", "50")), + # Number of pools to start (increasing is only useful for HTTP2, which we don't use by default) + count: String.to_integer(System.get_env("FINCH_POOL_COUNT", "1")), + # 10 min by default + pool_max_idle_time: String.to_integer(System.get_env("FINCH_POOL_MAX_IDLE_TIME", "600000")), + conn_opts: finch_conn_opts + ], + "https://icons.duckduckgo.com" => [ + conn_opts: [ + transport_opts: [size: 8, timeout: to_timeout(second: 3), conn_opts: finch_conn_opts] + ] + ], + "https://www.google.com/s2/favicons" => [ + conn_opts: [ + transport_opts: [size: 8, timeout: to_timeout(second: 3), conn_opts: finch_conn_opts] + ] + ] +} + +# config :tesla, adapter: Tesla.Adapter.Hackney +config :bonfire, :finch_pools, finch_pools + +if config_env() != :test or federate? do + config :tesla, :adapter, {Tesla.Adapter.Finch, name: Bonfire.Finch, pools: finch_pools} +end + +oban_notifier = + case System.get_env("OBAN_NOTIFIER", "postgres") do + "pg" -> Oban.Notifiers.PG + "phoenix" -> Oban.Notifiers.Phoenix + _postgres -> Oban.Notifiers.Postgres + end + +# disable insert notifications? +oban_insert_trigger = System.get_env("OBAN_INSERT_TRIGGER", "true") in ["true", "1", "yes"] + +# with insert_trigger enabled, polling is just a safety net for future-scheduled jobs and missed +# notifications — 30s is sufficient and avoids constant background DB queries +oban_stage_interval = System.get_env("OBAN_STAGE_INTERVAL_MS", "30000") |> String.to_integer() + +# average ms a producer waits before fetching more jobs after a trigger; jitter applies (0-2x actual) +oban_dispatch_cooldown = System.get_env("OBAN_DISPATCH_COOLDOWN_MS", "5") |> String.to_integer() + +# Set OBAN_TESTING=inline to run jobs synchronously on insert (useful for e2e tests — makes federation between servers effectively instant so tests don't need to poll and wait). +# Set to "manual" to disable automatic job execution entirely (useful for unit tests). +oban_testing = case System.get_env("OBAN_TESTING") do + "inline" -> [testing: :inline] + "manual" -> [testing: :manual] + _ -> [] +end + +config :bonfire, Oban, + notifier: oban_notifier, + repo: Bonfire.Common.Repo, + insert_trigger: oban_insert_trigger, + stage_interval: oban_stage_interval, + dispatch_cooldown: oban_dispatch_cooldown, + queues: [ + federator_incoming_mentions: + String.to_integer(System.get_env("QUEUE_SIZE_AP_IN_MENTIONS", "3")), + federator_incoming_follows: + String.to_integer(System.get_env("QUEUE_SIZE_AP_IN_FOLLOWS", "2")), + federator_incoming: String.to_integer(System.get_env("QUEUE_SIZE_AP_IN", "3")), + federator_incoming_unverified: + String.to_integer(System.get_env("QUEUE_SIZE_AP_IN_UNVERIFIED", "1")), + federator_outgoing: String.to_integer(System.get_env("QUEUE_SIZE_AP_OUT", "3")), + remote_fetcher: String.to_integer(System.get_env("QUEUE_SIZE_AP_FETCH", "1")), + import: String.to_integer(System.get_env("QUEUE_SIZE_IMPORT", "1")), + deletion: String.to_integer(System.get_env("QUEUE_SIZE_DELETION", "1")), + database_prune: String.to_integer(System.get_env("QUEUE_SIZE_DB_PRUNE", "1")), + static_generator: String.to_integer(System.get_env("QUEUE_SIZE_STATIC_GEN", "1")), + # video_transcode: 1, + # boost_activities: 1, + fetch_open_science: String.to_integer(System.get_env("QUEUE_SIZE_OPEN_SCIENCE_FETCH", "1")), + ghost_webhooks: String.to_integer(System.get_env("QUEUE_SIZE_GHOST_WEBHOOKS", "2")), + # batched Meilisearch indexing flush (Bonfire.Search.Workers.IndexWorker); + search_index: String.to_integer(System.get_env("QUEUE_SIZE_SEARCH_INDEX", "1")) + ], + plugins: [ + # delete job history after 6 hours + {Oban.Plugins.Pruner, max_age: 60 * 60 * 6}, + # rescue orphaned jobs + {Oban.Plugins.Lifeline, rescue_after: :timer.minutes(60)}, + {Oban.Plugins.Cron, + crontab: + [ + {"@daily", ActivityPub.Pruner.PruneDatabaseWorker, max_attempts: 1} + ] ++ + if Bonfire.Common.Extend.module_enabled?(Bonfire.UI.Common.StaticGenerator) do + #  generate static pages for guests every X min + interval = if config_env() == :prod, do: 10, else: 60 + IO.puts("Static pages will be generated and cached every #{interval} minutes.") + + [{"*/#{interval} * * * *", Bonfire.UI.Common.StaticGenerator, max_attempts: 3}] + else + IO.puts("Static pages will not be cached") + [] + end ++ + if Bonfire.Common.Extend.extension_enabled?(:bonfire_open_science) do + IO.puts("Open science publications will be fetched for all users once an hour.") + + [{"@hourly", Bonfire.OpenScience.APIs}] + else + IO.puts("Open science extension is not enabled") + [] + end} + # ++ + # if Bonfire.Common.Extend.module_enabled?(Bonfire.Social.TrendingLinksCacheWorker) do + # [{"@hourly", Bonfire.Social.TrendingLinksCacheWorker, max_attempts: 1}] + # else + # [] + # end + ] + +config :activity_pub, :oban_queues, + retries: [federator_incoming: 2, federator_outgoing: 3, remote_fetcher: 1] + +config :activity_pub, Oban, + notifier: Oban.Notifiers.PG, + # to avoid running it twice + queues: false, + repo: Bonfire.Common.Repo + +if oban_testing != [] do + config :bonfire, Oban, oban_testing + config :activity_pub, Oban, oban_testing +end + +config :activity_pub, :http, + user_agent: System.get_env("AP_USER_AGENT", "#{System.get_env("APP_NAME") || Bonfire.Application.name() || "Bonfire"} federation") + +config :activity_pub, ActivityPub.Federator.HTTP.RateLimit, + scale_ms: String.to_integer(System.get_env("AP_RATELIMIT_PER_MS", "10000")), + limit: String.to_integer(System.get_env("AP_RATELIMIT_NUM", "20")) + +case System.get_env("GRAPH_DB_URL") do + nil -> + nil + + url -> + pool_size = + case System.get_env("POOL_SIZE") do + pool when is_binary(pool) and pool not in ["", "0"] -> + String.to_integer(pool) + + # default to twice the number of CPU cores + _ -> + System.schedulers_online() * 2 + end + + # config :bolt_sips, Bolt, + # url: url, + # basic_auth: [username: "memgraph", password: "memgraph"], + # pool_size: pool_size + + config :boltx, Bolt, + uri: url, + auth: [username: "memgraph", password: "memgraph"], + pool_size: pool_size +end + +if (config_env() == :prod or System.get_env("OTEL_ENABLED") in yes?) and + (System.get_env("OTEL_EXPORTER_OTLP_ENDPOINT") || + EnvSecrets.env_or_file("OTEL_LIGHTSTEP_API_KEY") || + EnvSecrets.env_or_file("OTEL_HONEYCOMB_API_KEY")) do + # Enable tracing only when we have a configured endpoint + config :opentelemetry, + span_processor: :batch, + traces_exporter: {:opentelemetry_exporter, %{}} + + config :opentelemetry_exporter, + otlp_protocol: :http_protobuf + + otel_endpoint = System.get_env("OTEL_EXPORTER_OTLP_ENDPOINT") + + if otel_endpoint do + IO.puts("NOTE: OTLP (open telemetry) data will be sent to #{otel_endpoint}") + + config :opentelemetry_exporter, + otlp_endpoint: otel_endpoint + end + + config :opentelemetry_exporter, + # You can configure the compression type for exporting traces. + otlp_compression: :gzip, + otlp_traces_compression: :gzip + + if EnvSecrets.env_or_file("OTEL_LIGHTSTEP_API_KEY") do + IO.puts("NOTE: OTLP (open telemetry) data will be sent to lightstep / servicenow.com") + + # Example configuration, for more refer to: https://github.com/open-telemetry/opentelemetry-erlang/tree/main/apps/opentelemetry_exporter#application-environment + config :opentelemetry_exporter, + otlp_traces_endpoint: "https://ingest.lightstep.com:443/traces/otlp/v0.9", + otlp_headers: [ + {"lightstep-access-token", EnvSecrets.env_or_file("OTEL_LIGHTSTEP_API_KEY")} + ] + end + + if EnvSecrets.env_or_file("OTEL_HONEYCOMB_API_KEY") do + IO.puts("NOTE: OTLP (open telemetry) data will be sent to honeycomb.io") + + config :opentelemetry, :processors, + otel_batch_processor: %{ + exporter: { + :opentelemetry_exporter, + %{ + endpoints: [ + {:https, ~c"api.honeycomb.io", 443, + [ + verify: :verify_peer, + cacertfile: :certifi.cacertfile(), + depth: 3, + customize_hostname_check: [ + match_fun: :public_key.pkix_verify_hostname_match_fun(:https) + ] + ]} + ], + headers: [ + {"x-honeycomb-team", EnvSecrets.env_or_file!("OTEL_HONEYCOMB_API_KEY")}, + {"x-honeycomb-dataset", System.get_env("OTEL_SERVICE_NAME", "bonfire")} + ], + protocol: :grpc + } + } + } + end +else + # Ensure OpenTelemetry is properly disabled when no endpoints are configured + config :opentelemetry, + modularity: :disabled, + span_processor: :simple, + traces_exporter: :none +end + +# Error reporting + +case System.get_env("SENTRY_DSN", "") do + "" -> + config :sentry, modularity: :disabled + + dsn -> + IO.puts("NOTE: errors will be reported to Sentry.") + + config :sentry, + dsn: dsn, + integrations: [ + oban: [ + capture_errors: true, + cron: [enabled: true] + ] + ] + + if System.get_env("SENTRY_NAME") do + config :sentry, server_name: System.get_env("SENTRY_NAME") + end +end + +config :sentry, + source_code_exclude_patterns: [ + ~r/\/flavours\//, + ~r/\/extensions\//, + ~r/\/deps\//, + ~r/\/_build\//, + ~r/\/priv\//, + ~r/\/test\// + ] + +case EnvSecrets.env_or_file("APPSIGNAL_PUSH_API_KEY", "") do + "" -> + config :appsignal, :config, active: false + + api_key -> + IO.puts("NOTE: errors and performance metrics will be reported to AppSignal.") + + config :appsignal, :config, + otp_app: :bonfire, + name: System.get_env("APPSIGNAL_APP_NAME", "bonfire"), + push_api_key: api_key, + env: config_env(), + active: true +end + +config :untangle, + env: config_env(), + # level: :error, + to_io: System.get_env("UNTANGLE_TO_IO") in yes? + +# start prod-only config +if config_env() == :prod do + config :bonfire, Bonfire.Common.Repo, + # ssl: true, + # database: System.get_env("POSTGRES_DB", "bonfire"), + # Note: keep this disabled if using ecto_dev_logger or EctoSparkles.Log instead # + log: String.to_existing_atom(System.get_env("DB_QUERIES_LOG_LEVEL", "false")) +end + +# end prod only config + +# start prod and dev only config +if config_env() != :test do + config :bonfire, Bonfire.Common.Repo, + # The timeout for establishing new connections (default: 5000) + connect_timeout: String.to_integer(System.get_env("DB_CONNECT_TIMEOUT", "10000")), + # The time in milliseconds (as an integer) to wait for the query call to finish (default: 15_000) + timeout: String.to_integer(System.get_env("DB_QUERY_TIMEOUT", "20000")), + # DBConnection pool tuning - CRITICAL for federation workers under load + # queue_target: Time a connection checkout request can wait in queue before DBConnection + # starts to reject new requests. Default is 50ms which is too aggressive for high-concurrency + # workloads. Setting to 5000ms matches test config and allows patient waiting under load. + queue_target: String.to_integer(System.get_env("DB_QUEUE_TARGET", "5000")), + # queue_interval: How often to check if queue_target is being exceeded. Default is 1000ms. + queue_interval: String.to_integer(System.get_env("DB_QUEUE_INTERVAL", "2000")), + # pool_timeout: Overall timeout for getting a connection from the pool. Default is 5000ms. + # Increasing to 30000ms allows workers to wait patiently during load spikes. + pool_timeout: String.to_integer(System.get_env("DB_POOL_TIMEOUT", "30000")), + parameters: [ + # Abort any statement that takes more than the specified amount of time. The timeout is measured from the time a command arrives at the server until it is completed by the server. + statement_timeout: System.get_env("DB_STATEMENT_TIMEOUT", "20000"), + # idle-in-transaction timeout: terminates any session with an open transaction that has been idle for longer than the specified amount of time. This allows any locks held by that session to be released and the connection slot to be reused. WARNING: this seems to also apply to migrations when running in a release, so needs to be high enough for DB migrations and fixtures to run. + idle_in_transaction_session_timeout: System.get_env("DB_IDLE_TRANSACTION_TIMEOUT", "120000") + ] +end + +## bonfire_livebook +if Code.ensure_loaded?(Livebook) do + Livebook.config_runtime() +end + +if api_key = EnvSecrets.env_or_file("PIRATE_WEATHER_API_KEY") do + config :forecastr, + backend: Forecastr.PirateWeather, + appid: api_key, + # minutes to cache + ttl: to_timeout(minute: 14) +end + +if api_key = EnvSecrets.env_or_file("OPEN_WEATHER_MAP_API_KEY") do + config :forecastr, + backend: Forecastr.OWM, + appid: api_key, + # minutes to cache + ttl: to_timeout(minute: 14) +end + +db_ssl? = System.get_env("DB_SSL") in ~w(true yes 1) + +if db_ssl? do + ssl_opts = + case System.get_env("DB_CA_CERT_FILE") do + nil -> true + "" -> true + path -> [cacertfile: path] + end + + config :bonfire, Bonfire.Common.Repo, + ssl: ssl_opts +end + +IO.puts("Runtime config ready") diff --git a/docs/archives/services/app/bonfire/secret.example.yaml b/docs/archives/services/app/bonfire/secret.example.yaml new file mode 100644 index 0000000..775eed6 --- /dev/null +++ b/docs/archives/services/app/bonfire/secret.example.yaml @@ -0,0 +1,12 @@ +--- +postgresql: + password: + bonfire: "" +bonfire: + BONFIRE_SESSION_SECRET: "" + BONFIRE_SESSION_SALT: "" + BONFIRE_ENCRYPT_SALT: "" + BONFIRE_RELEASE_COOKIE: "" + oidc: + secret: "" + hash: "" diff --git a/docs/archives/services/app/bonfire/set_bonfire.yaml b/docs/archives/services/app/bonfire/set_bonfire.yaml new file mode 100644 index 0000000..8ea30dd --- /dev/null +++ b/docs/archives/services/app/bonfire/set_bonfire.yaml @@ -0,0 +1,101 @@ +--- +- name: Create bonfire directory + ansible.builtin.file: + path: "{{ node['home_path'] }}/{{ item }}" + state: "directory" + owner: "{{ ansible_user }}" + group: "{{ node['group'] }}" + mode: "0770" + loop: + - "data/containers/bonfire" + - "data/containers/bonfire/uploads" + - "containers/bonfire" + - "containers/bonfire/ssl" + - "containers/bonfire/config" + +- name: Deploy root certificate + ansible.builtin.copy: + content: | + {{ hostvars['console']['ca']['root']['crt'] }} + dest: "{{ node['home_path'] }}/containers/bonfire/ssl/{{ root_cert_filename }}" + owner: "{{ ansible_user }}" + group: "{{ node['group'] }}" + mode: "0440" + notify: "notification_restart_bonfire" + no_log: true + +- name: Register secret value to podman secret + containers.podman.podman_secret: + name: "{{ item.name }}" + data: "{{ item.value }}" + state: "present" + force: true + loop: + - name: "BONFIRE_SESSION_SECRET" + value: "{{ hostvars['console']['bonfire']['session_secret'] }}" + - name: "BONFIRE_SESSION_SALT" + value: "{{ hostvars['console']['bonfire']['session_salt'] }}" + - name: "BONFIRE_ENCRYPT_SALT" + value: "{{ hostvars['console']['bonfire']['encrypt_salt'] }}" + - name: "BONFIRE_RELEASE_COOKIE" + value: "{{ hostvars['console']['bonfire']['release_cookie'] }}" + - name: "BONFIRE_DB_PASSWORD" + value: "{{ hostvars['console']['postgresql']['password']['bonfire'] }}" + - name: "BONFIRE_OIDC_CLIENT_SECRET" + value: "{{ hostvars['console']['bonfire']['oidc']['secret'] }}" + notify: "notification_restart_bonfire" + no_log: true + +- name: Deploy config file + ansible.builtin.copy: + src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/bonfire/config/runtime.exs" + dest: "{{ node['home_path'] }}/containers/bonfire/config/runtime.exs" + owner: "{{ ansible_user }}" + group: "{{ node['group'] }}" + mode: "0440" + notify: "notification_restart_bonfire" + no_log: true + +- name: Pull image + containers.podman.podman_image: + name: "docker.io/bonfirenetworks/bonfire" + tag: "{{ version['containers']['bonfire'] }}" + state: "present" + pull: true + +- name: Deploy bonfire.container file + ansible.builtin.template: + src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/app/bonfire/bonfire.container.j2" + dest: "{{ node['home_path'] }}/.config/containers/systemd/bonfire.container" + owner: "{{ ansible_user }}" + group: "{{ node['group'] }}" + mode: "0644" + notify: "notification_restart_bonfire" + +- name: Enable bonfire.service + ansible.builtin.systemd: + name: "bonfire.service" + state: "started" + enabled: true + daemon_reload: true + scope: "user" + +- name: Check container archive images + ansible.builtin.stat: + path: "{{ node['home_path'] }}/archives/containers/docker.io_bonfirenetworks_bonfire_{{ version['containers']['bonfire'] }}.tar" + register: container_archive_images + +- name: Save container archive images + containers.podman.podman_save: + image: + - "docker.io/bonfirenetworks/bonfire:{{ version['containers']['bonfire'] }}" + dest: "{{ node['home_path'] }}/archives/containers/docker.io_bonfirenetworks_bonfire_{{ version['containers']['bonfire'] }}.tar" + format: "oci-archive" + force: false + when: not container_archive_images.stat.exists + +- name: Fetch container archive images + ansible.builtin.fetch: + src: "{{ node['home_path'] }}/archives/containers/docker.io_bonfirenetworks_bonfire_{{ version['containers']['bonfire'] }}.tar" + dest: "{{ hostvars['console']['node']['data_path'] }}/images/containers/" + flat: true