From 36e3a4b688215bb394f9f903cdb7a49c598071a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20B=C3=A4rlocher?= Date: Wed, 20 May 2026 22:13:34 +0200 Subject: [PATCH 1/2] feat: domain list refactor + demo-gymburgdorf fixes - Refactor: collapse `*_domain` + `*_extra_domains` into a single `*_domains` list across authentik, collabora, garage and nextcloud roles. First entry is the canonical FQDN (used for OVERWRITEHOST, BASE_URL, notify_push setup and garage root_domain). - Authentik blueprint: guard the OAuth sources block so an empty `authentik_login_sources` no longer renders an invalid YAML key. - Nextcloud: introduce `nextcloud_collabora_public_domain` and set Collabora's `public_wopi_url` separately from the server-to-server `wopi_url` so browsers can reach Collabora via the public name while Nextcloud still talks to it on the internal one. - Nextcloud: URL-encode the postgres user/password in DATABASE_URL. --- roles/authentik/defaults/main.yml | 6 ++++- .../blueprint-login-sources.yaml.j2 | 2 ++ .../authentik/templates/docker-compose.yml.j2 | 5 ++++- roles/collabora/defaults/main.yml | 6 ++++- .../collabora/templates/docker-compose.yml.j2 | 5 ++++- roles/garage/defaults/main.yml | 6 ++++- roles/garage/templates/docker-compose.yml.j2 | 8 ++++++- roles/garage/templates/garage.toml.j2 | 2 +- roles/nextcloud/defaults/main.yml | 7 +++++- roles/nextcloud/tasks/collabora.yml | 8 ++++++- roles/nextcloud/tasks/notify_push.yml | 2 +- .../nextcloud/templates/docker-compose.yml.j2 | 22 ++++++++++++++----- 12 files changed, 64 insertions(+), 15 deletions(-) diff --git a/roles/authentik/defaults/main.yml b/roles/authentik/defaults/main.yml index 9b2ca9a..3ff71be 100644 --- a/roles/authentik/defaults/main.yml +++ b/roles/authentik/defaults/main.yml @@ -12,7 +12,11 @@ authentik_docker_compose_dir: "{{ docker_compose_base_dir }}/{{ authentik_servic authentik_docker_volume_dir: "{{ docker_volume_base_dir }}/{{ authentik_service_name }}" # Authentik service configuration -authentik_domain: "authentik.local.test" +# FQDNs the authentik router accepts. The first entry is the canonical +# domain; further entries cover internal *.int.* names used for +# server-to-server traffic so backend calls don't hairpin via DMZ. +authentik_domains: + - "authentik.local.test" authentik_image: "ghcr.io/goauthentik/server:2026.2.2" authentik_port: 9000 authentik_secret_key: "changeme-generate-a-random-string" diff --git a/roles/authentik/templates/blueprints/blueprint-login-sources.yaml.j2 b/roles/authentik/templates/blueprints/blueprint-login-sources.yaml.j2 index acbb635..8bddb41 100644 --- a/roles/authentik/templates/blueprints/blueprint-login-sources.yaml.j2 +++ b/roles/authentik/templates/blueprints/blueprint-login-sources.yaml.j2 @@ -16,8 +16,10 @@ entries: {% for field in authentik_login_user_fields %} - {{ field }} {% endfor %} +{% if authentik_login_sources %} # OAuth/social login sources (use !Find to reference sources from other blueprints) sources: {% for src in authentik_login_sources %} - !Find [authentik_sources_oauth.oauthsource, [slug, {{ src.slug }}]] {% endfor %} +{% endif %} diff --git a/roles/authentik/templates/docker-compose.yml.j2 b/roles/authentik/templates/docker-compose.yml.j2 index c9796a2..f4a1f95 100644 --- a/roles/authentik/templates/docker-compose.yml.j2 +++ b/roles/authentik/templates/docker-compose.yml.j2 @@ -48,10 +48,13 @@ services: labels: - traefik.enable=true - traefik.docker.network={{ authentik_traefik_network }} - - traefik.http.routers.{{ authentik_service_name }}.rule=Host(`{{ authentik_domain }}`) + - traefik.http.routers.{{ authentik_service_name }}.rule=Host({% for d in authentik_domains %}`{{ d }}`{% if not loop.last %}, {% endif %}{% endfor %}) {% if authentik_use_ssl %} - traefik.http.routers.{{ authentik_service_name }}.entrypoints=websecure - traefik.http.routers.{{ authentik_service_name }}.tls=true +{% if traefik_cert_mode | default('selfsigned') == 'acme' %} + - traefik.http.routers.{{ authentik_service_name }}.tls.certresolver={{ traefik_ssl_cert_resolver | default('dns') }} +{% endif %} {% else %} - traefik.http.routers.{{ authentik_service_name }}.entrypoints=web {% endif %} diff --git a/roles/collabora/defaults/main.yml b/roles/collabora/defaults/main.yml index 3cfb559..11aa468 100644 --- a/roles/collabora/defaults/main.yml +++ b/roles/collabora/defaults/main.yml @@ -12,7 +12,11 @@ collabora_docker_compose_dir: "{{ docker_compose_base_dir }}/{{ collabora_servic collabora_docker_volume_dir: "{{ docker_volume_base_dir }}/{{ collabora_service_name }}" # Service configuration -collabora_domain: "office.local.test" +# FQDNs the collabora router accepts. The first entry is the canonical +# domain; further entries cover internal *.int.* names used for +# server-to-server WOPI discovery. +collabora_domains: + - "office.local.test" collabora_image: "collabora/code:latest" collabora_port: 9980 collabora_extra_hosts: [] diff --git a/roles/collabora/templates/docker-compose.yml.j2 b/roles/collabora/templates/docker-compose.yml.j2 index c0f589e..af6ecfc 100644 --- a/roles/collabora/templates/docker-compose.yml.j2 +++ b/roles/collabora/templates/docker-compose.yml.j2 @@ -20,11 +20,14 @@ services: labels: - traefik.enable=true - traefik.docker.network={{ collabora_traefik_network }} - - traefik.http.routers.{{ collabora_service_name }}.rule=Host(`{{ collabora_domain }}`) + - traefik.http.routers.{{ collabora_service_name }}.rule=Host({% for d in collabora_domains %}`{{ d }}`{% if not loop.last %}, {% endif %}{% endfor %}) - traefik.http.services.{{ collabora_service_name }}.loadbalancer.server.port={{ collabora_port }} {% if collabora_use_ssl %} - traefik.http.routers.{{ collabora_service_name }}.entrypoints=websecure - traefik.http.routers.{{ collabora_service_name }}.tls=true +{% if traefik_cert_mode | default('selfsigned') == 'acme' %} + - traefik.http.routers.{{ collabora_service_name }}.tls.certresolver={{ traefik_ssl_cert_resolver | default('dns') }} +{% endif %} {% else %} - traefik.http.routers.{{ collabora_service_name }}.entrypoints=web {% endif %} diff --git a/roles/garage/defaults/main.yml b/roles/garage/defaults/main.yml index 495317e..091e318 100644 --- a/roles/garage/defaults/main.yml +++ b/roles/garage/defaults/main.yml @@ -13,7 +13,11 @@ garage_docker_volume_dir: "{{ docker_volume_base_dir }}/{{ garage_service_name } # Garage service configuration garage_image: "dxflrs/garage:v2.1.0" -garage_s3_domain: "storage.local.test" +# FQDNs the garage S3 router accepts. The first entry is the canonical +# domain and is also used as the virtual-hosted-style root_domain in +# garage.toml; further entries cover internal *.int.* names. +garage_s3_domains: + - "storage.local.test" garage_web_domain: "web.storage.local.test" garage_webui_domain: "console.storage.local.test" diff --git a/roles/garage/templates/docker-compose.yml.j2 b/roles/garage/templates/docker-compose.yml.j2 index 9e3e862..0427fb2 100644 --- a/roles/garage/templates/docker-compose.yml.j2 +++ b/roles/garage/templates/docker-compose.yml.j2 @@ -14,10 +14,13 @@ services: - traefik.enable=true - traefik.docker.network={{ garage_traefik_network }} # S3 API endpoint - - traefik.http.routers.{{ garage_service_name }}.rule=Host(`{{ garage_s3_domain }}`) + - traefik.http.routers.{{ garage_service_name }}.rule=Host({% for d in garage_s3_domains %}`{{ d }}`{% if not loop.last %}, {% endif %}{% endfor %}) {% if garage_use_ssl %} - traefik.http.routers.{{ garage_service_name }}.entrypoints=websecure - traefik.http.routers.{{ garage_service_name }}.tls=true +{% if traefik_cert_mode | default('selfsigned') == 'acme' %} + - traefik.http.routers.{{ garage_service_name }}.tls.certresolver={{ traefik_ssl_cert_resolver | default('dns') }} +{% endif %} {% else %} - traefik.http.routers.{{ garage_service_name }}.entrypoints=web {% endif %} @@ -48,6 +51,9 @@ services: {% if garage_use_ssl %} - traefik.http.routers.{{ garage_service_name }}-console.entrypoints=websecure - traefik.http.routers.{{ garage_service_name }}-console.tls=true +{% if traefik_cert_mode | default('selfsigned') == 'acme' %} + - traefik.http.routers.{{ garage_service_name }}-console.tls.certresolver={{ traefik_ssl_cert_resolver | default('dns') }} +{% endif %} {% else %} - traefik.http.routers.{{ garage_service_name }}-console.entrypoints=web {% endif %} diff --git a/roles/garage/templates/garage.toml.j2 b/roles/garage/templates/garage.toml.j2 index 897ebcd..06b1164 100644 --- a/roles/garage/templates/garage.toml.j2 +++ b/roles/garage/templates/garage.toml.j2 @@ -14,7 +14,7 @@ rpc_secret = "{{ garage_rpc_secret }}" [s3_api] s3_region = "{{ garage_s3_region }}" api_bind_addr = "[::]:{{ garage_s3_api_port }}" -root_domain = ".s3.{{ garage_s3_domain }}" +root_domain = ".s3.{{ garage_s3_domains[0] }}" [s3_web] bind_addr = "[::]:{{ garage_s3_web_port }}" diff --git a/roles/nextcloud/defaults/main.yml b/roles/nextcloud/defaults/main.yml index 7535b5a..1c46015 100644 --- a/roles/nextcloud/defaults/main.yml +++ b/roles/nextcloud/defaults/main.yml @@ -9,7 +9,12 @@ nextcloud_service_name: nextcloud nextcloud_docker_compose_dir: "{{ docker_compose_base_dir }}/{{ nextcloud_service_name }}" nextcloud_docker_volume_dir: "{{ docker_volume_base_dir }}/{{ nextcloud_service_name }}" -nextcloud_domain: "nextcloud.local.test" +# FQDNs the nextcloud router accepts. The first entry is the canonical +# domain (used for OVERWRITEHOST and the notify_push setup); further +# entries cover internal *.int.* names so collabora's WOPI callback +# hits us on a name with a valid cert. +nextcloud_domains: + - "nextcloud.local.test" nextcloud_image: "nextcloud:fpm" nextcloud_redis_image: "redis:latest" nextcloud_port: 80 diff --git a/roles/nextcloud/tasks/collabora.yml b/roles/nextcloud/tasks/collabora.yml index 05c56e4..2a7bd82 100644 --- a/roles/nextcloud/tasks/collabora.yml +++ b/roles/nextcloud/tasks/collabora.yml @@ -1,11 +1,17 @@ #SPDX-License-Identifier: MIT-0 --- # tasks file for configuring Collabora in Nextcloud -- name: Configure Collabora WOPI URL +- name: Configure Collabora WOPI URL (server-to-server) community.docker.docker_container_exec: container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1" command: php /var/www/html/occ config:app:set richdocuments wopi_url --value=https://{{ nextcloud_collabora_domain }} +- name: Configure Collabora public WOPI URL (browser-facing) + community.docker.docker_container_exec: + container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1" + command: php /var/www/html/occ config:app:set richdocuments public_wopi_url --value=https://{{ nextcloud_collabora_public_domain }} + when: nextcloud_collabora_public_domain is defined and nextcloud_collabora_public_domain != nextcloud_collabora_domain + - name: Configure certificate verification for Collabora community.docker.docker_container_exec: container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1" diff --git a/roles/nextcloud/tasks/notify_push.yml b/roles/nextcloud/tasks/notify_push.yml index 18dbb8b..ccf2b72 100644 --- a/roles/nextcloud/tasks/notify_push.yml +++ b/roles/nextcloud/tasks/notify_push.yml @@ -5,4 +5,4 @@ - name: Configure notify_push base endpoint community.docker.docker_container_exec: container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1" - command: php /var/www/html/occ notify_push:setup https://{{ nextcloud_domain }}/push \ No newline at end of file + command: php /var/www/html/occ notify_push:setup https://{{ nextcloud_domains[0] }}/push \ No newline at end of file diff --git a/roles/nextcloud/templates/docker-compose.yml.j2 b/roles/nextcloud/templates/docker-compose.yml.j2 index 9f15760..0e05090 100644 --- a/roles/nextcloud/templates/docker-compose.yml.j2 +++ b/roles/nextcloud/templates/docker-compose.yml.j2 @@ -35,10 +35,13 @@ services: labels: - traefik.enable=true - traefik.docker.network={{ nextcloud_traefik_network }} - - traefik.http.routers.{{ nextcloud_service_name }}.rule=Host(`{{ nextcloud_domain }}`) + - traefik.http.routers.{{ nextcloud_service_name }}.rule=Host({% for d in nextcloud_domains %}`{{ d }}`{% if not loop.last %}, {% endif %}{% endfor %}) {% if nextcloud_use_ssl %} - traefik.http.routers.{{ nextcloud_service_name }}.entrypoints=websecure - traefik.http.routers.{{ nextcloud_service_name }}.tls=true +{% if traefik_cert_mode | default('selfsigned') == 'acme' %} + - traefik.http.routers.{{ nextcloud_service_name }}.tls.certresolver={{ traefik_ssl_cert_resolver | default('dns') }} +{% endif %} {% else %} - traefik.http.routers.{{ nextcloud_service_name }}.entrypoints=web {% endif %} @@ -60,7 +63,7 @@ services: PHP_MEMORY_LIMIT: {{ nextcloud_memory_limit_mb }}M PHP_UPLOAD_LIMIT: {{ nextcloud_upload_limit_mb }}M OVERWRITEPROTOCOL: https - OVERWRITEHOST: {{ nextcloud_domain }} + OVERWRITEHOST: {{ nextcloud_domains[0] }} TRUSTED_PROXIES: "{{ nextcloud_trusted_proxies }}" volumes: - {{ nextcloud_docker_volume_dir }}/nextcloud/:/var/www/html @@ -69,6 +72,12 @@ services: {% for net in nextcloud_extra_networks %} - {{ net }} {% endfor %} +{% if nextcloud_extra_hosts is defined and nextcloud_extra_hosts | length > 0 %} + extra_hosts: +{% for host in nextcloud_extra_hosts %} + - "{{ host }}" +{% endfor %} +{% endif %} nextcloud: image: {{ nextcloud_image }} @@ -88,7 +97,7 @@ services: PHP_MEMORY_LIMIT: {{ nextcloud_memory_limit_mb }}M PHP_UPLOAD_LIMIT: {{ nextcloud_upload_limit_mb }}M OVERWRITEPROTOCOL: https - OVERWRITEHOST: {{ nextcloud_domain }} + OVERWRITEHOST: {{ nextcloud_domains[0] }} TRUSTED_PROXIES: "{{ nextcloud_trusted_proxies }}" {% if nextcloud_use_s3_storage %} OBJECTSTORE_S3_KEY: {{ nextcloud_s3_key }} @@ -127,7 +136,7 @@ services: environment: PORT: "7867" REDIS_URL: "redis://redis:6379" - DATABASE_URL: "postgres://{{ nextcloud_postgres_user }}:{{ nextcloud_postgres_password }}@db:5432/{{ nextcloud_postgres_db }}" + DATABASE_URL: "postgres://{{ nextcloud_postgres_user | urlencode }}:{{ nextcloud_postgres_password | urlencode }}@db:5432/{{ nextcloud_postgres_db }}" DATABASE_PREFIX: "oc_" NEXTCLOUD_URL: "http://nginx" networks: @@ -136,11 +145,14 @@ services: labels: - traefik.enable=true - traefik.docker.network={{ nextcloud_traefik_network }} - - traefik.http.routers.{{ nextcloud_service_name }}-push.rule=Host(`{{ nextcloud_domain }}`) && PathPrefix(`/push`) + - traefik.http.routers.{{ nextcloud_service_name }}-push.rule=Host(`{{ nextcloud_domains[0] }}`) && PathPrefix(`/push`) - traefik.http.services.{{ nextcloud_service_name }}-push.loadbalancer.server.port=7867 {% if nextcloud_use_ssl %} - traefik.http.routers.{{ nextcloud_service_name }}-push.entrypoints=websecure - traefik.http.routers.{{ nextcloud_service_name }}-push.tls=true +{% if traefik_cert_mode | default('selfsigned') == 'acme' %} + - traefik.http.routers.{{ nextcloud_service_name }}-push.tls.certresolver={{ traefik_ssl_cert_resolver | default('dns') }} +{% endif %} {% else %} - traefik.http.routers.{{ nextcloud_service_name }}-push.entrypoints=web {% endif %} From 02d45026a559c02383ed32a3c123d02f5717650c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20B=C3=A4rlocher?= Date: Wed, 20 May 2026 22:44:41 +0200 Subject: [PATCH 2/2] feat: drop blanket recreates, ACME-DNS knobs, notify_push override - Drop `recreate: always` from collabora/drawio/homarr/opencloud/traefik handlers and the authentik_outpost_ldap start task. `up -d` with `state: present` already recreates exactly the services whose compose definition changed; the blanket recreate was forcing restarts even when nothing relevant moved. - Rewrite the `*_domains` Traefik Host loop to the `Host(\`a\`) || Host(\`b\`)` form across authentik/collabora/garage/nextcloud so the rule still matches when traefik can't normalize the comma-form into the same canonical shape. - Traefik: add `traefik_acme_tcp_only` (sets LEGO_EXPERIMENTAL_DNS_TCP_ONLY) and `traefik_acme_disable_ans_checks` (disables lego's authoritative-NS propagation check) for environments where the DNS path between the traefik container and the zone's nameservers is constrained. - Traefik DMZ collector: two-step merge so a `traefik_dmz_exposed_services` entry that sets its own `backend_host` wins over the host fallback; lets a route target an internal FQDN covered by the backend cert's SANs instead of the raw IP. - Nextcloud: add `nextcloud_notify_push_domain` override for the `occ notify_push:setup` call so the setup check can hit an internal FQDN instead of hairpinning through the DMZ. Push router now matches every entry in `nextcloud_domains`. - Nextcloud: also %2F-escape slashes in the postgres user/password inside the notify_push DATABASE_URL. --- roles/authentik/templates/docker-compose.yml.j2 | 2 +- roles/collabora/handlers/main.yml | 2 +- roles/collabora/templates/docker-compose.yml.j2 | 2 +- roles/drawio/handlers/main.yml | 2 +- roles/garage/templates/docker-compose.yml.j2 | 2 +- roles/homarr/handlers/main.yml | 2 +- roles/nextcloud/defaults/main.yml | 6 ++++++ roles/nextcloud/tasks/notify_push.yml | 2 +- roles/nextcloud/templates/docker-compose.yml.j2 | 6 +++--- roles/opencloud/handlers/main.yml | 2 +- roles/traefik/defaults/main.yml | 12 ++++++++++++ roles/traefik/handlers/main.yml | 2 +- roles/traefik/tasks/main.yml | 13 ++++++++++++- roles/traefik/templates/docker-compose.yml.j2 | 3 +++ roles/traefik/templates/traefik.yml.j2 | 4 ++++ 15 files changed, 49 insertions(+), 13 deletions(-) diff --git a/roles/authentik/templates/docker-compose.yml.j2 b/roles/authentik/templates/docker-compose.yml.j2 index f4a1f95..f0193ec 100644 --- a/roles/authentik/templates/docker-compose.yml.j2 +++ b/roles/authentik/templates/docker-compose.yml.j2 @@ -48,7 +48,7 @@ services: labels: - traefik.enable=true - traefik.docker.network={{ authentik_traefik_network }} - - traefik.http.routers.{{ authentik_service_name }}.rule=Host({% for d in authentik_domains %}`{{ d }}`{% if not loop.last %}, {% endif %}{% endfor %}) + - traefik.http.routers.{{ authentik_service_name }}.rule={% for d in authentik_domains %}Host(`{{ d }}`){% if not loop.last %} || {% endif %}{% endfor +%} {% if authentik_use_ssl %} - traefik.http.routers.{{ authentik_service_name }}.entrypoints=websecure - traefik.http.routers.{{ authentik_service_name }}.tls=true diff --git a/roles/collabora/handlers/main.yml b/roles/collabora/handlers/main.yml index bfd2b02..3364bcc 100644 --- a/roles/collabora/handlers/main.yml +++ b/roles/collabora/handlers/main.yml @@ -5,4 +5,4 @@ - name: restart collabora community.docker.docker_compose_v2: project_src: "{{ collabora_docker_compose_dir }}" - state: restarted \ No newline at end of file + state: present \ No newline at end of file diff --git a/roles/collabora/templates/docker-compose.yml.j2 b/roles/collabora/templates/docker-compose.yml.j2 index af6ecfc..353a08b 100644 --- a/roles/collabora/templates/docker-compose.yml.j2 +++ b/roles/collabora/templates/docker-compose.yml.j2 @@ -20,7 +20,7 @@ services: labels: - traefik.enable=true - traefik.docker.network={{ collabora_traefik_network }} - - traefik.http.routers.{{ collabora_service_name }}.rule=Host({% for d in collabora_domains %}`{{ d }}`{% if not loop.last %}, {% endif %}{% endfor %}) + - traefik.http.routers.{{ collabora_service_name }}.rule={% for d in collabora_domains %}Host(`{{ d }}`){% if not loop.last %} || {% endif %}{% endfor +%} - traefik.http.services.{{ collabora_service_name }}.loadbalancer.server.port={{ collabora_port }} {% if collabora_use_ssl %} - traefik.http.routers.{{ collabora_service_name }}.entrypoints=websecure diff --git a/roles/drawio/handlers/main.yml b/roles/drawio/handlers/main.yml index f1ef0da..b68633d 100644 --- a/roles/drawio/handlers/main.yml +++ b/roles/drawio/handlers/main.yml @@ -5,4 +5,4 @@ - name: restart drawio community.docker.docker_compose_v2: project_src: "{{ drawio_docker_compose_dir }}" - state: restarted \ No newline at end of file + state: present \ No newline at end of file diff --git a/roles/garage/templates/docker-compose.yml.j2 b/roles/garage/templates/docker-compose.yml.j2 index 0427fb2..d344e5f 100644 --- a/roles/garage/templates/docker-compose.yml.j2 +++ b/roles/garage/templates/docker-compose.yml.j2 @@ -14,7 +14,7 @@ services: - traefik.enable=true - traefik.docker.network={{ garage_traefik_network }} # S3 API endpoint - - traefik.http.routers.{{ garage_service_name }}.rule=Host({% for d in garage_s3_domains %}`{{ d }}`{% if not loop.last %}, {% endif %}{% endfor %}) + - traefik.http.routers.{{ garage_service_name }}.rule={% for d in garage_s3_domains %}Host(`{{ d }}`){% if not loop.last %} || {% endif %}{% endfor +%} {% if garage_use_ssl %} - traefik.http.routers.{{ garage_service_name }}.entrypoints=websecure - traefik.http.routers.{{ garage_service_name }}.tls=true diff --git a/roles/homarr/handlers/main.yml b/roles/homarr/handlers/main.yml index dedb949..da74ed4 100644 --- a/roles/homarr/handlers/main.yml +++ b/roles/homarr/handlers/main.yml @@ -5,4 +5,4 @@ - name: restart homarr community.docker.docker_compose_v2: project_src: "{{ homarr_docker_compose_dir }}" - state: restarted \ No newline at end of file + state: present \ No newline at end of file diff --git a/roles/nextcloud/defaults/main.yml b/roles/nextcloud/defaults/main.yml index 1c46015..b60dbc3 100644 --- a/roles/nextcloud/defaults/main.yml +++ b/roles/nextcloud/defaults/main.yml @@ -65,6 +65,12 @@ nextcloud_trusted_proxies: "172.16.0.0/12" # File locking and real-time push notifications nextcloud_enable_notify_push: false nextcloud_notify_push_image: "icewind1991/notify_push:1.3.1" +# Domain used when calling `occ notify_push:setup`. Defaults to the +# first nextcloud_domains entry (the canonical public name). Override +# with an internal FQDN to avoid hairpinning the setup check through +# the DMZ; the FQDN must also be in nextcloud_domains so the push +# router matches it. +# nextcloud_notify_push_domain: "cloud.int.example.com" # Non-default apps to install and enable nextcloud_apps_to_install: diff --git a/roles/nextcloud/tasks/notify_push.yml b/roles/nextcloud/tasks/notify_push.yml index ccf2b72..1497c68 100644 --- a/roles/nextcloud/tasks/notify_push.yml +++ b/roles/nextcloud/tasks/notify_push.yml @@ -5,4 +5,4 @@ - name: Configure notify_push base endpoint community.docker.docker_container_exec: container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1" - command: php /var/www/html/occ notify_push:setup https://{{ nextcloud_domains[0] }}/push \ No newline at end of file + command: php /var/www/html/occ notify_push:setup https://{{ nextcloud_notify_push_domain | default(nextcloud_domains[0]) }}/push \ No newline at end of file diff --git a/roles/nextcloud/templates/docker-compose.yml.j2 b/roles/nextcloud/templates/docker-compose.yml.j2 index 0e05090..365a766 100644 --- a/roles/nextcloud/templates/docker-compose.yml.j2 +++ b/roles/nextcloud/templates/docker-compose.yml.j2 @@ -35,7 +35,7 @@ services: labels: - traefik.enable=true - traefik.docker.network={{ nextcloud_traefik_network }} - - traefik.http.routers.{{ nextcloud_service_name }}.rule=Host({% for d in nextcloud_domains %}`{{ d }}`{% if not loop.last %}, {% endif %}{% endfor %}) + - traefik.http.routers.{{ nextcloud_service_name }}.rule={% for d in nextcloud_domains %}Host(`{{ d }}`){% if not loop.last %} || {% endif %}{% endfor +%} {% if nextcloud_use_ssl %} - traefik.http.routers.{{ nextcloud_service_name }}.entrypoints=websecure - traefik.http.routers.{{ nextcloud_service_name }}.tls=true @@ -136,7 +136,7 @@ services: environment: PORT: "7867" REDIS_URL: "redis://redis:6379" - DATABASE_URL: "postgres://{{ nextcloud_postgres_user | urlencode }}:{{ nextcloud_postgres_password | urlencode }}@db:5432/{{ nextcloud_postgres_db }}" + DATABASE_URL: "postgres://{{ nextcloud_postgres_user | urlencode | replace('/', '%2F') }}:{{ nextcloud_postgres_password | urlencode | replace('/', '%2F') }}@db:5432/{{ nextcloud_postgres_db }}" DATABASE_PREFIX: "oc_" NEXTCLOUD_URL: "http://nginx" networks: @@ -145,7 +145,7 @@ services: labels: - traefik.enable=true - traefik.docker.network={{ nextcloud_traefik_network }} - - traefik.http.routers.{{ nextcloud_service_name }}-push.rule=Host(`{{ nextcloud_domains[0] }}`) && PathPrefix(`/push`) + - traefik.http.routers.{{ nextcloud_service_name }}-push.rule=({% for d in nextcloud_domains %}Host(`{{ d }}`){% if not loop.last %} || {% endif %}{% endfor %}) && PathPrefix(`/push`) - traefik.http.services.{{ nextcloud_service_name }}-push.loadbalancer.server.port=7867 {% if nextcloud_use_ssl %} - traefik.http.routers.{{ nextcloud_service_name }}-push.entrypoints=websecure diff --git a/roles/opencloud/handlers/main.yml b/roles/opencloud/handlers/main.yml index 95b6986..7cbc094 100644 --- a/roles/opencloud/handlers/main.yml +++ b/roles/opencloud/handlers/main.yml @@ -5,4 +5,4 @@ - name: restart opencloud community.docker.docker_compose_v2: project_src: "{{ opencloud_docker_compose_dir }}" - state: restarted \ No newline at end of file + state: present \ No newline at end of file diff --git a/roles/traefik/defaults/main.yml b/roles/traefik/defaults/main.yml index 3e43412..eea7391 100644 --- a/roles/traefik/defaults/main.yml +++ b/roles/traefik/defaults/main.yml @@ -33,6 +33,18 @@ traefik_acme_tsig_secret: "" # TSIG secret traefik_acme_propagation_timeout: "120" traefik_acme_polling_interval: "2" traefik_acme_ttl: "60" +# Force lego's DNS lookups (SOA resolution, propagation checks) onto +# TCP instead of UDP. Useful when container egress can reach the +# nameserver on TCP/53 but UDP/53 is blocked or unreliable. Sets the +# upstream env var LEGO_EXPERIMENTAL_DNS_TCP_ONLY=true on the +# traefik container. +traefik_acme_tcp_only: false +# Disable lego's propagation check against the zone's authoritative +# nameservers. Use when the SOA-listed NS hostname resolves to an +# address that isn't reachable from this traefik host (e.g. a DMZ +# box that can only see the internal NS IP, not the public one). +# lego still polls via the configured `resolvers:` list. +traefik_acme_disable_ans_checks: false # Self-signed certificate configuration (for vagrant/testing) traefik_selfsigned_cert_dir: "{{ docker_volume_dir }}/certs" diff --git a/roles/traefik/handlers/main.yml b/roles/traefik/handlers/main.yml index 4abd95a..8af7aac 100644 --- a/roles/traefik/handlers/main.yml +++ b/roles/traefik/handlers/main.yml @@ -5,4 +5,4 @@ - name: restart traefik community.docker.docker_compose_v2: project_src: "{{ docker_compose_dir }}" - state: restarted + state: present diff --git a/roles/traefik/tasks/main.yml b/roles/traefik/tasks/main.yml index 159ba8f..8934037 100644 --- a/roles/traefik/tasks/main.yml +++ b/roles/traefik/tasks/main.yml @@ -9,7 +9,18 @@ - name: Build service registry from backend servers (DMZ mode) set_fact: - proxied_services: "{{ proxied_services | default([]) + hostvars[item].traefik_dmz_exposed_services | default([]) | map('combine', {'backend_host': hostvars[item].ansible_host | default(item)}) | list }}" + # Two-step merge so a service entry's own `backend_host` wins: + # entries that set it pass through unchanged, entries that don't + # get the backend host's ansible_host as fallback. The override + # lets a route target an internal FQDN covered by the backend + # cert's SANs instead of the raw IP (which would fail backend + # TLS verification at the proxy hop). + proxied_services: >- + {{ + proxied_services | default([]) + + (hostvars[item].traefik_dmz_exposed_services | default([]) | selectattr('backend_host', 'defined') | list) + + (hostvars[item].traefik_dmz_exposed_services | default([]) | rejectattr('backend_host', 'defined') | map('combine', {'backend_host': hostvars[item].ansible_host | default(item)}) | list) + }} loop: "{{ _backend_servers | default([]) }}" when: traefik_mode == 'dmz' diff --git a/roles/traefik/templates/docker-compose.yml.j2 b/roles/traefik/templates/docker-compose.yml.j2 index e45578b..6dbb9ec 100644 --- a/roles/traefik/templates/docker-compose.yml.j2 +++ b/roles/traefik/templates/docker-compose.yml.j2 @@ -12,6 +12,9 @@ services: RFC2136_PROPAGATION_TIMEOUT: "{{ traefik_acme_propagation_timeout }}" RFC2136_POLLING_INTERVAL: "{{ traefik_acme_polling_interval }}" RFC2136_TTL: "{{ traefik_acme_ttl }}" +{% if traefik_acme_tcp_only | default(false) %} + LEGO_EXPERIMENTAL_DNS_TCP_ONLY: "true" +{% endif %} {% endif %} ports: - "80:80" diff --git a/roles/traefik/templates/traefik.yml.j2 b/roles/traefik/templates/traefik.yml.j2 index 64bf351..1900bbb 100644 --- a/roles/traefik/templates/traefik.yml.j2 +++ b/roles/traefik/templates/traefik.yml.j2 @@ -48,6 +48,10 @@ certificatesResolvers: provider: rfc2136 resolvers: - "{{ traefik_acme_dns_nameserver }}" +{% if traefik_acme_disable_ans_checks | default(false) %} + propagation: + disableANSChecks: true +{% endif %} {% endif %} {% if traefik_use_ssl %}