Commit Graph
14682 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 4.8 59de61ec4f fix(suitecrm): harden the boot mutex against killed leaders and root-owned caches
Three hardenings of the mkdir boot lock: reclaim a stale lock (>=1800s dir mtime, beyond the orchestrator kill ceiling of start_period 15m plus retries) so a leader killed mid-install no longer deadlocks every future boot; check staleness BEFORE the install-flag break so a lock leaked after the flag was written (kill during cache:warmup) also heals instead of forcing the loser path forever; release the lock on TERM/INT. After install/cache (which run as root) chown the Symfony and legacy cache trees to www-data: apache regenerates wiped legacy language caches lazily and cannot write root-owned dirs, which served permanent 500s (live-verified: chown turned GET / from 500 to 200).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 02:37:26 +02:00
kevinveenbirkenbachandClaude Opus 4.8 fbe50d10ed fix(svc-runner): make the runner role swarm-compatible
Workers failed 'docker build: no Dockerfile': the build context, compose file, .env and runner binary were staged on the stack host only while every node deploys its own node-local runner stack. Build and push the image once on the manager (RUNNER_IMAGE_REF via the swarm registry, 04_image.yml) and let workers pull it; render compose.yml (template with lstrip_blocks, matching the replaced stack_host_template action), .env and the extracted run.sh on every node. Add a node_local kwarg to the compose_networks/container_networks/compose_restart/compose_only lookups so this node-local compose stack renders a bridge default network (workers cannot create overlays), keeps restart: unless-stopped (ephemeral runners died after one job) and keeps container_name under swarm; default is unchanged for every other role. Treat build/install.stamp as stale when the venv is missing and exclude ./build from the nested-src copy, closing the nested-e2e ruamel failure class. Regression tests for the node_local render paths; the network include tests now accept lookup kwargs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 02:37:26 +02:00
kevinveenbirkenbach 60aee40185 Merge pull request #391 from infinito-nexus/update/docker-image-versions-web-app-baserow-20260708-2519565
update(images): update Docker image versions (web-app-baserow)
2026-07-08 02:36:35 +02:00
kevinveenbirkenbach 0dd895aafc Merge pull request #393 from infinito-nexus/update/repository-refs-web-app-bluesky-20260708-ec34b8b
update(repos): bump git refs to latest semver tags (web-app-bluesky)
2026-07-08 02:36:11 +02:00
kevinveenbirkenbach 03c4803092 Merge pull request #392 from infinito-nexus/update/docker-image-versions-web-app-semaphore-20260708-8e70481
update(images): update Docker image versions (web-app-semaphore)
2026-07-08 02:35:48 +02:00
infinito-nexus-updater[bot] 82505f833e update(repos): bump git refs to latest semver tags (web-app-bluesky) 2026-07-08 00:28:48 +00:00
infinito-nexus-updater[bot] 5ca11d2380 update(images): update Docker image versions (web-app-semaphore) 2026-07-08 00:27:12 +00:00
infinito-nexus-updater[bot] 4a12c96083 update(images): update Docker image versions (web-app-baserow) 2026-07-08 00:27:07 +00:00
kevinveenbirkenbach ab920c1365 Merge pull request #343 from pragepani/feature/web-app-n8n/add-role
feat(web-app-n8n): add n8n workflow automation role
2026-07-08 01:45:05 +02:00
kevinveenbirkenbach aac894db7f Merge pull request #390 from infinito-nexus/update/docker-image-versions-web-app-pihole-20260707-f87f5f3
update(images): update Docker image versions (web-app-pihole)
2026-07-08 01:39:20 +02:00
infinito-nexus-updater[bot] 6f8386dffb update(images): update Docker image versions (web-app-pihole) 2026-07-07 14:40:19 +00:00
kevinveenbirkenbach 54295cf7a6 Merge pull request #388 from infinito-nexus/update/docker-image-versions-web-app-pihole-20260706-f3ff108
update(images): update Docker image versions (web-app-pihole)
2026-07-07 16:03:02 +02:00
kevinveenbirkenbach 141557b03b Merge branch 'main' into update/docker-image-versions-web-app-pihole-20260706-f3ff108 2026-07-07 16:02:47 +02:00
kevinveenbirkenbach 7ae8850739 Merge pull request #387 from infinito-nexus/update/docker-image-versions-web-app-pihole-20260706-c18e2d5
update(images): update Docker image versions (web-app-pihole)
2026-07-07 16:00:26 +02:00
Prageeth Panicker c59d9daf7b fix(web-app-n8n): resolve subnet collision with web-app-semaphore
192.168.105.208/28 -> 192.168.106.16/28
2026-07-07 13:14:45 +00:00
Prageeth Panicker 48032e3eb9 fix(web-app-n8n): resolve port collisions with new roles
- http port: 8086 -> 8080 (conflict with web-app-semaphore)
- sso port: 16509 -> 16510 (conflict with web-app-checkmk)
2026-07-07 13:01:04 +00:00
Prageeth Panicker 2b831f56d1 fix(web-app-n8n): address Kevin's review comments
- tasks/02_bootstrap.yml: set retries=4 delay=30 on owner probe to
  stay within n8n's 5-attempt/5-minute rate limit window
