@dockerforge/core 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dockerforge/core",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "DockerForge engine: analyse a local project and generate production-grade Dockerfiles, .dockerignore, and Compose, and lint Dockerfiles. Offline, no network.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Docker Forge",
@@ -697,13 +697,23 @@ function generateNodeRoot(a, envVars = [], rootConfigFiles = []) {
697
697
  // Build with `next build`, then run `next start` on a lean Node runtime. Serving the
698
698
  // .next output via a generic static server would break SSR, API routes, and dynamic rendering.
699
699
  if (a.framework === 'nextjs') {
700
- const prodInstall = nodeInstallLine(a.packageManager, true, { hasScopedPackages: a.hasScopedPackages });
701
700
  const nextSrcCopy = nextSourceCopyBlock(a, rootConfigFiles);
702
701
  const hasPublic = Array.isArray(a.sourceDirs) && a.sourceDirs.includes('public');
703
702
  const publicCopy = hasPublic ? `COPY --from=builder /app/public ./public\n` : '';
704
703
  const runtimeConfigCopy = a.nextRuntimeConfig
705
704
  ? `COPY --from=builder /app/${a.nextRuntimeConfig} ./${a.nextRuntimeConfig}\n`
706
705
  : '';
706
+ // Prune dev deps to production IN THE BUILDER, then copy node_modules into the runtime
707
+ // stage. This avoids a second install at runtime — which would re-hit the registry (and,
708
+ // for private/scoped packages, need the npmrc secret a second time). The builder already
709
+ // fetched and authenticated everything; pruning just drops the dev-only packages.
710
+ const secretMount = a.hasScopedPackages ? '--mount=type=secret,id=npmrc,target=/root/.npmrc ' : '';
711
+ const prodPrune =
712
+ a.packageManager === 'pnpm' ? 'RUN pnpm prune --prod'
713
+ : a.packageManager === 'yarn' ? `RUN ${secretMount}yarn install --frozen-lockfile --production --ignore-scripts && yarn cache clean`
714
+ : 'RUN npm prune --omit=dev';
715
+ // node images ship npm + yarn, but not pnpm — install it in the runtime stage if needed.
716
+ const runtimePmSetup = a.packageManager === 'pnpm' ? `RUN ${installPnpmGlobalCmd()}\n` : '';
707
717
 
708
718
  dockerfile = `
709
719
  # ─── Stage 1: Build ───────────────────────────────────────
@@ -718,6 +728,9 @@ ${buildInstall}
718
728
  ${nextSrcCopy}
719
729
  RUN ${buildPrefix}${a.buildCommand}
720
730
 
731
+ # Drop dev dependencies so only production node_modules carry into the runtime image
732
+ ${prodPrune}
733
+
721
734
  # ─── Stage 2: Runtime ─────────────────────────────────────
722
735
  FROM ${baseImage}
723
736
 
@@ -725,10 +738,10 @@ WORKDIR /app
725
738
 
726
739
  ${buildEnvBlock(envVars)}
727
740
 
728
- COPY ${a.lockFile} package.json ./
729
- ${prodInstall}
730
-
731
- # Next.js runs as a Node server; copy the build output, static assets, and the config it reads at runtime
741
+ # Next.js runs as a Node server. Copy production deps + build output from the builder —
742
+ # no second install here, so the (often private) registry is only contacted once, in the builder.
743
+ ${runtimePmSetup}COPY --from=builder /app/package.json ./package.json
744
+ COPY --from=builder /app/node_modules ./node_modules
732
745
  COPY --from=builder /app/${buildOut} ./${buildOut}
733
746
  ${publicCopy}${runtimeConfigCopy}${runtimeUserBlock(true)}
734
747
 
@@ -738,8 +751,11 @@ ${simpleHttpHealthcheck(a.port)}
738
751
  CMD ${startCmdJson}`.trim();
739
752
 
740
753
  improvements.push('Next.js is built and run as a Node server (next start) so SSR and API routes work — a generic static server image would break them.');
741
- improvements.push('Smaller image option: set `output: "standalone"` in next.config, then copy .next/standalone + .next/static instead of installing node_modules.');
754
+ improvements.push('Production node_modules are copied from the build stage (deps fetched once). For an even smaller image, set `output: "standalone"` in next.config and copy .next/standalone + .next/static.');
742
755
  improvements.push('Only known framework config files are auto-copied — verify any project-specific build config is also COPYed (e.g. sentry.*.config.ts, next-i18next.config).');
756
+ if (a.hasScopedPackages) {
757
+ improvements.push('Scoped packages detected (may include a private registry): the install keeps a secret mount — build with `docker build --secret id=npmrc,src=.npmrc .` and a .npmrc that authenticates your scope.');
758
+ }
743
759
  improvements.push('HEALTHCHECK probes /health, which Next.js does not expose by default — add a health route or point the healthcheck at an existing path.');
744
760
  return { dockerfile, dockerignore: nodeDockerignore(), improvements };
745
761
  }
@@ -138,8 +138,11 @@ function toSimpleDockerfile(dockerfile) {
138
138
  return dockerfile.split('\n').map(line => {
139
139
  const isRun = /^RUN\s/.test(line);
140
140
  if (!isRun) return line;
141
- // Strip --mount=type=cache,... and --mount=type=secret,...
142
- line = line.replace(/--mount=type=\w+,\S+\s+/g, '');
141
+ // Strip only --mount=type=cache,... (a pure build-speed optimisation).
142
+ // KEEP --mount=type=secret,... — for private/scoped registries it's the only way the
143
+ // default Dockerfile can authenticate (e.g. `docker build --secret id=npmrc,src=.npmrc`).
144
+ // An unsupplied secret mount is a no-op, so this is safe even when no secret is passed.
145
+ line = line.replace(/--mount=type=cache,\S+\s+/g, '');
143
146
  return line;
144
147
  }).join('\n');
145
148
  }