fix(apps): swarm-specific deploy fixes (matomo, nextcloud, prometheus, dashboard)

matomo: force-restart the swarm service after patching config.ini.php DB creds (the compose-up handler is a no-op in swarm), extracted into 10_resolve_host_cid. nextcloud: gate the redis PONG wait on the engine being local. prometheus: config 0444 (swarm config objects are root) and gate the host-monitor binds plus privileged cadvisor/node-exporter to compose only. dashboard: probe the public icon URL in swarm.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 4.8 committed 2026-06-23 19:29:39 +02:00
1 parent 8000dbe947
commit 9f43364c00
16 files changed
+136 -74

No files matched your search

@@ -67,7 +67,11 @@ def resolve_simpleicons_base(simpleicons_value, web_protocol="https"):
def add_simpleicon_source(
cards, simpleicons_value, web_protocol="https", public_url_base=None
cards,
simpleicons_value,
web_protocol="https",
public_url_base=None,
probe_public=False,
):
"""
For each card in portfolio_cards, check if an icon exists in the simpleicons server.
@@ -80,6 +84,10 @@ def add_simpleicon_source(
:param simpleicons_value: Fully rendered URL/domain used for the HEAD reachability check
:param web_protocol: Protocol to use (https or http) when resolving a bare domain
:param public_url_base: Optional separate public base URL written into icon.source
:param probe_public: Probe `public_url_base` instead of the sync URL. The sync
URL is published on the swarm node, not on the Ansible controller that
runs this filter, so per-card probes from the controller never reach it;
the public URL goes through the front proxy the controller can resolve.
:return: New list of cards with icon.source set when the icon is reachable
"""
probe_base = resolve_simpleicons_base(simpleicons_value, web_protocol)
@@ -88,6 +96,8 @@ def add_simpleicon_source(
if public_url_base
else probe_base
)
if probe_public and public_url_base:
probe_base = rewrite_base
enhanced = []
for card in cards:
@@ -16,7 +16,7 @@
- name: "Load Dashboard Brand logos"
set_fact:
portfolio_cards: "{{ portfolio_cards | add_simpleicon_source(DASHBOARD_SIMPLEICONS_SYNC_URL_BASE, public_url_base=DASHBOARD_SIMPLEICONS_URL_BASE) }}"
portfolio_cards: "{{ portfolio_cards | add_simpleicon_source(DASHBOARD_SIMPLEICONS_SYNC_URL_BASE, public_url_base=DASHBOARD_SIMPLEICONS_URL_BASE, probe_public=(DEPLOYMENT_MODE == 'swarm')) }}"
when: DASHBOARD_SIMPLEICONS_ENABLED | bool
changed_when: false
@@ -11,7 +11,8 @@
- name: Reload docker cards once Simple Icons is reachable
set_fact:
portfolio_cards: "{{ portfolio_cards | add_simpleicon_source(DASHBOARD_SIMPLEICONS_SYNC_URL_BASE, public_url_base=DASHBOARD_SIMPLEICONS_URL_BASE) }}"
portfolio_cards: "{{ portfolio_cards | add_simpleicon_source(DASHBOARD_SIMPLEICONS_SYNC_URL_BASE, public_url_base=DASHBOARD_SIMPLEICONS_URL_BASE, probe_public=(DEPLOYMENT_MODE
== 'swarm')) }}"
changed_when: false
- name: Regroup docker cards after Simple Icons refresh
+1
View File
@@ -60,6 +60,7 @@ redis:
backup:
disabled: true
enabled: false
shared: "{{ 'svc-db-redis' in group_names }}"
# nocheck: playwright-service-flag — verified by web-svc-css provider spec
css:
enabled: "{{ 'web-svc-css' in group_names }}"
+4
View File
@@ -17,6 +17,8 @@
shared: true
mariadb:
shared: true
redis:
shared: true
prometheus:
enabled: true
shared: true
@@ -32,6 +34,8 @@
shared: false
mariadb:
shared: false
redis:
shared: false
prometheus:
enabled: false
shared: false
+2 -62
View File
@@ -10,68 +10,8 @@
- name: flush docker service
meta: flush_handlers
- name: "Matomo install | Manager-side path (swarm)"
when: DEPLOYMENT_MODE == 'swarm'
block:
- name: "Matomo install | Wait for matomo task to converge"
ansible.builtin.shell: |
container service ps {{ lookup('container_service', application_id, MATOMO_SERVICE) | quote }} \
--filter desired-state=running \
--format '{{ "{{.CurrentState}}" }}' \
| head -1 | grep -qE '^Running'
register: matomo_running_swarm
changed_when: false
retries: 600
delay: 2
until: matomo_running_swarm.rc == 0
- name: "Matomo install | Resolve hosting node"
ansible.builtin.shell: |
container service ps {{ lookup('container_service', application_id, MATOMO_SERVICE) | quote }} \
--filter desired-state=running \
--format '{{ "{{.Node}}" }}' | head -1
register: matomo_swarm_node_query
changed_when: false
- name: "Matomo install | Set MATOMO_HOST_NODE fact"
ansible.builtin.set_fact:
MATOMO_HOST_NODE: "{{ matomo_swarm_node_query.stdout | trim }}"
- name: "Matomo install | Manager-side path (compose)"
when: DEPLOYMENT_MODE != 'swarm'
block:
- name: "Matomo install | Wait for matomo container to be running"
ansible.builtin.shell: >
container inspect -f '{{ "{{" }}.State.Running{{ "}}" }}'
{{ lookup('container_address', application_id, MATOMO_SERVICE) }}
register: matomo_running_compose
changed_when: false
retries: 150
delay: 2
until: >
matomo_running_compose.rc == 0 and (matomo_running_compose.stdout | trim) == 'true'
- name: "Matomo install | Set MATOMO_HOST_NODE fact"
ansible.builtin.set_fact:
MATOMO_HOST_NODE: "{{ inventory_hostname }}"
- name: "Matomo install | Resolve local container ID on hosting node"
ansible.builtin.shell: |
{% if DEPLOYMENT_MODE == 'swarm' %}
container ps \
--filter "label=com.docker.swarm.service.name={{ lookup('container_service', application_id, MATOMO_SERVICE) }}" \
--filter status=running \
--format '{{ "{{.ID}}" }}' | head -1
{% else %}
echo {{ MATOMO_NAME | quote }}
{% endif %}
register: matomo_local_cid_query
changed_when: false
delegate_to: "{{ MATOMO_HOST_NODE }}"
- name: "Matomo install | Set MATOMO_LOCAL_CID fact"
ansible.builtin.set_fact:
MATOMO_LOCAL_CID: "{{ matomo_local_cid_query.stdout | trim }}"
- name: "Matomo install | Resolve hosting node + local container ID"
include_tasks: 10_resolve_host_cid.yml
- name: "Matomo install | Run downstream install path"
include_tasks: 04_install_path.yml
@@ -30,6 +30,22 @@
- name: flush docker service
meta: flush_handlers
# Exception: in swarm the 'compose up' handler runs `docker stack deploy`, which is a
# no-op when only config.ini.php on the NFS bind changed; the running container keeps
# the pre-patch creds in memory and loops on "Database access denied". Force a service
# restart so the patched config is re-read, then re-resolve the (possibly new) task.
- name: "Matomo install | Swarm: restart matomo so patched config is re-read"
when: DEPLOYMENT_MODE == 'swarm'
block:
- name: "Matomo install | Swarm: force-restart matomo service"
ansible.builtin.command: >-
container service update --force
{{ lookup('container_service', application_id, MATOMO_SERVICE) }}
changed_when: true
- name: "Matomo install | Swarm: re-resolve hosting node + container ID after restart"
include_tasks: 10_resolve_host_cid.yml
- name: "Matomo install | Wait for Matomo container to be healthy"
ansible.builtin.shell: >
container inspect -f '{{ "{{" }}.State.Health.Status{{ "}}" }}'
@@ -0,0 +1,63 @@
---
- name: "Matomo resolve | Swarm host node + CID"
when: DEPLOYMENT_MODE == 'swarm'
block:
- name: "Matomo resolve | Wait for matomo task to converge"
ansible.builtin.shell: |
container service ps {{ lookup('container_service', application_id, MATOMO_SERVICE) | quote }} \
--filter desired-state=running \
--format '{{ "{{.CurrentState}}" }}' \
| head -1 | grep -qE '^Running'
register: matomo_running_swarm
changed_when: false
retries: 600
delay: 2
until: matomo_running_swarm.rc == 0
- name: "Matomo resolve | Resolve hosting node"
ansible.builtin.shell: |
container service ps {{ lookup('container_service', application_id, MATOMO_SERVICE) | quote }} \
--filter desired-state=running \
--format '{{ "{{.Node}}" }}' | head -1
register: matomo_swarm_node_query
changed_when: false
- name: "Matomo resolve | Set MATOMO_HOST_NODE fact"
ansible.builtin.set_fact:
MATOMO_HOST_NODE: "{{ matomo_swarm_node_query.stdout | trim }}"
- name: "Matomo resolve | Compose host node"
when: DEPLOYMENT_MODE != 'swarm'
block:
- name: "Matomo resolve | Wait for matomo container to be running"
ansible.builtin.shell: >
container inspect -f '{{ "{{" }}.State.Running{{ "}}" }}'
{{ lookup('container_address', application_id, MATOMO_SERVICE) }}
register: matomo_running_compose
changed_when: false
retries: 150
delay: 2
until: >
matomo_running_compose.rc == 0 and (matomo_running_compose.stdout | trim) == 'true'
- name: "Matomo resolve | Set MATOMO_HOST_NODE fact"
ansible.builtin.set_fact:
MATOMO_HOST_NODE: "{{ inventory_hostname }}"
- name: "Matomo resolve | Resolve local container ID on hosting node"
ansible.builtin.shell: |
{% if DEPLOYMENT_MODE == 'swarm' %}
container ps \
--filter "label=com.docker.swarm.service.name={{ lookup('container_service', application_id, MATOMO_SERVICE) }}" \
--filter status=running \
--format '{{ "{{.ID}}" }}' | head -1
{% else %}
echo {{ MATOMO_NAME | quote }}
{% endif %}
register: matomo_local_cid_query
changed_when: false
delegate_to: "{{ MATOMO_HOST_NODE }}"
- name: "Matomo resolve | Set MATOMO_LOCAL_CID fact"
ansible.builtin.set_fact:
MATOMO_LOCAL_CID: "{{ matomo_local_cid_query.stdout | trim }}"
@@ -31,6 +31,7 @@ redis:
backup:
disabled: true
enabled: true
shared: "{{ 'svc-db-redis' in group_names }}"
cpus: "1"
mem_reservation: 1g
mem_limit: 8g
@@ -28,6 +28,8 @@
shared: true
mariadb:
shared: true
redis:
shared: true
matomo:
enabled: true
shared: true
@@ -69,6 +71,8 @@
shared: false
mariadb:
shared: false
redis:
shared: false
matomo:
enabled: false
shared: false
@@ -11,6 +11,8 @@
retries: 60
delay: 2
until: redis_ping.stdout is search('PONG')
changed_when: false
when: lookup('engine', 'redis', application_id, 'local')
- name: Update\Upgrade Nextcloud
include_tasks: 03_upgrade.yml
@@ -82,7 +82,7 @@
- "{{ container_port }}"
shm_size: 1g
{{ lookup('container_networks') | indent(4) }}
{{ lookup('depends_on', {'redis': 'service_healthy'}) }}
{% if lookup('engine', 'redis', application_id, 'local') %}{{ lookup('depends_on', {'redis': 'service_healthy'}) }}{% endif %}
{% endif %}
+8 -3
View File
@@ -27,8 +27,13 @@ TRUSTED_PROXIES={{ NEXTCLOUD_TRUSTED_PROXIES | join(',') }}
OVERWRITECLIURL={{ NEXTCLOUD_URL }}
OVERWRITEPROTOCOL={{ NEXTCLOUD_PROTOCOL }}
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_HOST={{ lookup('engine', 'redis', application_id, 'host') }}
REDIS_PORT={{ lookup('engine', 'redis', application_id, 'port') }}
{% set _redis_user = lookup('engine', 'redis', application_id, 'username') %}
{% if _redis_user | length > 0 %}
REDIS_HOST_USER={{ _redis_user }}
{% endif %}
REDIS_HOST_PASSWORD={{ lookup('engine', 'redis', application_id, 'password') | dotenv_quote }}
{% if lookup('objstore', application_id, 'enabled') | bool %}
OBJECTSTORE_S3_BUCKET= {{ lookup('objstore', application_id, 'bucket') }}
@@ -60,7 +65,7 @@ TZ={{ HOST_TIMEZONE }}
NEXTCLOUD_URL={{ NEXTCLOUD_URL }}
JWT_SECRET_KEY={{ NEXTCLOUD_WHITEBOARD_JWT }}
STORAGE_STRATEGY=redis
REDIS_URL=redis://redis:6379/0
REDIS_URL={{ lookup('engine', 'redis', application_id, 'url') }}
CHROMIUM_FLAGS=--headless=new --no-sandbox --disable-gpu --disable-dev-shm-usage --use-gl=swiftshader --disable-software-rasterizer
PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
PUPPETEER_SKIP_DOWNLOAD=true
@@ -32,6 +32,8 @@ javascript:
shared: "{{ lookup('config', 'web-app-prometheus', 'services.sso.enabled') | bool }}"
# nocheck: playwright-service-flag — self-provider
prometheus:
# nocheck: default-placement-manager TSDB on-disk data; node-local in swarm
default_placement: manager
enabled: true
shared: true
provides:
+12 -4
View File
@@ -13,10 +13,13 @@ alertmanager_data:
- service: alertmanager
target: /alertmanager
# Swarm materialises config objects as root:root; the prom/* images run as
# `nobody`, so 0440 (no "other" read bit) makes them unreadable -> exit 2.
# 0444 keeps them read-only while letting the non-root runtime user load them.
prometheus_config:
type: config
source: "{{ [lookup('container', 'web-app-prometheus', 'directories.config'), 'prometheus.yml'] | path_join }}"
mode: "0440"
mode: "0444"
mounts:
- service: prometheus
target: /etc/prometheus/prometheus.yml
@@ -24,7 +27,7 @@ prometheus_config:
alert_rules:
type: config
source: "{{ [lookup('container', 'web-app-prometheus', 'directories.config'), 'alert_rules.yml'] | path_join }}"
mode: "0440"
mode: "0444"
mounts:
- service: prometheus
target: /etc/prometheus/alert_rules.yml
@@ -32,7 +35,7 @@ alert_rules:
blackbox_config:
type: config
source: "{{ [lookup('container', 'web-app-prometheus', 'directories.config'), 'blackbox.yml'] | path_join }}"
mode: "0440"
mode: "0444"
mounts:
- service: blackbox-exporter
target: /etc/blackbox_exporter/config.yml
@@ -40,7 +43,7 @@ blackbox_config:
alertmanager_config:
type: config
source: "{{ [lookup('container', 'web-app-prometheus', 'directories.config'), 'alertmanager.yml'] | path_join }}"
mode: "0440"
mode: "0444"
mounts:
- service: alertmanager
target: /etc/alertmanager/alertmanager.yml
@@ -81,6 +84,10 @@ host_var_lib_docker:
- service: cadvisor
target: /var/lib/docker
# /dev/disk does not exist on swarm DinD nodes, so the bind makes the swarm
# service Reject ("bind source path does not exist"). cadvisor only uses it to
# enrich per-disk stats, which is optional, so gate the mount to compose where
# the host path is real.
host_dev_disk:
type: bind
source: "/dev/disk/"
@@ -88,6 +95,7 @@ host_dev_disk:
mounts:
- service: cadvisor
target: /dev/disk
when: "{{ DEPLOYMENT_MODE != 'swarm' }}"
host_proc:
type: bind
@@ -39,15 +39,18 @@
{% include 'roles/sys-svc-container/templates/base.yml.j2' %}
{{ lookup('container_image', application_id, 'cadvisor') }}
{{ lookup('compose_only', 'container_name', CADVISOR_CONTAINER) }}
{% if DEPLOYMENT_MODE != 'swarm' %}
privileged: true
devices:
- /dev/kmsg
{% endif %}
{{ lookup('container_volumes', application_id, 'cadvisor') | indent(4) }}
node-exporter:
{% include 'roles/sys-svc-container/templates/base.yml.j2' %}
{{ lookup('container_image', application_id, 'node-exporter') }}
{{ lookup('compose_only', 'container_name', NODE_EXPORTER_CONTAINER) }}
{% if DEPLOYMENT_MODE != 'swarm' %}
# network_mode: host is required so node-exporter sees the host's real
# network interfaces, disk mounts, and process table. Without it, interface
# bandwidth metrics reflect the container's virtual stack only and are
@@ -55,10 +58,12 @@
# and memory metrics. Security trade-off: removes Docker network isolation
# for this read-only monitoring container, which is the standard accepted
# practice for node-exporter. Port note: node-exporter binds on host port
# 9100; ensure nothing else on the host uses port 9100.
# 9100; ensure nothing else on the host uses port 9100. Swarm services
# cannot honor network_mode/pid host, so this is compose-only.
# nocheck: network-mode-host-without-swarm-gate
network_mode: host
pid: host
{% endif %}
{{ lookup('container_volumes', application_id, 'node-exporter') | indent(4) }}
command:
- "--path.procfs=/host/proc"