fix(swarm): reach gitea, shopware and fider from inside their containers

In swarm, `docker stack deploy` drops the host IP of a short port mapping,
so a `127.0.0.1:<ports.local>` publish lands on the ingress routing mesh
on every node. The provisioning of these three roles only worked because
the mesh answered on the host:

- web-app-gitea waited for `DOCKER_REACH_HOST:<ports.local.http>` with
  wait_for and polled `/api/v1/version` there with uri_retry.
- web-app-shopware waited for `DOCKER_REACH_HOST:<ports.local.http>`.
- web-app-fider bootstrapped its first tenant in swarm by POSTing to
  `127.0.0.1:<ports.local.http>` on the hosting node.

As long as these calls depend on the mesh, container_ports cannot stop
publishing loopback ports in swarm. The probes now run inside the
service's own container through the resolved `<PREFIX>_LOCAL_CID` on
`<PREFIX>_HOST_NODE`, against the internal port:

- Gitea uses the curl it ships. The first wait accepts any HTTP answer,
  as wait_for did. The post-migration wait still requires 200.
- Shopware runs the wget probe its compose healthcheck already uses.
- Fider ships no HTTP client, so bash writes the tenant POST to
  /dev/tcp. The body comes in on stdin and the status line is checked
  for 200/201.

Fider's compose path is unchanged. The compose render of all three roles
is unchanged. The host-side ingress guard from svc-swarm-node stays in
place until the remaining host-side consumers are moved.

Refs #1089

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5.5 committed 2026-10-29 00:01:20 +01:00
1 parent e8cb250c51
commit c59e2838d6
5 files changed
+44 -28

No files matched your search

