mirror of
https://github.com/fosrl/pangolin.git
synced 2026-10-30 15:39:06 +01:00
Only install the packages we need
This commit is contained in:
+28
-11
@@ -16,11 +16,19 @@ FROM ${NODE_IMAGE} AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*
|
||||
COPY package.json ./
|
||||
|
||||
COPY package*.json .npmrc ./
|
||||
|
||||
RUN npm ci
|
||||
# esbuild.mjs marks every npm package external, so bundling needs only the
|
||||
# bundler's own tooling (at the versions package.json pins), not the full
|
||||
# dependency tree. Installed outside /app so the root package.json's
|
||||
# dependencies aren't pulled in with it.
|
||||
RUN mkdir /tools && cd /tools && echo '{"private": true}' > package.json && \
|
||||
npm install --no-audit --no-fund --no-package-lock $(node -p ' \
|
||||
const p = require("/app/package.json"); \
|
||||
const all = { ...p.dependencies, ...p.devDependencies }; \
|
||||
["esbuild", "esbuild-node-externals", "yargs"] \
|
||||
.map((n) => `${n}@${all[n]}`).join(" ")') && \
|
||||
ln -s /tools/node_modules /app/node_modules
|
||||
|
||||
COPY . .
|
||||
|
||||
@@ -28,15 +36,24 @@ COPY . .
|
||||
ARG BUILD=enterprise
|
||||
ARG DATABASE=pg
|
||||
|
||||
# Bundle, then write a package.json listing only the packages the bundle
|
||||
# imports at runtime
|
||||
RUN npm run set:$DATABASE && \
|
||||
npm run set:$BUILD && \
|
||||
mkdir -p dist && \
|
||||
node esbuild.mjs -e server/private/dnsServer.ts -o dist/dns.mjs && \
|
||||
test -f dist/dns.mjs
|
||||
node esbuild.mjs -e server/private/dnsServer.ts -o dist/dns.mjs -m dist/dns.meta.json && \
|
||||
node scripts/bundle-package-json.mjs dist/dns.meta.json /out/package.json
|
||||
|
||||
# Copy only the node_modules files the bundle can load
|
||||
RUN npm prune --omit=dev && \
|
||||
node trace-deps.mjs -o /out dist/dns.mjs
|
||||
FROM ${NODE_IMAGE} AS deps
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Installing the trimmed package.json against the root lockfile keeps every
|
||||
# transitive version pinned to what the main Pangolin image ships
|
||||
COPY --from=builder /out/package.json ./package.json
|
||||
COPY package-lock.json .npmrc ./
|
||||
|
||||
RUN npm install --omit=dev --no-audit --no-fund
|
||||
|
||||
FROM ${NODE_IMAGE} AS runner
|
||||
|
||||
@@ -44,9 +61,9 @@ WORKDIR /app
|
||||
|
||||
RUN apt-get update && apt-get install -y tzdata && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=builder /out ./
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY --from=deps /app/package.json ./package.json
|
||||
COPY --from=builder /app/dist/dns.mjs ./dist/dns.mjs
|
||||
COPY --from=builder /app/package.json ./package.json
|
||||
|
||||
ENV ENVIRONMENT=prod
|
||||
ENV NODE_ENV=production
|
||||
|
||||
+10
@@ -45,6 +45,11 @@ const argv = yargs(hideBin(process.argv))
|
||||
choices: ["oss", "saas", "enterprise"],
|
||||
default: build
|
||||
})
|
||||
.option("metafile", {
|
||||
alias: "m",
|
||||
describe: "Write the esbuild metafile (inputs/imports) to this path",
|
||||
type: "string"
|
||||
})
|
||||
.help()
|
||||
.alias("help", "h").argv;
|
||||
|
||||
@@ -281,6 +286,7 @@ esbuild
|
||||
})
|
||||
],
|
||||
sourcemap: "inline",
|
||||
metafile: Boolean(argv.metafile),
|
||||
target: "node24"
|
||||
})
|
||||
.then((result) => {
|
||||
@@ -306,6 +312,10 @@ esbuild
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (argv.metafile) {
|
||||
fs.writeFileSync(argv.metafile, JSON.stringify(result.metafile));
|
||||
}
|
||||
|
||||
console.log("Build completed successfully");
|
||||
})
|
||||
.catch((error) => {
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
// Writes a package.json that depends only on the npm packages an esbuild
|
||||
// bundle imports at runtime, so a standalone image (e.g. Dockerfile.dns) can
|
||||
// `npm install` just those instead of the full dependency tree. Versions and
|
||||
// overrides come from the root package.json, and running `npm install` next
|
||||
// to the root package-lock.json keeps transitive versions pinned to the lock.
|
||||
//
|
||||
// Usage: node scripts/bundle-package-json.mjs <metafile.json> <out/package.json>
|
||||
|
||||
import { isBuiltin } from "node:module";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const [metafilePath, outPath] = process.argv.slice(2);
|
||||
if (!metafilePath || !outPath) {
|
||||
console.error(
|
||||
"Usage: node scripts/bundle-package-json.mjs <metafile.json> <out/package.json>"
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const metafile = JSON.parse(fs.readFileSync(metafilePath, "utf8"));
|
||||
const rootPkg = JSON.parse(fs.readFileSync("package.json", "utf8"));
|
||||
|
||||
// "@scope/name/sub/path" -> "@scope/name", "name/sub/path" -> "name"
|
||||
function packageName(specifier) {
|
||||
const parts = specifier.split("/");
|
||||
return specifier.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
|
||||
}
|
||||
|
||||
const used = new Set();
|
||||
for (const output of Object.values(metafile.outputs)) {
|
||||
for (const imp of output.imports) {
|
||||
if (!imp.external || isBuiltin(imp.path)) continue;
|
||||
used.add(packageName(imp.path));
|
||||
}
|
||||
}
|
||||
|
||||
const dependencies = {};
|
||||
const missing = [];
|
||||
for (const name of [...used].sort()) {
|
||||
const version = rootPkg.dependencies?.[name];
|
||||
if (version) {
|
||||
dependencies[name] = version;
|
||||
} else {
|
||||
missing.push(name);
|
||||
}
|
||||
}
|
||||
|
||||
// A runtime import of something that is only a devDependency (or not
|
||||
// declared at all) would be pruned from the image and fail at startup
|
||||
if (missing.length > 0) {
|
||||
console.error(
|
||||
`Bundle imports packages not in package.json dependencies: ${missing.join(", ")}`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const pkg = {
|
||||
name: rootPkg.name,
|
||||
version: rootPkg.version,
|
||||
private: true,
|
||||
type: rootPkg.type,
|
||||
dependencies,
|
||||
...(rootPkg.overrides && { overrides: rootPkg.overrides })
|
||||
};
|
||||
|
||||
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
||||
fs.writeFileSync(outPath, JSON.stringify(pkg, null, 4) + "\n");
|
||||
|
||||
console.log(
|
||||
`Wrote ${outPath} with ${Object.keys(dependencies).length} dependencies: ${Object.keys(dependencies).join(", ")}`
|
||||
);
|
||||
Reference in New Issue
Block a user