From e4f3d4be17a85ee63e330921c57a5e6eab75c481 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Tue, 4 Aug 2026 02:17:33 +0800 Subject: [PATCH 01/11] test: guard Railway Dockerfile portability --- tests/container-ci-contract.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/container-ci-contract.test.ts b/tests/container-ci-contract.test.ts index 9323bb1b82..8bb6dfab32 100644 --- a/tests/container-ci-contract.test.ts +++ b/tests/container-ci-contract.test.ts @@ -71,6 +71,12 @@ describe("container delivery contract", () => { expect(read("Dockerfile.worker")).toContain("STOPSIGNAL SIGTERM"); }); + it("keeps Railway Dockerfiles portable across service IDs", () => { + expect(read("Dockerfile")).not.toContain("--mount=type=cache"); + expect(read("Dockerfile.worker")).not.toContain("--mount=type=cache"); + }); + + it("runs a provider-free runtime validator inside the worker image", () => { expect(read("Dockerfile.worker")).toContain("dist/worker/validate-runtime.mjs"); }); From a4fab9d12898d95fc19ddf2310a18cbde8b4accb Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Tue, 4 Aug 2026 02:23:24 +0800 Subject: [PATCH 02/11] test: format Railway portability guard --- tests/container-ci-contract.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/container-ci-contract.test.ts b/tests/container-ci-contract.test.ts index 8bb6dfab32..58b13d62d5 100644 --- a/tests/container-ci-contract.test.ts +++ b/tests/container-ci-contract.test.ts @@ -76,7 +76,6 @@ describe("container delivery contract", () => { expect(read("Dockerfile.worker")).not.toContain("--mount=type=cache"); }); - it("runs a provider-free runtime validator inside the worker image", () => { expect(read("Dockerfile.worker")).toContain("dist/worker/validate-runtime.mjs"); }); From 6bfc1586e2d1ef691d8968da58214010669e6948 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Tue, 4 Aug 2026 02:23:27 +0800 Subject: [PATCH 03/11] fix: remove Railway-incompatible app cache mounts --- Dockerfile | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5c457947c9..4abad1e2c4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,8 +30,7 @@ COPY scripts/check-node-engine.cjs scripts/check-node-engine.cjs COPY scripts/install-git-hooks.mjs scripts/install-git-hooks.mjs # Registry blips (ECONNRESET) have failed CI app-image builds mid-install; retry # the whole `npm ci` rather than relying only on per-request fetch retries. -RUN --mount=type=cache,target=/root/.npm,sharing=locked \ - for attempt in 1 2 3; do \ +RUN for attempt in 1 2 3; do \ npm ci --fetch-retries=5 --fetch-retry-mintimeout=20000 --fetch-retry-maxtimeout=120000 && break; \ if [ "$attempt" -eq 3 ]; then exit 1; fi; \ sleep $((attempt * 10)); \ @@ -68,8 +67,7 @@ WORKDIR /app COPY package.json package-lock.json .npmrc ./ COPY scripts/check-node-engine.cjs scripts/check-node-engine.cjs COPY scripts/install-git-hooks.mjs scripts/install-git-hooks.mjs -RUN --mount=type=cache,target=/root/.npm,sharing=locked \ - for attempt in 1 2 3; do \ +RUN for attempt in 1 2 3; do \ npm ci --omit=dev --fetch-retries=5 --fetch-retry-mintimeout=20000 --fetch-retry-maxtimeout=120000 && break; \ if [ "$attempt" -eq 3 ]; then exit 1; fi; \ sleep $((attempt * 10)); \ From 3aacbe198db8bda4097996d06d66637dff9f7ff5 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Tue, 4 Aug 2026 02:23:29 +0800 Subject: [PATCH 04/11] fix: remove Railway-incompatible worker cache mounts --- Dockerfile.worker | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Dockerfile.worker b/Dockerfile.worker index af0863050e..fd786a0d54 100644 --- a/Dockerfile.worker +++ b/Dockerfile.worker @@ -27,8 +27,7 @@ COPY package.json package-lock.json .npmrc ./ COPY scripts/check-node-engine.cjs scripts/check-node-engine.cjs COPY scripts/install-git-hooks.mjs scripts/install-git-hooks.mjs # Same install-retry contract as the app Dockerfile (registry ECONNRESET flakes). -RUN --mount=type=cache,id=npm,target=/root/.npm,sharing=locked \ - for attempt in 1 2 3; do \ +RUN for attempt in 1 2 3; do \ npm ci --fetch-retries=5 --fetch-retry-mintimeout=20000 --fetch-retry-maxtimeout=120000 && break; \ if [ "$attempt" -eq 3 ]; then exit 1; fi; \ sleep $((attempt * 10)); \ @@ -43,8 +42,7 @@ WORKDIR /app COPY package.json package-lock.json .npmrc ./ COPY scripts/check-node-engine.cjs scripts/check-node-engine.cjs COPY scripts/install-git-hooks.mjs scripts/install-git-hooks.mjs -RUN --mount=type=cache,id=npm,target=/root/.npm,sharing=locked \ - for attempt in 1 2 3; do \ +RUN for attempt in 1 2 3; do \ npm ci --omit=dev --fetch-retries=5 --fetch-retry-mintimeout=20000 --fetch-retry-maxtimeout=120000 && break; \ if [ "$attempt" -eq 3 ]; then exit 1; fi; \ sleep $((attempt * 10)); \ @@ -57,8 +55,7 @@ RUN apt-get update \ WORKDIR /app COPY worker/python/requirements.txt worker/python/requirements.txt RUN python3 -m venv /opt/ocr-venv -RUN --mount=type=cache,id=pip,target=/root/.cache/pip \ - /opt/ocr-venv/bin/pip install --upgrade --require-hashes -r worker/python/requirements.txt \ +RUN /opt/ocr-venv/bin/pip install --upgrade --require-hashes -r worker/python/requirements.txt \ && /opt/ocr-venv/bin/pip check ENV PATH="/opt/ocr-venv/bin:${PATH}" ENV PYTHONDONTWRITEBYTECODE=1 From fa61e7b98825248c684ff53679f092827e19145d Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Tue, 4 Aug 2026 02:23:31 +0800 Subject: [PATCH 05/11] docs: explain portable Railway cache policy --- docs/deployment-architecture.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/deployment-architecture.md b/docs/deployment-architecture.md index a853147929..c1c6a4ba16 100644 --- a/docs/deployment-architecture.md +++ b/docs/deployment-architecture.md @@ -207,9 +207,10 @@ comparable (~200 ms) from Singapore or Sydney and does not favour either host. ### Image contract (`Dockerfile`) - `node:24-bookworm-slim` is pinned by multi-platform SHA-256 digest in a - shared `node-base` stage and used by every stage. BuildKit cache mounts - speed `npm ci` and the worker Python venv install without bloating final - images. + shared `node-base` stage and used by every stage. Dependency installs use + ordinary Docker layer caching. The shared Dockerfiles deliberately avoid + BuildKit cache mounts because Railway requires hard-coded, service-specific + cache IDs, which would couple each image to one Railway service. - The build stage runs the repo's own `npm run build` (`guard-next-build.mjs` + `next build --webpack` + the client-bundle secret scan) — **the image build fails exactly where a local build would**. The From 8c38ce1126b8846642a1ce129ae42b302fec79fc Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Tue, 4 Aug 2026 02:41:50 +0800 Subject: [PATCH 06/11] test(ci): require non-blocking image cache exports --- tests/container-ci-contract.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/container-ci-contract.test.ts b/tests/container-ci-contract.test.ts index 58b13d62d5..2730ec7898 100644 --- a/tests/container-ci-contract.test.ts +++ b/tests/container-ci-contract.test.ts @@ -76,6 +76,11 @@ describe("container delivery contract", () => { expect(read("Dockerfile.worker")).not.toContain("--mount=type=cache"); }); + it("does not fail image verification when the optional GitHub cache export is unavailable", () => { + const workflow = read(".github/workflows/docker-image.yml"); + expect(workflow.match(/cache-to:.*ignore-error=true/g)).toHaveLength(2); + }); + it("runs a provider-free runtime validator inside the worker image", () => { expect(read("Dockerfile.worker")).toContain("dist/worker/validate-runtime.mjs"); }); From a9692a2980d49425a3758850f6230c87c8e3ea23 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Tue, 4 Aug 2026 02:42:05 +0800 Subject: [PATCH 07/11] ci(docker): tolerate cache export outages --- .github/workflows/docker-image.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index a9fe7728ef..2323c2e22f 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -57,7 +57,7 @@ jobs: NEXT_PUBLIC_MAX_UPLOAD_MB=50 ALLOW_LOW_RAM_BUILD=1 cache-from: type=gha,scope=clinical-kb-app - cache-to: type=gha,scope=clinical-kb-app,mode=max + cache-to: type=gha,scope=clinical-kb-app,mode=max,ignore-error=true - name: Build worker image (load, no push) uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7 @@ -68,7 +68,7 @@ jobs: push: false load: true cache-from: type=gha,scope=clinical-kb-worker - cache-to: type=gha,scope=clinical-kb-worker,mode=max + cache-to: type=gha,scope=clinical-kb-worker,mode=max,ignore-error=true - name: Verify image content contract run: node scripts/check-image-content-contract.mjs From 3b84e005da62181922589158505ac37febf3e642 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Tue, 4 Aug 2026 02:50:08 +0800 Subject: [PATCH 08/11] test(ci): cap optional cache waits --- tests/container-ci-contract.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/container-ci-contract.test.ts b/tests/container-ci-contract.test.ts index 2730ec7898..3dcaab1eef 100644 --- a/tests/container-ci-contract.test.ts +++ b/tests/container-ci-contract.test.ts @@ -79,6 +79,7 @@ describe("container delivery contract", () => { it("does not fail image verification when the optional GitHub cache export is unavailable", () => { const workflow = read(".github/workflows/docker-image.yml"); expect(workflow.match(/cache-to:.*ignore-error=true/g)).toHaveLength(2); + expect(workflow.match(/cache-(?:from|to):.*timeout=2m/g)).toHaveLength(4); }); it("runs a provider-free runtime validator inside the worker image", () => { From 51df6196c4129e6237be8e967500510baae00b2a Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Tue, 4 Aug 2026 02:50:27 +0800 Subject: [PATCH 09/11] ci(docker): bound optional cache waits --- .github/workflows/docker-image.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 2323c2e22f..611bc543f0 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -56,8 +56,8 @@ jobs: MAX_UPLOAD_MB=50 NEXT_PUBLIC_MAX_UPLOAD_MB=50 ALLOW_LOW_RAM_BUILD=1 - cache-from: type=gha,scope=clinical-kb-app - cache-to: type=gha,scope=clinical-kb-app,mode=max,ignore-error=true + cache-from: type=gha,scope=clinical-kb-app,timeout=2m + cache-to: type=gha,scope=clinical-kb-app,mode=max,timeout=2m,ignore-error=true - name: Build worker image (load, no push) uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7 @@ -67,8 +67,8 @@ jobs: tags: clinical-kb-worker:ci push: false load: true - cache-from: type=gha,scope=clinical-kb-worker - cache-to: type=gha,scope=clinical-kb-worker,mode=max,ignore-error=true + cache-from: type=gha,scope=clinical-kb-worker,timeout=2m + cache-to: type=gha,scope=clinical-kb-worker,mode=max,timeout=2m,ignore-error=true - name: Verify image content contract run: node scripts/check-image-content-contract.mjs From e1221962dec7eba95f52b89c24e16835bad5c0fe Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Tue, 4 Aug 2026 03:10:27 +0800 Subject: [PATCH 10/11] test(docker): keep pip cache out of worker image --- tests/container-ci-contract.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/container-ci-contract.test.ts b/tests/container-ci-contract.test.ts index 3dcaab1eef..de7e12f8e0 100644 --- a/tests/container-ci-contract.test.ts +++ b/tests/container-ci-contract.test.ts @@ -82,6 +82,10 @@ describe("container delivery contract", () => { expect(workflow.match(/cache-(?:from|to):.*timeout=2m/g)).toHaveLength(4); }); + it("does not bake pip's download cache into the final worker image", () => { + expect(read("Dockerfile.worker")).toContain("pip install --no-cache-dir"); + }); + it("runs a provider-free runtime validator inside the worker image", () => { expect(read("Dockerfile.worker")).toContain("dist/worker/validate-runtime.mjs"); }); From a2c6343b50e390cb5eaaacde5737c3413c37257f Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Tue, 4 Aug 2026 03:10:52 +0800 Subject: [PATCH 11/11] fix(docker): keep pip downloads out of worker image --- Dockerfile.worker | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile.worker b/Dockerfile.worker index fd786a0d54..7701d54d09 100644 --- a/Dockerfile.worker +++ b/Dockerfile.worker @@ -55,7 +55,7 @@ RUN apt-get update \ WORKDIR /app COPY worker/python/requirements.txt worker/python/requirements.txt RUN python3 -m venv /opt/ocr-venv -RUN /opt/ocr-venv/bin/pip install --upgrade --require-hashes -r worker/python/requirements.txt \ +RUN /opt/ocr-venv/bin/pip install --no-cache-dir --upgrade --require-hashes -r worker/python/requirements.txt \ && /opt/ocr-venv/bin/pip check ENV PATH="/opt/ocr-venv/bin:${PATH}" ENV PYTHONDONTWRITEBYTECODE=1