+13 -7
View File
@@ -45,15 +45,21 @@
- DEPLOYMENT_MODE == 'compose'
- (fider_tenant_count.rows[0][0] | int) == 0
- name: "🥾 Fider | Bootstrap initial tenant via API (swarm, routing-mesh port)"
uri_retry:
url: "http://127.0.0.1:{{ lookup('config', application_id, 'services.fider.ports.local.http') }}/_api/tenants"
method: POST
body_format: json
body: "{{ FIDER_TENANT_BOOTSTRAP_BODY }}"
status_code: [200, 201]
- name: "🥾 Fider | Bootstrap initial tenant via API (swarm in-container)"
ansible.builtin.shell:
cmd: >- # nocheck: container-exec-resolver runtime-resolved CID, delegated to the hosting node
container exec -i {{ FIDER_LOCAL_CID | quote }} bash -c
'set -e; body=$(cat); length=$(printf %s "$body" | wc -c);
exec 3<>"/dev/tcp/127.0.0.1/$1";
printf "POST /_api/tenants HTTP/1.0\r\nContent-Type: application/json\r\nContent-Length: %d\r\n\r\n%s" "$length" "$body" >&3;
head -n1 <&3 | cut -d" " -f2' _ {{ FIDER_INTERNAL_PORT | quote }}
stdin: "{{ FIDER_TENANT_BOOTSTRAP_BODY | to_json }}"
register: fider_tenant_bootstrap
changed_when: true
failed_when: (fider_tenant_bootstrap.stdout | trim) not in ['200', '201']
retries: 30
delay: 2
until: fider_tenant_bootstrap.rc == 0
delegate_to: "{{ FIDER_HOST_NODE }}"
when:
- DEPLOYMENT_MODE == 'swarm'
+2 -1
View File
@@ -3,6 +3,7 @@
application_id: "web-app-fider"
FIDER_CONTAINER: "{{ lookup('config', application_id, 'services.fider.name') }}"
FIDER_INTERNAL_PORT: "{{ lookup('config', application_id, 'services.fider.ports.internal.http') }}"
FIDER_OIDC_ENABLED: "{{ lookup('config', application_id, 'services.sso.enabled') }}"
@@ -20,7 +21,7 @@ FIDER_MCP_PORT: "{{ lookup('config', application_id, 'services.fidermcp.ports.in
FIDER_MCP_BEARER: "{{ lookup('config', application_id, 'secrets.credentials.mcp_bearer') }}"
FIDER_MCP_INSTANCE_DIR: "{{ [lookup('container', application_id, 'directories.instance'), 'mcp'] | path_join }}"
FIDER_MCP_UPSTREAM_HOST: "{{ lookup('config', application_id, 'services.fider.name') }}"
FIDER_MCP_UPSTREAM_PORT: "{{ lookup('config', application_id, 'services.fider.ports.internal.http') }}"
FIDER_MCP_UPSTREAM_PORT: "{{ FIDER_INTERNAL_PORT }}"
FIDER_MCP_UPSTREAM_URL: "http://{{ FIDER_MCP_UPSTREAM_HOST }}:{{ FIDER_MCP_UPSTREAM_PORT }}"
FIDER_MCP_CONTRACT:
provider: "{{ application_id }}"
+17 -13
View File
@@ -1,11 +1,4 @@
---
- name: ⏳ Wait for Gitea HTTP endpoint
wait_for:
host: "{{ DOCKER_REACH_HOST }}"
port: "{{ lookup('config', application_id, 'services.gitea.ports.local.http') }}"
delay: 5
timeout: "{{ lookup('timeout', 300) | int }}"
- name: "🔍 Resolve {{ application_id }} hosting node + container id"
ansible.builtin.include_tasks: "{{ lookup('path_absolute', 'roles/sys-svc-compose/tasks/utils/swarm/resolve_host_cid.yml') }}"
vars:
@@ -13,6 +6,17 @@
resolve_output_prefix: GITEA
resolve_compose_cid: "{{ GITEA_CONTAINER_ADDRESS }}"
- name: ⏳ Wait for Gitea HTTP endpoint
ansible.builtin.shell: >- # nocheck: container-exec-resolver runtime-resolved CID, delegated to the hosting node
container exec {{ GITEA_LOCAL_CID | quote }}
{{ 10 | curl(retries=0) }} -S -o /dev/null {{ GITEA_LOOPBACK_VERSION_URL | quote }}
delegate_to: "{{ GITEA_HOST_NODE }}"
register: gitea_http_ready
changed_when: false
retries: 60
delay: 5
until: gitea_http_ready.rc == 0
- name: "⏳ Wait for Gitea app.ini to be present after redeploy"
shell: | # nocheck: container-exec-resolver runtime-resolved CID, delegated to the hosting node
container exec -i --user {{ GITEA_USER }} {{ GITEA_LOCAL_CID | quote }} \
@@ -48,13 +52,13 @@
failed_when: create_admin.rc != 0 and 'user already exists' not in create_admin.stderr
- name: "⏳ Wait until Gitea setup and migrations are ready"
uri_retry:
url: "http://{{ DOCKER_REACH_HOST }}:{{ lookup('config', application_id, 'services.gitea.ports.local.http') }}/api/v1/version"
method: GET
status_code: 200
return_content: false
ansible.builtin.shell: >- # nocheck: container-exec-resolver runtime-resolved CID, delegated to the hosting node
container exec {{ GITEA_LOCAL_CID | quote }}
{{ 10 | curl(retries=0) }} -fS -o /dev/null {{ GITEA_LOOPBACK_VERSION_URL | quote }}
delegate_to: "{{ GITEA_HOST_NODE }}"
register: gitea_ready
until: gitea_ready.status == 200
changed_when: false
until: gitea_ready.rc == 0
retries: 20
delay: 5
when: (GITEA_OIDC_ENABLED | bool) or (GITEA_LDAP_ENABLED | bool)
+1
View File
@@ -22,6 +22,7 @@ GITEA_CONTAINER: "{{ lookup('config', application_id, 'services.gitea.name') }}"
GITEA_CONTAINER_ADDRESS: "{{ lookup('container_address', application_id, entity_name) }}"
GITEA_USER: "git"
GITEA_CONFIG: "/data/gitea/conf/app.ini"
GITEA_LOOPBACK_VERSION_URL: "http://127.0.0.1:{{ lookup('config', application_id, 'services.gitea.ports.internal.http') }}/api/v1/version"
GITEA_REDIS_ENABLED: "{{ lookup('config', application_id, 'services.redis.enabled') }}"
GITEA_REDIS_ADDRESS: "{{ lookup('engine', 'redis', application_id, 'host') }}:{{ lookup('engine', 'redis', application_id, 'port') }}"
@@ -37,13 +37,6 @@
- name: "🚿 Flush compose handlers"
meta: flush_handlers
- name: ⏳ Wait for Shopware HTTP endpoint
wait_for:
host: "{{ DOCKER_REACH_HOST }}"
port: "{{ lookup('config', application_id, 'services.shopware.ports.local.http') }}"
delay: 5
timeout: "{{ lookup('timeout', 300) | int }}"
- name: "🔍 Resolve Shopware web hosting node + container id"
ansible.builtin.include_tasks: "{{ lookup('path_absolute', 'roles/sys-svc-compose/tasks/utils/swarm/resolve_host_cid.yml') }}"
vars:
@@ -53,6 +46,17 @@
resolve_converge_retries: 120
resolve_converge_delay: 5
- name: ⏳ Wait for Shopware HTTP endpoint
ansible.builtin.shell: >- # nocheck: container-exec-resolver runtime-resolved CID, delegated to the hosting node
container exec {{ SHOPWARE_WEB_LOCAL_CID | quote }}
sh -c 'wget -q --spider "$1/robots.txt" || wget -q --spider "$1/"' _ {{ SHOPWARE_API_BASE | quote }}
delegate_to: "{{ SHOPWARE_WEB_HOST_NODE }}"
register: shopware_http_ready
changed_when: false
retries: 60
delay: 5
until: shopware_http_ready.rc == 0
- name: "✅ Ensure admin user exists with correct password"
ansible.builtin.include_tasks:
file: 01_admin.yml