22#
33# Multi-stage build:
44# 1. Copy Syft + Grype binaries from official Anchore images (no curl|sh supply chain risk)
5- # 2. Build CLI with pnpm ci + tsup in Chainguard node:latest-dev (has shell + npm/pnpm)
6- # 3. Minimal Chainguard Node runtime (near-zero CVEs, runs as nonroot user)
5+ # 2. Build CLI + scanner with pnpm in Chainguard node:latest-dev
6+ # 3. Install production deps with npm (no symlinks) for clean runtime copy
7+ # 4. Minimal Chainguard Node runtime (near-zero CVEs, runs as nonroot user)
78
89# Stage 1: Syft binary — official Anchore image, binary at /syft
910FROM anchore/syft:latest AS syft
@@ -12,7 +13,6 @@ FROM anchore/syft:latest AS syft
1213FROM anchore/grype:latest AS grype
1314
1415# Stage 3: Build stage — cgr.dev/chainguard/node:latest-dev has npm, pnpm, and shell
15- # We need the dev variant here because the distroless runtime has no package manager
1616FROM cgr.dev/chainguard/node:latest-dev AS builder
1717WORKDIR /app
1818
@@ -27,36 +27,36 @@ COPY --chown=node:node packages/cli/tsconfig.json packages/cli/
2727COPY --chown=node:node packages/cli/src/ packages/cli/src/
2828COPY --chown=node:node package.json pnpm-workspace.yaml pnpm-lock.yaml ./
2929
30- # Install deps and build (pnpm is pre-installed in chainguard node:latest-dev)
31- # Scanner must be built first so dist/index.d.ts exists for CLI typecheck
30+ # Install deps and build with pnpm
3231RUN pnpm install --frozen-lockfile && \
3332 pnpm --filter @ottersight/scanner build && \
3433 pnpm --filter @ottersight/cli build
3534
35+ # Create a clean deploy directory with npm (no pnpm symlinks)
36+ # Remove workspace:* dep — scanner is copied manually below
37+ RUN mkdir -p /app/deploy && \
38+ cp -r /app/packages/cli/dist /app/deploy/dist && \
39+ cat /app/packages/cli/package.json | sed '/"@ottersight\/ scanner"/d' > /app/deploy/package.json && \
40+ cd /app/deploy && npm install --omit=dev --ignore-scripts && \
41+ mkdir -p /app/deploy/node_modules/@ottersight/scanner && \
42+ cp -r /app/packages/scanner/dist /app/deploy/node_modules/@ottersight/scanner/dist && \
43+ cp /app/packages/scanner/package.json /app/deploy/node_modules/@ottersight/scanner/package.json
44+
3645# Stage 4: Minimal runtime — Chainguard distroless Node (no shell, nonroot user)
37- # cgr.dev/chainguard/node entrypoint is /usr/bin/node — we pass args directly via ENTRYPOINT
3846FROM cgr.dev/chainguard/node:latest
3947WORKDIR /app
4048
41- # Copy built CLI dist and its node_modules from builder
42- COPY --from=builder /app/packages/cli/dist ./dist
43- COPY --from=builder /app/packages/cli/node_modules ./node_modules
44-
45- # Copy scanner dist and package.json into the CLI's node_modules directory
46- # (workspace symlink won't exist in runtime stage; copy manually)
47- COPY --from=builder /app/packages/scanner/dist ./node_modules/@ottersight/scanner/dist
48- COPY --from=builder /app/packages/scanner/package.json ./node_modules/@ottersight/scanner/package.json
49+ # Copy deployed CLI (dist + real node_modules, no symlinks)
50+ COPY --from=builder /app/deploy/dist ./dist
51+ COPY --from=builder /app/deploy/node_modules ./node_modules
4952
5053# Copy Syft + Grype binaries from official Anchore images
51- # Using binary copy pattern (not install script) eliminates supply chain risk
5254COPY --from=syft /syft /usr/local/bin/syft
5355COPY --from=grype /grype /usr/local/bin/grype
5456
55- # Volume mount point for user's repo — convention: mount local directory at /repo
56- # docker run --rm -v $(pwd):/repo ghcr.io/ottersight/cli scan /repo
57+ # Volume mount point for user's repo
5758VOLUME ["/repo" ]
5859
5960# Entrypoint: node runs the CLI script directly (no shell needed)
60- # CMD provides default scan target; override by passing different args to docker run
6161ENTRYPOINT ["node" , "/app/dist/index.js" ]
6262CMD ["scan" , "/repo" ]
0 commit comments