- files/playwright: remove test-oidc-login.js - redundant with
  test-login-administrator.js which already covers the full OIDC flow
- rebase onto latest main
2026-07-07 12:53:39 +00:00
Prageeth Panicker c4a73d61d7 fix(web-app-n8n): gate hooks.js copy and mount on SSO being enabled
- tasks/01_core.yml: add when: N8N_SSO_ENABLED | bool to hooks.js copy task
- templates/compose.yml.j2: wrap hooks.js volume mount in SSO conditional

Per Kevin's review - hooks.js should only be staged and mounted
when SSO is actually enabled, adding extra redundancy.
2026-07-07 12:51:06 +00:00
Prageeth Panicker 167bbe9e24 fix(web-app-n8n): fix hooks.js for n8n 1.95.3 API
Remove relations: ['role'] from UserRepo.findOne() - in 1.95.3
user.role is a plain string column, not a relation. The relations
array caused EntityPropertyNotFoundError which produced 500 on
every SSO login attempt. Also removed dead RoleRepo/dbCollections.Role
references dropped in 1.95.3's DropRoleTable migration.

Verified: biber@infinito.example now auto-provisioned as global:member
after Keycloak SSO. All 5 Playwright tests pass locally.
2026-07-07 12:51:06 +00:00
Prageeth Panicker 31f3434d66 feat(web-app-n8n): implement community SSO via trusted-header hook
- Add files/hooks.js (adapted from PavelSozonov/n8n-community-sso)
  Auto-provisions users and issues n8n session from Remote-Email header
- Add templates/proxy.conf.j2 - openresty vhost forwarding Remote-Email
  trusted header in SSO-gated branch
