Reduce image size by pruning stuff and things

Ref https://github.com/orgs/fosrl/discussions/2069
This commit is contained in:
Owen
2026-10-12 15:41:50 -04:00
parent 3d590493d6
commit 9afcd68d68
3 changed files with 112 additions and 18 deletions
+15 -17
View File
@@ -24,13 +24,19 @@ RUN if [ "$BUILD" = "oss" ]; then rm -rf server/private; fi && \
npm run build:cli && \
test -f dist/server.mjs
# Create placeholder files for MaxMind databases to avoid COPY errors
# Real files should be present for saas builds, placeholders for oss builds
RUN touch /app/GeoLite2-Country.mmdb /app/GeoLite2-ASN.mmdb
# Instead of shipping the full production node_modules (most of which is UI-only
# code already compiled into .next), copy only the files the server bundles can
# load, merged with the node_modules Next.js traced for its standalone server.
# Pruning first keeps dev dependencies out of the trace.
RUN npm prune --omit=dev && \
node trace-deps.mjs -o .next/standalone dist/server.mjs dist/migrations.mjs dist/cli.mjs
FROM base AS builder
RUN npm ci --omit=dev
# MaxMind databases are only shipped in saas builds
RUN mkdir -p maxmind && \
if [ "$BUILD" = "saas" ]; then \
touch GeoLite2-Country.mmdb GeoLite2-ASN.mmdb && \
mv GeoLite2-Country.mmdb GeoLite2-ASN.mmdb maxmind/; \
fi
# FROM node:24.18.1-slim AS runner
FROM public.ecr.aws/docker/library/node:24.18.1-slim AS runner
@@ -39,9 +45,6 @@ WORKDIR /app
RUN apt-get update && apt-get install -y curl tzdata && rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
COPY --from=builder-dev /app/.next/standalone ./
COPY --from=builder-dev /app/.next/static ./.next/static
COPY --from=builder-dev /app/dist ./dist
@@ -55,17 +58,12 @@ COPY server/db/ios_models.json ./dist/ios_models.json
COPY server/db/mac_models.json ./dist/mac_models.json
COPY public ./public
# Copy MaxMind databases for SaaS builds
# Copy MaxMind databases (empty directory for non-saas builds)
ARG BUILD=oss
RUN mkdir -p ./maxmind
COPY --from=builder-dev /app/maxmind ./maxmind
# Copy MaxMind databases (placeholders exist for oss builds, real files for saas)
COPY --from=builder-dev /app/GeoLite2-Country.mmdb ./maxmind/GeoLite2-Country.mmdb
COPY --from=builder-dev /app/GeoLite2-ASN.mmdb ./maxmind/GeoLite2-ASN.mmdb
# Remove MaxMind databases for non-saas builds (keep only for saas)
RUN if [ "$BUILD" != "saas" ]; then rm -rf ./maxmind; fi
RUN if [ "$BUILD" != "saas" ]; then rmdir ./maxmind; fi
# OCI Image Labels - Build Args for dynamic values
ARG VERSION="dev"
+22 -1
View File
@@ -1,5 +1,7 @@
import next from "next";
import express from "express";
import fs from "fs";
import path from "path";
import { parse } from "url";
import logger from "@server/logger";
import config from "@server/lib/config";
@@ -8,9 +10,28 @@ import { stripDuplicateSesions } from "./middlewares/stripDuplicateSessions";
const nextPort = config.getRawConfig().server.next_port;
export async function createNextServer() {
const dev = process.env.ENVIRONMENT !== "prod";
// The image only ships the files Next traced for its standalone output,
// which leaves out what is needed to load next.config at runtime. Hand
// Next the config resolved at build time instead, the same way the
// generated .next/standalone/server.js does.
const requiredServerFilesPath = path.join(
process.cwd(),
".next",
"required-server-files.json"
);
if (!dev && fs.existsSync(requiredServerFilesPath)) {
const { config: nextConfig } = JSON.parse(
fs.readFileSync(requiredServerFilesPath, "utf8")
);
process.env.__NEXT_PRIVATE_STANDALONE_CONFIG =
JSON.stringify(nextConfig);
}
// const app = next({ dev });
const app = next({
dev: process.env.ENVIRONMENT !== "prod",
dev,
turbopack: false
});
const handle = app.getRequestHandler();
+75
View File
@@ -0,0 +1,75 @@
// Copies only the node_modules files that the bundled server entry points can
// actually load into an output directory, so the Docker image does not need
// the full production node_modules tree (most of which is UI-only code that
// Next.js has already compiled into .next).
//
// Usage: node trace-deps.mjs -o <outDir> <entry.mjs> [entry.mjs...]
//
// Uses the copy of @vercel/nft that ships with Next.js (the same tracer Next
// uses for its standalone output), so it works after `npm prune --omit=dev`.
import nft from "next/dist/compiled/@vercel/nft/index.js";
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import path from "path";
import fs from "fs";
const argv = yargs(hideBin(process.argv))
.usage("Usage: $0 -o [string] <entry...>")
.option("out", {
alias: "o",
describe: "Directory to copy the traced node_modules files into",
type: "string",
demandOption: true
})
.demandCommand(1)
.help()
.alias("help", "h").argv;
const { nodeFileTrace } = nft;
const base = process.cwd();
const outDir = path.resolve(argv.out);
const { fileList, warnings } = await nodeFileTrace(
argv._.map((entry) => path.resolve(String(entry))),
{
base,
// Next.js is traced by `next build` for the standalone output (which
// includes next/dist/server/next, the entry createNextServer uses).
// Tracing it from here would pull in its dev server and bundlers.
ignore: (file) => file.startsWith("node_modules/next/")
}
);
// Unresolvable optional requires are expected (native binaries for other
// platforms, pg-native, bufferutil, ...), so only list them on request
if (process.env.TRACE_VERBOSE) {
for (const warning of warnings) {
console.warn(`trace warning: ${warning.message}`);
}
} else if (warnings.size > 0) {
console.log(
`${warnings.size} trace warnings (set TRACE_VERBOSE=1 to list them)`
);
}
let count = 0;
for (const file of fileList) {
if (!file.startsWith("node_modules/")) continue;
const src = path.join(base, file);
const dest = path.join(outDir, file);
const stat = fs.lstatSync(src);
if (stat.isDirectory() || fs.existsSync(dest)) continue;
fs.mkdirSync(path.dirname(dest), { recursive: true });
if (stat.isSymbolicLink()) {
fs.symlinkSync(fs.readlinkSync(src), dest);
} else {
fs.copyFileSync(src, dest);
fs.chmodSync(dest, stat.mode);
}
count++;
}
console.log(`Traced ${count} node_modules files into ${outDir}`);