Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/docker-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
Expand All @@ -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
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
Expand Down
6 changes: 2 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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)); \
Expand Down Expand Up @@ -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)); \
Expand Down
9 changes: 3 additions & 6 deletions Dockerfile.worker
Original file line number Diff line number Diff line change
Expand Up @@ -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)); \
Expand All @@ -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)); \
Expand All @@ -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 --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
Expand Down
7 changes: 4 additions & 3 deletions docs/deployment-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions tests/container-ci-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,21 @@ 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("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("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");
});
Expand Down
Loading