- Update env.j2: EXTERNAL_HOOK_FILES + N8N_FORWARD_AUTH_HEADER when SSO enabled
- Update compose.yml.j2: bind-mount hooks.js read-only into container
- Update tasks/01_core.yml: stage hooks.js before container starts
- Fix Playwright: V1 SSO tests now assert direct workflow editor landing
- Fix Playwright: administrator test works in both V1 (SSO) and V2 (no SSO)
- Update README: document auto-provisioning SSO behavior
2026-07-07 12:51:06 +00:00
Prageeth Panicker 58ce233f97 fix(web-app-n8n): fix Playwright tests for CE edge-SSO model
- Remove LDAP variant (V3) - unsupported on n8n Community Edition
- Fix test-login-administrator: Keycloak SSO + n8n native login form
- Fix test-login-biber: verify oauth2-proxy gate only (no n8n account)
- Add performN8nLoginForm() helper to _shared.js
- Add ADMIN_EMAIL and N8N_OWNER_PASSWORD to playwright.env.j2
- Update README to clarify edge-SSO vs native n8n session
2026-07-07 12:51:06 +00:00
Prageeth Panicker f2388849e9 fix(web-app-n8n): fix LDAP sign-in button selector and related fixes
- Fix button selector in _shared.js: button[type=submit] → getByRole('button', {name: /sign in/i})
- Update test-login-via-ldap.js accordingly
- Fix 03_ldap.yml LDAP configuration task
- Update playwright.env.j2
2026-07-07 12:51:06 +00:00
Prageeth Panicker faa8b6b3ee fix(ci): resolve 4 CI failures across n8n, pihole, and mastodon roles
- Add bond:1 to all 9 enabled+shared services in web-app-n8n/meta/services.yml
- Fix web-app-n8n/meta/variants.yml V3 postgres shared:false → shared:true (only V2 is standalone)
- Remove dead PIHOLE_SSO_PROXY_CA_CONTAINER_PATH var from web-app-pihole/vars/main.yml
- Add nocheck:url marker for dead link in web-app-mastodon/README.md
- Fix LDAP sign-in button selector in _shared.js (button[type=submit] → getByRole)
2026-07-07 12:51:06 +00:00
Prageeth Panicker 8eabd71cea fix(web-app-n8n): add 405 to status_codes.default to skip puppeteer CSP probe
n8n is SSO-gated and redirects unauthenticated requests cross-origin to
Keycloak, causing ERR_ABORTED (405) in the headless Chromium probe. Adding
405 opts out of the probe, matching the web-app-prometheus pattern.
2026-07-07 12:51:06 +00:00
Prageeth PanickerandClaude Sonnet 4.6 2cb5f902fc style(web-app-n8n): fix MD031 lint error in README (blank lines around code block)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 12:51:06 +00:00
Prageeth PanickerandClaude Sonnet 4.6 6cf7769997 fix(web-app-n8n): address review comments — info, health check, CA trust, docs
1. meta/info.yml: add homepage and video fields.
2. meta/server.yml: add status_codes.default [200, 302] so the CSP/webserver
   health check accepts the oauth2-proxy redirect response.
3. Move self-signed CA trust for oauth2-proxy into the shared
   oauth2-proxy-keycloak.cfg.j2 template (provider_ca_files, conditional on
   TLS_MODE=self_signed). This fixes the root cause for all oauth2-proxy roles
   (including web-app-pihole) without role-local lineinfile hacks.
4. Drop compose_handlers_flush: false + explicit meta: flush_handlers from
   both web-app-n8n and web-app-pihole — now that no post-render file surgery
   is needed, compose_handlers_flush: true lets sys-stk-backend flush normally.
5. Remove the now-unused N8N_SSO_PROXY_CA_CONTAINER_PATH and
   PIHOLE_SSO_PROXY_CA_CONTAINER_PATH role variables.
