---
id: 10902
title: 'Dockerfile npm ci fails: prepare lifecycle needs buildScripts before COPY'
state: CLOSED
labels:
  - bug
  - ai
  - architecture
  - build
assignees:
  - neo-opus-ada
createdAt: '2026-05-07T13:22:07Z'
updatedAt: '2026-05-09T21:16:00Z'
githubUrl: 'https://github.com/neomjs/neo/issues/10902'
author: neo-opus-ada
commentsCount: 0
parentIssue: null
subIssues: []
subIssuesCompleted: 0
subIssuesTotal: 0
blockedBy: []
blocking: []
closedAt: '2026-05-07T13:43:25Z'
---
# Dockerfile npm ci fails: prepare lifecycle needs buildScripts before COPY

## Context

Surfaced 2026-05-07 by [#10897](https://github.com/neomjs/neo/issues/10897) Lane C CI workflow (PR [#10899](https://github.com/neomjs/neo/pull/10899)) on first integration-job run. The Dockerfile in [`ai/deploy/Dockerfile`](https://github.com/neomjs/neo/blob/dev/ai/deploy/Dockerfile) (shipped via [#10880](https://github.com/neomjs/neo/pull/10880)) fails to build in any clean CI environment.

## The Problem

The Dockerfile's COPY/RUN ordering creates a hard dependency cycle:

```dockerfile
WORKDIR /app
COPY package*.json ./       # Step 4: only package.json + package-lock.json
RUN npm ci --omit=dev       # Step 5: triggers `prepare` lifecycle
COPY . .                    # Step 7: brings buildScripts/ but TOO LATE
```

The `prepare` script in [`package.json:57`](https://github.com/neomjs/neo/blob/dev/package.json#L57) is:

```
"prepare": "node ./buildScripts/ai/initServerConfigs.mjs && node ./buildScripts/ai/downloadKnowledgeBase.mjs"
```

`npm ci` automatically invokes `prepare` after install. At that step in the Dockerfile, only `package*.json` exists in `/app` — `buildScripts/` hasn't been COPY'd yet. The script fails with:

```
Error: Cannot find module '/app/buildScripts/ai/initServerConfigs.mjs'
    at Function._resolveFilename (node:internal/modules/cjs/loader:1383:15)
    ...
npm error code 1
target mc-server: failed to solve: process "/bin/sh -c npm ci --omit=dev" did not complete successfully: exit code: 1
```

**Empirical anchor:** [Lane C CI run 25497549380](https://github.com/neomjs/neo/actions/runs/25497549380) — both `kb-server` and `mc-server` Docker builds fail at the same step.

## Why this wasn't caught earlier

[#10880](https://github.com/neomjs/neo/pull/10880) (Docker artifacts) and [#10893](https://github.com/neomjs/neo/pull/10893) (Lane A integration test using these artifacts) both passed local verification because:

1. **Local Docker has cached layers** — when the developer ran `docker compose -f ai/deploy/docker-compose.test.yml up --build` after a prior successful build, the `RUN npm ci` layer was cached from a state when buildScripts/ had been previously available (e.g., during local dev with full repo).
2. **Local `.neo-ai-data/` exists** — the second `prepare` step (downloadKnowledgeBase) short-circuits via existing-dir guard, so even if the first step were to succeed locally, the failure mode is masked.
3. **No CI gate existed** — until [#10899](https://github.com/neomjs/neo/pull/10899), no automated environment was building from a truly clean state. Local builds from scratch would have surfaced this immediately.

This is a classic "works on my machine" pattern hidden by Docker layer caching. The fix prevents future regressions and makes the Docker artifacts genuinely reproducible across all environments.

## The Architectural Reality

- [`ai/deploy/Dockerfile`](https://github.com/neomjs/neo/blob/dev/ai/deploy/Dockerfile) — 21 lines, multi-stage build (builder → runtime).
- The Docker images are for **production deployment** of MCP servers (KB / MC). They do NOT need:
  - **`initServerConfigs.mjs`** — copies template configs to gitignored `config.mjs`. Production deployment provides config via env vars + the template defaults; the gitignored copy is for local-dev override only.
  - **`downloadKnowledgeBase.mjs`** — downloads release-artifact KB zip into `.neo-ai-data/`. Production KB MCP server provides KB via mounted Chroma volume + sync APIs, not bundled artifacts.
- Both prepare-lifecycle scripts exist for **local-dev convenience**, not production deployment. They should not run inside the Docker build.

## The Fix

Single-line change to [`ai/deploy/Dockerfile:5`](https://github.com/neomjs/neo/blob/dev/ai/deploy/Dockerfile#L5):

```diff
- RUN npm ci --omit=dev
+ RUN npm ci --omit=dev --ignore-scripts
```

`--ignore-scripts` instructs npm to skip lifecycle hooks (`prepare`, `postinstall`, etc.) during install. Aligns with Docker production semantics — the runtime image carries already-installed deps and has no need to run dev-time bootstrap.

**Why not "move COPY . . before RUN npm ci"?** That alternative works but defeats Docker layer-caching: every source change invalidates the npm-install layer. The `--ignore-scripts` fix preserves layer caching while skipping the dev-only hooks.

**Why not "split prepare into prepare:dev + prepare:prod"?** Larger surgery; introduces npm-script proliferation. The `--ignore-scripts` flag is the standard idiomatic move in Dockerfile contexts.

## Contract Ledger (T3)

| Target Surface | Source of Authority | Proposed Behavior | Fallback / Edge Case | Docs | Evidence |
|---|---|---|---|---|---|
| `ai/deploy/Dockerfile` line 5 | This ticket; surfaced by [#10897](https://github.com/neomjs/neo/issues/10897) Lane C CI run | Add `--ignore-scripts` flag to `npm ci --omit=dev`. Both `kb-server` and `mc-server` builds inherit the fix (same Dockerfile, parameterized via `TARGET_SERVER` build-arg). | Future Dockerfile evolution that genuinely needs a lifecycle script in production: re-introduce explicit step (e.g., `RUN node ./buildScripts/specific-prod-step.mjs`) with COPY of the needed files first. The `--ignore-scripts` is the production-ergonomic default; explicit scripting is the escape hatch. | Inline comment in Dockerfile linking to this ticket; possibly cookbook ([`learn/agentos/DeploymentCookbook.md`](https://github.com/neomjs/neo/blob/dev/learn/agentos/DeploymentCookbook.md)) Section 3 (Container Setup) reference. | L3 — Lane C CI ([#10899](https://github.com/neomjs/neo/pull/10899)) `integration` job runs green on the very PR fixing this; documented in PR body with run URL. |

## Acceptance Criteria

- [ ] [`ai/deploy/Dockerfile:5`](https://github.com/neomjs/neo/blob/dev/ai/deploy/Dockerfile#L5) contains `RUN npm ci --omit=dev --ignore-scripts`.
- [ ] Inline Dockerfile comment cross-links this ticket as the rationale.
- [ ] Lane C CI ([#10899](https://github.com/neomjs/neo/pull/10899)) `integration` matrix row passes after this fix lands (verified by re-running CI).
- [ ] Local Docker build smoke verification: `docker compose -f ai/deploy/docker-compose.test.yml build --no-cache kb-server mc-server` completes successfully on a clean checkout (no cached layers).

## Out of Scope

- **Multi-stage refactor of the Dockerfile** — current 2-stage shape (builder → runtime) is fine; only the `npm ci` step needs the flag.
- **Removing `prepare` lifecycle from `package.json`** — script remains valuable for local-dev workflow; only Docker context needs to opt out.
- **Audit of other Docker contexts** — only `ai/deploy/Dockerfile` exists in the repo today; cross-project Docker context audit isn't needed.
- **Splitting prepare into prepare:dev + prepare:prod** — see Avoided Traps.

## Avoided Traps / Gold Standards Rejected

- **Rejected: split `prepare` into `prepare:dev` + `prepare:prod` scripts.** Larger surgery; introduces npm-script proliferation and downstream version-management debt. The `--ignore-scripts` flag is the idiomatic Dockerfile pattern for the same goal.
- **Rejected: move `COPY . .` before `RUN npm ci`.** Defeats Docker layer caching — every source-file change invalidates the (slow) npm install layer. Only acceptable if cache-bust is desired (it's not here).
- **Rejected: explicit `COPY buildScripts/ai/initServerConfigs.mjs ./buildScripts/ai/initServerConfigs.mjs` before `RUN npm ci`.** Adds path-coupling fragility (file moves break the Dockerfile silently) for no production-side benefit, since the script itself doesn't need to run in production.
- **Rejected: defer to a follow-up PR after Lane C lands with `unit`-only matrix.** Lane C's whole point is gating PRs on test failures; landing the workflow without ability to gate `integration` undermines it. The 1-line fix is the elegant unblock.

## Related

- **Surfacing context:** Lane C CI workflow PR [#10899](https://github.com/neomjs/neo/pull/10899) (closes [#10897](https://github.com/neomjs/neo/issues/10897)) — first PR to run a clean-environment Docker build.
- **Original substrate:** [#10801](https://github.com/neomjs/neo/issues/10801) → PR [#10880](https://github.com/neomjs/neo/pull/10880) (Docker artifacts, KB+MC compose, ai/deploy/ namespace).
- **Downstream consumer:** [#10805](https://github.com/neomjs/neo/issues/10805) → PR [#10893](https://github.com/neomjs/neo/pull/10893) (Lane A integration test using the broken Dockerfile — passed locally via cached layers).
- **Sibling lane impact:** [#10896](https://github.com/neomjs/neo/issues/10896) (Lane B sustained-liveness PR [#10898](https://github.com/neomjs/neo/pull/10898)) is also blocked from L2/L3 verification by this same Dockerfile bug.

Origin Session ID: `7e897a0b-33ce-4d6c-b1a9-a1ff93e4e571`

Retrieval Hint: `query_raw_memories(query="Dockerfile npm ci prepare lifecycle initServerConfigs Lane C CI substrate fix")`

## Timeline

- 2026-05-07T13:22:09Z @neo-opus-ada added the `bug` label
- 2026-05-07T13:22:09Z @neo-opus-ada added the `ai` label
- 2026-05-07T13:22:09Z @neo-opus-ada added the `architecture` label
- 2026-05-07T13:22:09Z @neo-opus-ada added the `build` label
- 2026-05-07T13:23:18Z @neo-opus-ada cross-referenced by #10903
- 2026-05-07T13:25:29Z @neo-opus-ada cross-referenced by PR #10904
- 2026-05-07T13:28:07Z @neo-opus-ada cross-referenced by PR #10899
- 2026-05-07T13:43:25Z @tobiu referenced in commit `2721678` - "fix(deploy): add --ignore-scripts to npm ci in Dockerfile (#10902) (#10904)

The Dockerfile's COPY/RUN ordering creates a hard dependency cycle:
COPY only brings package*.json, then RUN npm ci triggers the prepare
lifecycle which needs buildScripts/ai/initServerConfigs.mjs — but
buildScripts/ isn't COPY'd until the next step. Build fails with
'Cannot find module /app/buildScripts/ai/initServerConfigs.mjs'.

The --ignore-scripts flag instructs npm to skip lifecycle hooks during
install. Production Docker images don't need the prepare-lifecycle scripts
(initServerConfigs is for local-dev gitignored config copying;
downloadKnowledgeBase is for local-dev KB-artifact downloads). Both are
zero-value in the runtime image.

Local builds passed via Docker layer caching (cached npm ci layer from
when buildScripts/ had been previously available); clean CI runs surfaced
the bug. Lane C #10897 / PR #10899 was the first PR to run a clean
ubuntu-latest Docker build.

Co-authored-by: tobiu <tobiasuhlig78@gmail.com>"
- 2026-05-07T13:43:26Z @tobiu closed this issue
- 2026-05-07T15:51:45Z @neo-opus-ada cross-referenced by #10908
- 2026-05-07T15:53:38Z @neo-opus-ada cross-referenced by PR #10909
- 2026-05-07T15:59:12Z @tobiu referenced in commit `3f94362` - "fix(deploy): use python urllib for Chroma healthcheck (curl missing) (#10908) (#10909)

The chromadb/chroma:1.5.9 image is debian-slim-based and does NOT
include curl. The healthcheck at ai/deploy/docker-compose.test.yml:7
'test: [CMD, curl, -f, ...]' fails every 5s for 60s, marks the chroma
container unhealthy, and blocks kb-server + mc-server from starting
(both have depends_on: chroma: service_healthy). Result: integration
tests time out at the 240s webServer envelope.

Same root pattern as #10902/#10904 (Dockerfile prepare-lifecycle):
substrate config bug masked by local Docker layer caching, surfaces
on first clean ubuntu-latest CI build via Lane C #10899.

Fix: use Python urllib (stdlib, guaranteed in the chromadb image
since Chroma is itself a Python app). Same /api/v2/heartbeat
endpoint, same interval/timeout/retries, no external binary
dependency.

Empirical anchor: Lane C CI run 25506320077 integration job —
'dependency failed to start: container neo-integration-test-chroma-1
is unhealthy' at 60s, then 240s webServer timeout.

Co-authored-by: tobiu <tobiasuhlig78@gmail.com>"
- 2026-05-07T16:10:29Z @neo-opus-ada cross-referenced by #10911
- 2026-05-07T16:11:39Z @neo-opus-ada cross-referenced by PR #10912
- 2026-05-07T16:30:29Z @neo-opus-ada cross-referenced by #10913
- 2026-05-07T17:00:22Z @neo-opus-ada cross-referenced by #10915
- 2026-05-07T18:24:02Z @neo-opus-ada cross-referenced by PR #10922
- 2026-05-08T14:35:55Z @neo-gpt cross-referenced by #10964
- 2026-05-08T15:49:36Z @neo-opus-ada cross-referenced by PR #10968
- 2026-05-09T21:16:00Z @neo-gemini-pro assigned to @neo-opus-ada

