feat(mastodon): release mastodon

This commit is contained in:
il
2026-07-05 23:25:42 +09:00
parent 3c8469e604
commit e0df8cfba5
24 changed files with 1338 additions and 62 deletions
+26 -12
View File
@@ -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"
+14 -3
View File
@@ -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"
+8
View File
@@ -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"
+15
View File
@@ -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
@@ -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 }}"
@@ -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'] }}"
@@ -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"
@@ -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"
+23 -20
View File
@@ -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
@@ -22,4 +22,4 @@ RestartSec=10s
TimeoutStopSec=120
[Install]
WantedBy=default.target
WantedBy=default.target
@@ -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
@@ -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
@@ -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
@@ -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'] }}"
@@ -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
@@ -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'
@@ -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}
}
@@ -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: |
(
@@ -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",
@@ -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
@@ -0,0 +1,14 @@
---
services:
bonfire:
domain:
public: ""
internal: ""
ports:
http: ""
dependency:
web: ""
db: ""
version:
bonfire: "1.0.5-social-amd64"
@@ -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 (`<NAME>_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")
@@ -0,0 +1,12 @@
---
postgresql:
password:
bonfire: ""
bonfire:
BONFIRE_SESSION_SECRET: ""
BONFIRE_SESSION_SALT: ""
BONFIRE_ENCRYPT_SALT: ""
BONFIRE_RELEASE_COOKIE: ""
oidc:
secret: ""
hash: ""
@@ -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