6. README: document the V2 (no-auth) native administrator login flow.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 12:51:06 +00:00
Prageeth PanickerandClaude Sonnet 4.6 22d0432a91 fix(web-app-n8n): suppress seaweedfs-required CI check
n8n persists all data in postgres, not object storage.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 12:51:06 +00:00
Prageeth PanickerandClaude Sonnet 4.6 e3591fc526 fix(web-app-n8n): fix 3 CI issues in n8n role
- tasks/02_bootstrap.yml: replace legacy uri: with uri_retry: (platform convention)
- README.md: fix credits author to canonical Kevin Veen-Birkenbach
- vars/main.yml: remove N8N_EMAIL_ENABLED (declared but never referenced; env.j2 reads email config directly via lookup)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 12:51:06 +00:00
Prageeth Panicker 8332c7edf0 feat(web-app-n8n): add n8n workflow automation role
- Community edition with edge oauth2-proxy SSO (Decision #1b)
- PostgreSQL backend via svc-db-postgres
- Three variants: V1 sso+ldap, V2 no-auth, V3 ldap-only
- Bootstrap owner account via /api/v1/owner/setup
- LDAP config via direct PostgreSQL upsert (CE API limitation)
- Playwright: baseline, CSP, OIDC, administrator and biber personas
- All 5 active tests pass, 2 skipped correctly for this variant
2026-07-07 12:51:06 +00:00
kevinveenbirkenbachandClaude Opus 4.8 e191b91de0 style: apply make autoformat (whitespace + quote/line normalization)
make autoformat output across role meta files, a task file and 19_tor.yml (single->double quotes on mode values, Jinja line reflow). No semantic changes. Kept separate from the onion CSP / oauth2-proxy fixes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:25:44 +02:00
kevinveenbirkenbachandClaude Opus 4.8 4e86513fbc fix(svc-net-tor): declare PORT_BANDS for the Tor TransPort/DNSPort
The egress-router trans (9040) and dns (9053) ports had no PORT_BANDS entry and failed the port-band-membership lint. Add the local.trans and local.dns bands next to socks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:25:44 +02:00
kevinveenbirkenbachandClaude Opus 4.8 954da6d180 fix(tor): onion-aware CSP and oauth2-proxy (exclusive vs primary)
Over Tor a page served on an .onion still advertised a clearnet-only CSP and oauth2-proxy config, so onion origins were blocked (dashboard could not frame auth.<onion>; oauth2-proxy rejected the redirect back to the onion app). build_csp_header now mirrors the clearnet primary to the node onion per token; the keycloak oauth2-proxy config derives whitelist_domains and cookie_domains from the app resolved domains. Exclusive (onion-only) apps get onion tokens only, primary apps get both, non-tor apps stay clearnet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:25:44 +02:00
kevinveenbirkenbachandClaude Opus 4.8 c09eb61dc6 style: drop explanatory comments from the Tor onion changes
Remove the inline/trailing comments added across the Tor egress-router, onion-domain, mailu-notls and vault round-trip changes, per the repo no-comments convention. Comment-only: no code behaviour changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:25:44 +02:00
kevinveenbirkenbachandClaude Opus 4.8 40c5727684 style: apply make autoformat (whitespace only)
Indentation/whitespace normalization from make autoformat. No semantic changes (git diff --ignore-all-space empty for every file). Kept separate from the onion CSP / oauth2-proxy fixes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:25:43 +02:00
kevinveenbirkenbachandClaude Opus 4.8 32daff8773 fix(svc-net-tor): idempotent egress-router upstream capture
The upstream-capture copy re-read /etc/resolv.conf on every run and filtered out 127.0.0.1. Once the router has pointed the resolver at dnsmasq (127.0.0.1), that filter yields an empty list, so the copy failed with "src (or content) is required" and aborted the deploy on re-runs. Guard the resolver read and the persist copy on the persisted upstreams file being absent (via a stat), so the capture runs only on first setup and later runs reuse the persisted upstreams.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 11:24:56 +02:00
kevinveenbirkenbachandClaude Opus 4.8 e7d987b7c5 fix(email): relay plaintext to onion mail hosts (notls, no cert)
The email lookups tls default keyed on DOMAIN_PRIMARY, which can stay clearnet while Mailus canonical domain is turned into a .onion by the Tor onion injection. TLS therefore stayed on, but Mailu has no public certificate for the .onion (LE cannot sign it) and disabled TLS, so the SMTP handshake to the local front failed and the msmtp health check errored. Key the .onion detection on the resolved mail host instead (host is now resolved before tls in RESOLUTION_ORDER); an onion mail host relays in plaintext, which Tor already authenticates and encrypts end-to-end.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 11:24:56 +02:00
kevinveenbirkenbachandClaude Opus 4.8 fac2de5fd3 style: apply make autoformat (whitespace only)
Indentation/whitespace normalization from make autoformat across role meta files. No semantic changes (git diff --ignore-all-space is empty for every file). Kept separate from the mailu notls-over-onion and Tor-router idempotency fixes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 11:24:55 +02:00
kevinveenbirkenbach 738434a096 Merge pull request #389 from infinito-nexus/dependabot/npm_and_yarn/eslint-plugin-playwright-2.10.5
chore(deps-dev): bump eslint-plugin-playwright from 2.10.4 to 2.10.5
2026-07-07 08:36:27 +02:00
kevinveenbirkenbachandClaude Opus 4.8 62ffb10d8e feat(svc-net-tor): host-level transparent Tor egress router for .onion
Route any outbound TCP to a *.onion address through Tor, protocol-agnostically (HTTP, SMTP, IMAP, ...), while clearnet traffic resolves normally and goes direct. Implemented on the target host and gated on TOR_EGRESS_ROUTER_ENABLED: torrc gains AutomapHostsOnResolve + AutomapHostsSuffixes .onion + VirtualAddrNetworkIPv4 + TransPort/DNSPort; a dnsmasq drop-in split-resolves *.onion via the DNSPort (clearnet upstreams embedded inline, since an AppArmor-confined dnsmasq may only read /etc/dnsmasq.d/**) and the host resolver points at it; an iptables nat OUTPUT rule redirects the automapped virtual range to TransPort. Verified in-container: http://<app>.<node-onion>/ returns 200 with no SOCKS wrapper, clearnet DNS + HTTPS stay direct.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 08:22:01 +02:00
kevinveenbirkenbachandClaude Opus 4.8 c2fa2da88e style: apply make autoformat (whitespace only)
Indentation/whitespace normalization emitted by make autoformat across role meta/services.yml, meta/variants.yml and a task file. No semantic changes (verified: git diff --ignore-all-space is empty for every file). Kept separate from the Tor egress-router feature.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 08:22:01 +02:00
kevinveenbirkenbachandClaude Opus 4.8 713eb6a1eb fix(env): keep tor-socks-port handler stdlib-only for .env bootstrap
The handler imported utils.cache.yaml (PyYAML) at module load, which broke python -m cli.meta.env in the stdlib-only .env bootstrap on fresh distro images (manjaro CI failed with ModuleNotFoundError: No module named yaml). Import lazily inside apply() and skip when PyYAML is absent; the SOCKS port is re-set on any later build that has PyYAML.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 08:22:01 +02:00
kevinveenbirkenbachandClaude Opus 4.8 21d76f922f fix(vault): preserve !vault tag on cache YAML round-trip
The !vault YAML constructor returned a plain str, so round-tripping a host_vars
file through utils.cache.yaml (e.g. apply_tor_node_onion writing
services.tor.node) re-emitted every credential as an untagged quoted string.
Ansible then never decrypted them and LDAP/DB admin binds failed with result 49.
Return a VaultScalar so SafeDumper re-emits the !vault literal block; add a
regression test asserting apply_tor_node_onion keeps existing vault credentials
tagged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 08:22:00 +02:00
dependabot[bot] f4f0d13014 chore(deps-dev): bump eslint-plugin-playwright from 2.10.4 to 2.10.5
Bumps [eslint-plugin-playwright](https://github.com/mskelton/eslint-plugin-playwright) from 2.10.4 to 2.10.5.
- [Release notes](https://github.com/mskelton/eslint-plugin-playwright/releases)
- [Changelog](https://github.com/mskelton/eslint-plugin-playwright/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mskelton/eslint-plugin-playwright/compare/v2.10.4...v2.10.5)

---
updated-dependencies:
- dependency-name: eslint-plugin-playwright
  dependency-version: 2.10.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-07 00:55:02 +00:00
kevinveenbirkenbachandClaude Opus 4.8 a0bc739537 chore(claude): allow WebFetch developer.shopware.com
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 02:25:30 +02:00
kevinveenbirkenbachandClaude Opus 4.8 91072457b0 refactor(mediawiki): hoist delegate_to to the 02_extensions include site
Every task in 02_extensions.yml repeated delegate_to: MEDIAWIKI_HOST_NODE. Move it to the include_tasks apply: block and drop the per-task lines, satisfying the include-uniform-apply-keyword lint. Functionally identical (apply propagates the keyword to every included task).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 02:25:08 +02:00
kevinveenbirkenbachandClaude Opus 4.8 415d5c3c4f chore(espocrm): justify the single-replica pin and note the scale-up path
espocrm stays at replicas:1: making it multi-replica needs a control-flow rewrite of its entrypoint (run the image init under a flock via /bin/true so it returns, then own the server launch) plus relocating PHP sessions to the NFS-shared volume. That restructures a currently-working, compose-green deploy and is coupled to the pinned 9.3.10 image's exec behavior, so it could not be verified to the required confidence without a live swarm loop. Add the single-replica nocheck marker and a TODO capturing the path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 02:23:52 +02:00
kevinveenbirkenbachandClaude Opus 4.8 4e005841da fix(shopware): wire redis sessions so web/worker can scale, pin singleton sidecars
cache and messenger already use the mounted REDIS_URL, but the framework.yaml omitted a session handler, so sessions were node-local files and logins/carts broke behind the swarm VIP. Set framework.session.handler_id to the existing REDIS_URL so sessions are shared, and drop the entity replicas:1 pin so web and worker default to the topology count. Re-pin the genuine singletons: init (one-shot install/migrate), scheduler (double-dispatches on >1 poller) and opensearch (discovery.type=single-node). Compose is byte-identical (replicas ignored). Confidence ~94% (session cross-replica reasoned, not live-verified).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 02:23:40 +02:00
kevinveenbirkenbachandClaude Opus 4.8 07ede9b549 fix(suitecrm): scale to N replicas, flock the first-boot install and share sessions
suitecrm_data is NFS-shared and the datastore is external MariaDB, so replicas share /var/www/html, uploads and cache. Two hazards remained: N replicas racing the first-boot suitecrm:app:install, and node-local PHP file sessions. Wrap the installer + cache pass in a flock on the shared volume so exactly one replica installs while the others block then skip, and pin session.save_path to /var/www/html/tmp on the shared volume so sessions are cluster-wide. flock is a no-op with a single replica, so compose is unchanged. Confidence ~95%.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 02:23:24 +02:00
kevinveenbirkenbachandClaude Opus 4.8 901931fb56 fix(odoo): scale to N replicas, serialize first-boot init with a pg advisory lock
Sessions are redis, the filestore /var/lib/odoo is on the NFS-shared odoo_data volume, and cron serializes on Postgres advisory locks, so the only multi-replica hazard was the entrypoint's check-then-init TOCTOU: two replicas booting against an empty DB could both run odoo --init. Replace _schema_present with _guarded_init, which holds a database-scoped pg_advisory_lock across the schema check and the init in one connection (auto-released if the process dies). Uncontended (compose/single-node) it is a no-op, so behavior is unchanged there. Confidence ~95%.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 02:23:01 +02:00
kevinveenbirkenbachandClaude Opus 4.8 ffd8ef2f93 refactor(pixelfed): scale the web tier, keep the worker a singleton
SESSION/CACHE/QUEUE drivers are redis, the first-boot migration runs once via Ansible with migrate --force --isolated (redis lock), and pixelfed_data (passport keys + init marker) is on the NFS-shared named volume, so the web service is safe at N replicas. Drop the entity replicas:1 pin but keep the worker at replicas:1 because Horizon runs the Laravel scheduler and >1 would double-dispatch scheduled jobs. Confidence ~95%.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 02:22:50 +02:00