From 066254e5c4a0fe3599282d720275d015984c63cf Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 25 Sep 2026 13:39:16 +0200 Subject: [PATCH 01/11] nextcloud: pin an image whose PHP matches this Nextcloud version The pinned digest resolved to a PHP 8.5.10 image; Nextcloud 30 refuses to run above PHP 8.4. Repinned to the current digest for the nextcloud:30 tag (matches HAL's own NEXTCLOUD_VERSION), which carries PHP 8.3.28 -- the same version the data being migrated was actually running under. --- modules/nextcloud/module.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/nextcloud/module.json b/modules/nextcloud/module.json index 05a982b..b667c20 100644 --- a/modules/nextcloud/module.json +++ b/modules/nextcloud/module.json @@ -78,7 +78,7 @@ "id": "server", "type": "container", "name": "nextcloud", - "image": "nextcloud@sha256:0b8261f6335af6b95264ce893b4d645857638e0fa151b5ba620f25f377318ae1", + "image": "nextcloud@sha256:fb966733647ea03f0446b0c22eac9733c8eb616d37b960caca9d4c3010e14a08", "env-file": [ "/var/lib/nextcloud-module/server.env" ], -- 2.54.0 From 4329ca939209d7fb294c53cb8797235b8dc6562e Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 25 Sep 2026 13:41:17 +0200 Subject: [PATCH 02/11] nextcloud: deliver the admin password to the sidecar too The sidecar's own client needs MESH_NEXTCLOUD_ADMIN_PASSWORD to list shares over the OCS API, but the runtime container's env/volumes never carried it -- only the server container did. Delivered the same way every other sealed value in this manifest already is: a generated env-file with the ${secret:admin} substitution, not a raw value in the container's env. --- modules/nextcloud/module.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/modules/nextcloud/module.json b/modules/nextcloud/module.json index b667c20..9eb683e 100644 --- a/modules/nextcloud/module.json +++ b/modules/nextcloud/module.json @@ -98,6 +98,13 @@ "content": "{}\n", "merge": "json" }, + { + "id": "runtime-admin-env", + "type": "file", + "path": "/var/lib/mesh/nextcloud/admin.env", + "mode": "0600", + "content": "MESH_NEXTCLOUD_ADMIN_PASSWORD=${secret:admin}\n" + }, { "id": "runtime", "type": "container", @@ -108,6 +115,9 @@ "/var/lib/mesh/nextcloud/config.json:/run/config/config.json:ro", "/var/run/docker.sock:/var/run/docker.sock" ], + "env-file": [ + "/var/lib/mesh/nextcloud/admin.env" + ], "env": { "MESH_BROKER_FILE": "/run/secrets/broker", "MESH_NEXTCLOUD_URL": "http://127.0.0.1:80", -- 2.54.0 From 5f74c41a3e263a4b561e587d67032cbb0c6b5766 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 25 Sep 2026 13:42:21 +0200 Subject: [PATCH 03/11] nextcloud: give the admin password a real _FILE variant, not an env-file The mesh's own check caught it: an env-file-loaded secret still reaches the process environment, readable via docker inspect and /proc (hq 04-ISSUES/041) -- the same class of exposure the file-based delivery exists to avoid. Added MESH_NEXTCLOUD_ADMIN_PASSWORD_FILE support to the client, matching the pattern the minio client already uses, and mounted the sealed admin secret directly rather than writing it into an env-file. --- modules/nextcloud/client.ts | 9 +++++++-- modules/nextcloud/module.json | 14 +++----------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/modules/nextcloud/client.ts b/modules/nextcloud/client.ts index 3ee0ce4..e93d9c7 100644 --- a/modules/nextcloud/client.ts +++ b/modules/nextcloud/client.ts @@ -52,8 +52,13 @@ export class NextcloudClient { const container = cfg.container ?? env.MESH_NEXTCLOUD_CONTAINER ?? "nextcloud"; const ocsUrl = cfg.url ?? env.MESH_NEXTCLOUD_URL ?? `http://127.0.0.1:${env.NEXTCLOUD_PORT ?? "80"}`; const adminUser = cfg.user ?? env.MESH_NEXTCLOUD_ADMIN_USER ?? "admin"; - const adminPassword = cfg.password ?? env.MESH_NEXTCLOUD_ADMIN_PASSWORD; - if (!adminPassword) throw new Error("no Nextcloud admin password — set MESH_NEXTCLOUD_ADMIN_PASSWORD"); + const passwordFile = env.MESH_NEXTCLOUD_ADMIN_PASSWORD_FILE; + const adminPassword = cfg.password ?? env.MESH_NEXTCLOUD_ADMIN_PASSWORD + ?? (passwordFile ? readFileSync(passwordFile, "utf8").trim() : undefined); + if (!adminPassword) { + throw new Error("no Nextcloud admin password — set MESH_NEXTCLOUD_ADMIN_PASSWORD_FILE " + + "(or MESH_NEXTCLOUD_ADMIN_PASSWORD)"); + } return new NextcloudClient(container, ocsUrl.replace(/\/$/, ""), adminUser, adminPassword); } diff --git a/modules/nextcloud/module.json b/modules/nextcloud/module.json index 9eb683e..7c8103b 100644 --- a/modules/nextcloud/module.json +++ b/modules/nextcloud/module.json @@ -98,13 +98,6 @@ "content": "{}\n", "merge": "json" }, - { - "id": "runtime-admin-env", - "type": "file", - "path": "/var/lib/mesh/nextcloud/admin.env", - "mode": "0600", - "content": "MESH_NEXTCLOUD_ADMIN_PASSWORD=${secret:admin}\n" - }, { "id": "runtime", "type": "container", @@ -113,15 +106,14 @@ "volumes": [ "/var/lib/mesh/nextcloud/broker:/run/secrets/broker:ro", "/var/lib/mesh/nextcloud/config.json:/run/config/config.json:ro", + "/var/lib/nextcloud-module/admin.secret:/run/secrets/admin:ro", "/var/run/docker.sock:/var/run/docker.sock" ], - "env-file": [ - "/var/lib/mesh/nextcloud/admin.env" - ], "env": { "MESH_BROKER_FILE": "/run/secrets/broker", "MESH_NEXTCLOUD_URL": "http://127.0.0.1:80", - "MESH_NEXTCLOUD_CONFIG_FILE": "/run/config/config.json" + "MESH_NEXTCLOUD_CONFIG_FILE": "/run/config/config.json", + "MESH_NEXTCLOUD_ADMIN_PASSWORD_FILE": "/run/secrets/admin" }, "restart-on": [ "runtime-config" -- 2.54.0 From e2b3723dd9ed7994edbf2f01d09967d296e75d04 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 25 Sep 2026 13:47:03 +0200 Subject: [PATCH 04/11] nextcloud: fix hardcoded sidecar port and missing docker CLI in runtime image - MESH_NEXTCLOUD_URL hardcoded :80 instead of the mesh-assigned ${port:80} - sidecar's occ() shells to docker exec but the docker CLI binary was never present in the runtime image, only the mounted socket --- modules/nextcloud/Dockerfile | 5 +++++ modules/nextcloud/module.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/modules/nextcloud/Dockerfile b/modules/nextcloud/Dockerfile index 5a6e5a8..0d4460e 100644 --- a/modules/nextcloud/Dockerfile +++ b/modules/nextcloud/Dockerfile @@ -22,6 +22,11 @@ RUN node /app/node_modules/typescript/bin/tsc client.ts index.ts tools/index.ts FROM ${RUNTIME_BASE} COPY --from=build /app/modules/nextcloud/dist /app/modules/nextcloud/dist +# occ runs inside nextcloud's own container, reached over the mounted docker socket — which needs +# the docker CLI itself present here, not only the socket. Copied from Docker's own official client +# image rather than apt-installed, so this stays the one binary and nothing else (no daemon, no +# systemd unit, no package manager tree pulled in for it). +COPY --from=docker:cli /usr/local/bin/docker /usr/local/bin/docker # Every serve-time entrypoint, loaded by the runtime in serve mode: tools and events serve, and a # provider's provisioner runs its reconcile loop in the same process, with the broker connected — # the convention novox/hq issues 060/061 settled. A container that instead ran only its diff --git a/modules/nextcloud/module.json b/modules/nextcloud/module.json index 7c8103b..a2e7da9 100644 --- a/modules/nextcloud/module.json +++ b/modules/nextcloud/module.json @@ -111,7 +111,7 @@ ], "env": { "MESH_BROKER_FILE": "/run/secrets/broker", - "MESH_NEXTCLOUD_URL": "http://127.0.0.1:80", + "MESH_NEXTCLOUD_URL": "http://127.0.0.1:${port:80}", "MESH_NEXTCLOUD_CONFIG_FILE": "/run/config/config.json", "MESH_NEXTCLOUD_ADMIN_PASSWORD_FILE": "/run/secrets/admin" }, -- 2.54.0 From cdbc850c52a6f30cea173fd016d8c16356319b82 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 25 Sep 2026 13:48:00 +0200 Subject: [PATCH 05/11] nextcloud: declare docker:cli as a pinned build.on base build refused to reach docker:cli implicitly (novox/hq ADR 0097); pin it by digest and thread it through as DOCKER_CLI, redeclared in the final stage since args declared before the first FROM don't carry past it --- modules/nextcloud/Dockerfile | 6 +++++- modules/nextcloud/module.json | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/modules/nextcloud/Dockerfile b/modules/nextcloud/Dockerfile index 0d4460e..56b9607 100644 --- a/modules/nextcloud/Dockerfile +++ b/modules/nextcloud/Dockerfile @@ -9,6 +9,7 @@ # image it RUNS in — the second must not carry a compiler. Declared in module.json's `build.on`. ARG BUILD_BASE ARG RUNTIME_BASE +ARG DOCKER_CLI FROM ${BUILD_BASE} AS build # Compiled under /app/modules so `@novox/mesh-sdk` resolves upward into the base's own @@ -21,12 +22,15 @@ RUN node /app/node_modules/typescript/bin/tsc client.ts index.ts tools/index.ts --module NodeNext --moduleResolution NodeNext --target ES2022 --outDir dist FROM ${RUNTIME_BASE} +# ARGs declared before the first FROM are out of scope past it; redeclared here so COPY --from +# below can see it. +ARG DOCKER_CLI COPY --from=build /app/modules/nextcloud/dist /app/modules/nextcloud/dist # occ runs inside nextcloud's own container, reached over the mounted docker socket — which needs # the docker CLI itself present here, not only the socket. Copied from Docker's own official client # image rather than apt-installed, so this stays the one binary and nothing else (no daemon, no # systemd unit, no package manager tree pulled in for it). -COPY --from=docker:cli /usr/local/bin/docker /usr/local/bin/docker +COPY --from=${DOCKER_CLI} /usr/local/bin/docker /usr/local/bin/docker # Every serve-time entrypoint, loaded by the runtime in serve mode: tools and events serve, and a # provider's provisioner runs its reconcile loop in the same process, with the broker connected — # the convention novox/hq issues 060/061 settled. A container that instead ran only its diff --git a/modules/nextcloud/module.json b/modules/nextcloud/module.json index a2e7da9..d24f1c2 100644 --- a/modules/nextcloud/module.json +++ b/modules/nextcloud/module.json @@ -132,6 +132,10 @@ "arg": "RUNTIME_BASE", "module": "mesh-tools", "artifact": "runtime" + }, + { + "arg": "DOCKER_CLI", + "image": "docker@sha256:018edbc908e08fcc9dbf029c812c34251e9b4719e6f71ca0e5eae2a987d014ca" } ], "artifacts": [ -- 2.54.0 From b865978e19ad9046d03b69a9c387b72abcc41761 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 25 Sep 2026 13:49:02 +0200 Subject: [PATCH 06/11] nextcloud: use a named build stage for docker:cli, not ARG-in-COPY-from MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit the legacy (non-BuildKit) docker build this host runs doesn't expand ARGs inside COPY --from — only FROM. Give it its own named stage instead --- modules/nextcloud/Dockerfile | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/modules/nextcloud/Dockerfile b/modules/nextcloud/Dockerfile index 56b9607..d8a00e9 100644 --- a/modules/nextcloud/Dockerfile +++ b/modules/nextcloud/Dockerfile @@ -11,6 +11,10 @@ ARG BUILD_BASE ARG RUNTIME_BASE ARG DOCKER_CLI +# Named so the final stage's COPY --from can reference a stage, not an ARG — the legacy builder +# this host still runs doesn't expand ARGs inside COPY --from, only inside FROM. +FROM ${DOCKER_CLI} AS dockercli + FROM ${BUILD_BASE} AS build # Compiled under /app/modules so `@novox/mesh-sdk` resolves upward into the base's own # node_modules — the module is compiled against exactly the sdk it will run against. The compiler @@ -22,15 +26,12 @@ RUN node /app/node_modules/typescript/bin/tsc client.ts index.ts tools/index.ts --module NodeNext --moduleResolution NodeNext --target ES2022 --outDir dist FROM ${RUNTIME_BASE} -# ARGs declared before the first FROM are out of scope past it; redeclared here so COPY --from -# below can see it. -ARG DOCKER_CLI COPY --from=build /app/modules/nextcloud/dist /app/modules/nextcloud/dist # occ runs inside nextcloud's own container, reached over the mounted docker socket — which needs # the docker CLI itself present here, not only the socket. Copied from Docker's own official client # image rather than apt-installed, so this stays the one binary and nothing else (no daemon, no # systemd unit, no package manager tree pulled in for it). -COPY --from=${DOCKER_CLI} /usr/local/bin/docker /usr/local/bin/docker +COPY --from=dockercli /usr/local/bin/docker /usr/local/bin/docker # Every serve-time entrypoint, loaded by the runtime in serve mode: tools and events serve, and a # provider's provisioner runs its reconcile loop in the same process, with the broker connected — # the convention novox/hq issues 060/061 settled. A container that instead ran only its -- 2.54.0 From b60dd5e3e3b21bd128cfd80b6c38db23933ee559 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 25 Sep 2026 13:51:40 +0200 Subject: [PATCH 07/11] nextcloud: use a module-owned mesh-admin account instead of 'admin' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit the migrated data has no literal 'admin' account — HAL's real admin login is a personal account (jochens), not a generic one. Resetting that would touch a real user's own credential, so the module gets its own dedicated admin-group service account instead, same pattern as the minio per-module service accounts. mesh-admin was created once by hand on novox to match this manifest for the already-migrated data; a genuinely fresh install seeds it automatically via NEXTCLOUD_ADMIN_USER/NEXTCLOUD_ADMIN_PASSWORD. --- modules/nextcloud/module.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/nextcloud/module.json b/modules/nextcloud/module.json index d24f1c2..3b118d8 100644 --- a/modules/nextcloud/module.json +++ b/modules/nextcloud/module.json @@ -65,7 +65,7 @@ "type": "file", "path": "/var/lib/nextcloud-module/server.env", "mode": "0600", - "content": "POSTGRES_HOST=${bound:postgres-database:at}:${bound:postgres-database:port}\nPOSTGRES_DB=${bound:postgres-database:as}\nPOSTGRES_USER=${bound:postgres-database:as}\nPOSTGRES_PASSWORD=${secret:postgres-database}\nNEXTCLOUD_ADMIN_USER=admin\nNEXTCLOUD_ADMIN_PASSWORD=${secret:admin}\nOBJECTSTORE_S3_HOST=${bound:s3-bucket:at}\nOBJECTSTORE_S3_PORT=${bound:s3-bucket:port}\nOBJECTSTORE_S3_BUCKET=nextcloud\nOBJECTSTORE_S3_KEY=${bound:s3-bucket:as}\nOBJECTSTORE_S3_SECRET=${secret:s3-bucket}\nOBJECTSTORE_S3_SSL=false\nOBJECTSTORE_S3_USEPATH_STYLE=true\n" + "content": "POSTGRES_HOST=${bound:postgres-database:at}:${bound:postgres-database:port}\nPOSTGRES_DB=${bound:postgres-database:as}\nPOSTGRES_USER=${bound:postgres-database:as}\nPOSTGRES_PASSWORD=${secret:postgres-database}\nNEXTCLOUD_ADMIN_USER=mesh-admin\nNEXTCLOUD_ADMIN_PASSWORD=${secret:admin}\nOBJECTSTORE_S3_HOST=${bound:s3-bucket:at}\nOBJECTSTORE_S3_PORT=${bound:s3-bucket:port}\nOBJECTSTORE_S3_BUCKET=nextcloud\nOBJECTSTORE_S3_KEY=${bound:s3-bucket:as}\nOBJECTSTORE_S3_SECRET=${secret:s3-bucket}\nOBJECTSTORE_S3_SSL=false\nOBJECTSTORE_S3_USEPATH_STYLE=true\n" }, { "id": "html", @@ -113,6 +113,7 @@ "MESH_BROKER_FILE": "/run/secrets/broker", "MESH_NEXTCLOUD_URL": "http://127.0.0.1:${port:80}", "MESH_NEXTCLOUD_CONFIG_FILE": "/run/config/config.json", + "MESH_NEXTCLOUD_ADMIN_USER": "mesh-admin", "MESH_NEXTCLOUD_ADMIN_PASSWORD_FILE": "/run/secrets/admin" }, "restart-on": [ -- 2.54.0 From df0e9017e0e2e02a63c9c9b4f00e8cc08e1d05d0 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 25 Sep 2026 14:12:05 +0200 Subject: [PATCH 08/11] nextcloud: set OBJECTSTORE_S3_REGION to match minio's actual region minio runs with MINIO_REGION=eu-west; nextcloud's S3 config never set a region, so every object write (avatars, file writes) failed signature validation with AuthorizationHeaderMalformed, surfacing as Internal Server Error on real page loads --- modules/nextcloud/module.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/nextcloud/module.json b/modules/nextcloud/module.json index 3b118d8..1ab8314 100644 --- a/modules/nextcloud/module.json +++ b/modules/nextcloud/module.json @@ -65,7 +65,7 @@ "type": "file", "path": "/var/lib/nextcloud-module/server.env", "mode": "0600", - "content": "POSTGRES_HOST=${bound:postgres-database:at}:${bound:postgres-database:port}\nPOSTGRES_DB=${bound:postgres-database:as}\nPOSTGRES_USER=${bound:postgres-database:as}\nPOSTGRES_PASSWORD=${secret:postgres-database}\nNEXTCLOUD_ADMIN_USER=mesh-admin\nNEXTCLOUD_ADMIN_PASSWORD=${secret:admin}\nOBJECTSTORE_S3_HOST=${bound:s3-bucket:at}\nOBJECTSTORE_S3_PORT=${bound:s3-bucket:port}\nOBJECTSTORE_S3_BUCKET=nextcloud\nOBJECTSTORE_S3_KEY=${bound:s3-bucket:as}\nOBJECTSTORE_S3_SECRET=${secret:s3-bucket}\nOBJECTSTORE_S3_SSL=false\nOBJECTSTORE_S3_USEPATH_STYLE=true\n" + "content": "POSTGRES_HOST=${bound:postgres-database:at}:${bound:postgres-database:port}\nPOSTGRES_DB=${bound:postgres-database:as}\nPOSTGRES_USER=${bound:postgres-database:as}\nPOSTGRES_PASSWORD=${secret:postgres-database}\nNEXTCLOUD_ADMIN_USER=mesh-admin\nNEXTCLOUD_ADMIN_PASSWORD=${secret:admin}\nOBJECTSTORE_S3_HOST=${bound:s3-bucket:at}\nOBJECTSTORE_S3_PORT=${bound:s3-bucket:port}\nOBJECTSTORE_S3_BUCKET=nextcloud\nOBJECTSTORE_S3_KEY=${bound:s3-bucket:as}\nOBJECTSTORE_S3_SECRET=${secret:s3-bucket}\nOBJECTSTORE_S3_SSL=false\nOBJECTSTORE_S3_USEPATH_STYLE=true\nOBJECTSTORE_S3_REGION=eu-west\n" }, { "id": "html", -- 2.54.0 From 61eca75315b5629701579fbb4c124a75297d4d99 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 25 Sep 2026 14:15:40 +0200 Subject: [PATCH 09/11] nextcloud: pull the S3 region from the binding, not a hardcoded literal the s3-bucket binding already carries serves.region (same mechanism as at/port); ${bound:s3-bucket:region} tracks whatever minio is actually configured with instead of a copy that can drift --- modules/nextcloud/module.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/nextcloud/module.json b/modules/nextcloud/module.json index 1ab8314..621edeb 100644 --- a/modules/nextcloud/module.json +++ b/modules/nextcloud/module.json @@ -65,7 +65,7 @@ "type": "file", "path": "/var/lib/nextcloud-module/server.env", "mode": "0600", - "content": "POSTGRES_HOST=${bound:postgres-database:at}:${bound:postgres-database:port}\nPOSTGRES_DB=${bound:postgres-database:as}\nPOSTGRES_USER=${bound:postgres-database:as}\nPOSTGRES_PASSWORD=${secret:postgres-database}\nNEXTCLOUD_ADMIN_USER=mesh-admin\nNEXTCLOUD_ADMIN_PASSWORD=${secret:admin}\nOBJECTSTORE_S3_HOST=${bound:s3-bucket:at}\nOBJECTSTORE_S3_PORT=${bound:s3-bucket:port}\nOBJECTSTORE_S3_BUCKET=nextcloud\nOBJECTSTORE_S3_KEY=${bound:s3-bucket:as}\nOBJECTSTORE_S3_SECRET=${secret:s3-bucket}\nOBJECTSTORE_S3_SSL=false\nOBJECTSTORE_S3_USEPATH_STYLE=true\nOBJECTSTORE_S3_REGION=eu-west\n" + "content": "POSTGRES_HOST=${bound:postgres-database:at}:${bound:postgres-database:port}\nPOSTGRES_DB=${bound:postgres-database:as}\nPOSTGRES_USER=${bound:postgres-database:as}\nPOSTGRES_PASSWORD=${secret:postgres-database}\nNEXTCLOUD_ADMIN_USER=mesh-admin\nNEXTCLOUD_ADMIN_PASSWORD=${secret:admin}\nOBJECTSTORE_S3_HOST=${bound:s3-bucket:at}\nOBJECTSTORE_S3_PORT=${bound:s3-bucket:port}\nOBJECTSTORE_S3_BUCKET=nextcloud\nOBJECTSTORE_S3_KEY=${bound:s3-bucket:as}\nOBJECTSTORE_S3_SECRET=${secret:s3-bucket}\nOBJECTSTORE_S3_SSL=false\nOBJECTSTORE_S3_USEPATH_STYLE=true\nOBJECTSTORE_S3_REGION=${bound:s3-bucket:region}\n" }, { "id": "html", -- 2.54.0 From 5d13a5078c22e8ceba7636ddc1c01cda3047d211 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 25 Sep 2026 14:37:23 +0200 Subject: [PATCH 10/11] =?UTF-8?q?minio:=20install=20mc=20in=20the=20runtim?= =?UTF-8?q?e=20image=20=E2=80=94=20the=20provisioner=20needs=20it=20to=20r?= =?UTF-8?q?un?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mesh-minio's s3-bucket provisioner shells out to mc to create buckets and service accounts on the live server, but mc was never in this module's own runtime image, only in minio's own. It's been silently retrying 'spawn mc ENOENT' forever, so every s3-bucket grant reached the control-plane layer (store.json, sealed secret) without the credential ever actually existing on minio — nextcloud's live instance just hit this as InvalidAccessKeyId on a real user session. Copies mc from minio's own image (docker.io/pgsty/minio, already pinned and pulled as this module's server container) rather than introducing a new base — mc there is a working, already-verified binary. /usr/bin/mc is a symlink to mcli; both are copied so it resolves. --- modules/minio/Dockerfile | 12 ++++++++++++ modules/minio/module.json | 4 ++++ 2 files changed, 16 insertions(+) diff --git a/modules/minio/Dockerfile b/modules/minio/Dockerfile index e5588e6..fc0e121 100644 --- a/modules/minio/Dockerfile +++ b/modules/minio/Dockerfile @@ -9,6 +9,11 @@ # image it RUNS in — the second must not carry a compiler. Declared in module.json's `build.on`. ARG BUILD_BASE ARG RUNTIME_BASE +ARG MC_CLI + +# Named so the final stage's COPY --from can reference a stage, not an ARG — the legacy builder +# this host still runs doesn't expand ARGs inside COPY --from, only inside FROM. +FROM ${MC_CLI} AS mccli FROM ${BUILD_BASE} AS build # Compiled under /app/modules so `@novox/mesh-sdk` resolves upward into the base's own @@ -22,6 +27,13 @@ RUN node /app/node_modules/typescript/bin/tsc client.ts tools/index.ts provision FROM ${RUNTIME_BASE} COPY --from=build /app/modules/minio/dist /app/modules/minio/dist +# The provisioner shells out to mc to actually create buckets and service accounts on the running +# minio server — mc itself was never in this runtime image, only in minio's own. Silently retried +# "spawn mc ENOENT" forever: a requirement was granted at the control-plane level without ever +# materializing the credential on minio. /usr/bin/mc there is a symlink to the real binary, mcli — +# both copied so the symlink resolves. +COPY --from=mccli /usr/bin/mcli /usr/bin/mcli +COPY --from=mccli /usr/bin/mc /usr/bin/mc # Every serve-time entrypoint, loaded by the runtime in serve mode: tools and events serve, and a # provider's provisioner runs its reconcile loop in the same process, with the broker connected — # the convention novox/hq issues 060/061 settled. diff --git a/modules/minio/module.json b/modules/minio/module.json index bbca5d2..ef31847 100644 --- a/modules/minio/module.json +++ b/modules/minio/module.json @@ -133,6 +133,10 @@ "arg": "RUNTIME_BASE", "module": "mesh-tools", "artifact": "runtime" + }, + { + "arg": "MC_CLI", + "image": "docker.io/pgsty/minio@sha256:b6bfe7239bfc83fb90d31612d9704d86039dd714f7904b3f1ad68f211e602372" } ], "artifacts": [ -- 2.54.0 From b3de7e7944b24b806e0af2f33994e7e70540031c Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 25 Sep 2026 15:10:30 +0200 Subject: [PATCH 11/11] minio: declare region eu-west, don't leave it as live-only drift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit serves.s3-bucket.region still said us-east-1 (PR #58 already fixed this, unmerged) while the live mesh-minio sidecar had MESH_MINIO_REGION=eu-west set out-of-band, not in the manifest at all — and the actual minio server had no region configured whatsoever (mc admin config get region: empty), apparently lost across a container recreation since nothing declared it. Every s3-bucket consumer binding ${bound:s3-bucket:region} was reading the stale us-east-1 declaration regardless of what was actually live. Declares MINIO_REGION on the server container and MESH_MINIO_REGION on the sidecar, matching the static serves declaration, so this is mesh- managed and durable rather than a manual mc admin config or docker env override that the next recreation silently drops. --- modules/minio/module.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/minio/module.json b/modules/minio/module.json index ef31847..8b1a05a 100644 --- a/modules/minio/module.json +++ b/modules/minio/module.json @@ -25,7 +25,7 @@ "serves": { "s3-bucket": { "scheme": "http", - "region": "us-east-1", + "region": "eu-west", "port": 9000 } }, @@ -99,7 +99,8 @@ "/var/lib/minio/root.secret:/run/secrets/root:ro" ], "env": { - "MINIO_ROOT_PASSWORD_FILE": "/run/secrets/root" + "MINIO_ROOT_PASSWORD_FILE": "/run/secrets/root", + "MINIO_REGION": "eu-west" } }, { @@ -116,6 +117,7 @@ "MESH_MINIO_ENDPOINT": "http://minio:9000", "MESH_MINIO_ROOT_USER": "meshroot", "MESH_MINIO_ROOT_PASSWORD_FILE": "/run/secrets/root", + "MESH_MINIO_REGION": "eu-west", "MESH_BROKER_FILE": "/run/secrets/broker", "MESH_RECEIVES": "/var/lib/minio/grants/mesh.json" }, -- 2.54.0