build(web): standalone Docker image + drop Turbopack build cache (#4619)

The Docker production-stage opts into Next.js `output: 'standalone'` via a
BUILD_STANDALONE env flag, so it ships only the traced runtime (server.js +
hoisted node_modules + static/public) and runs `node server.js` instead of
pnpm over the full source tree. The flag — and `outputFileTracingRoot`,
which traces from the monorepo root so workspace packages are included — is
set only in the Dockerfile build stage. Every other path keeps its original
output: Tauri `export`, local `build-web`, dev, and the Cloudflare/OpenNext
deploy (which forces standalone itself via NEXT_PRIVATE_STANDALONE).

Disable the experimental `turbopackFileSystemCacheForBuild`: a build
interrupted mid-compile leaves a partial cache that the next build
mishandles, fanning out workers until it exhausts host RAM. Remove the
pull-request CI step that cached `.next/cache` for it, now unused.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Huang Xin
2026-06-17 03:21:24 +08:00
committed by GitHub
parent f6fbbf59f2
commit 6514d4aa58
3 changed files with 35 additions and 24 deletions
-12
View File
@@ -60,18 +60,6 @@ jobs:
node-version: 24
cache: pnpm
# Turbopack persists its build cache here (turbopackFileSystemCacheForBuild).
# The key is deterministic (no commit SHA) so every PR restores the same
# entry — in practice the one `main` last saved, which is the only cache
# sibling PRs can all see.
- name: cache Turbopack build cache
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: apps/readest-app/.next/cache
key: turbo-build-web-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
turbo-build-web-${{ runner.os }}-
- name: install Dependencies
working-directory: apps/readest-app
run: |
+19 -7
View File
@@ -45,15 +45,27 @@ COPY --from=dependencies /app/apps/readest-app/public/vendor /app/apps/readest-a
COPY --from=dependencies /app/packages/foliate-js/node_modules /app/packages/foliate-js/node_modules
COPY . .
WORKDIR /app/apps/readest-app
# Opt into the self-contained `.next/standalone` tree for this image only;
# next.config.mjs gates `output: 'standalone'` on BUILD_STANDALONE so other
# web builds keep their default output.
ENV BUILD_STANDALONE=true
RUN pnpm build-web
# Production runtime ships only the standalone server, its traced node_modules,
# and the static/public assets — no pnpm, no source tree, no dev dependencies,
# no build cache. `output: 'standalone'` (next.config.mjs) emits the self-contained
# tree under .next/standalone, so the entrypoint is a plain `node server.js`.
FROM docker.io/library/node:24-slim@sha256:24dc26ef1e3c3690f27ebc4136c9c186c3133b25563ae4d7f0692e4d1fe5db0e AS production-stage
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
RUN corepack prepare pnpm@11.1.1 --activate
ENV NODE_ENV=production
ENV PORT=3000
ENV HOSTNAME=0.0.0.0
WORKDIR /app
COPY --from=build /app /app
WORKDIR /app/apps/readest-app
ENTRYPOINT ["pnpm", "start-web", "-H", "0.0.0.0"]
# Monorepo-rooted standalone tree: server.js + hoisted, traced node_modules.
COPY --from=build --chown=node:node /app/apps/readest-app/.next/standalone ./
# Static and public assets are not part of the standalone trace; copy them next
# to the server so their default relative paths resolve.
COPY --from=build --chown=node:node /app/apps/readest-app/.next/static ./apps/readest-app/.next/static
COPY --from=build --chown=node:node /app/apps/readest-app/public ./apps/readest-app/public
USER node
EXPOSE 3000
ENTRYPOINT ["node", "apps/readest-app/server.js"]
+16 -5
View File
@@ -14,12 +14,23 @@ if (isDev) {
}
const exportOutput = appPlatform !== 'web' && !isDev;
// Opt-in standalone output, set only by the Docker production build
// (Dockerfile). Every other path keeps the original behavior: Tauri `export`,
// local `build-web` (output undefined), dev, and the Cloudflare/OpenNext
// deploy — which forces standalone itself via NEXT_PRIVATE_STANDALONE.
const standaloneOutput = !exportOutput && process.env['BUILD_STANDALONE'] === 'true';
/** @type {import('next').NextConfig} */
const nextConfig = {
// Ensure Next.js uses SSG instead of SSR
// https://nextjs.org/docs/pages/building-your-application/deploying/static-exports
output: exportOutput ? 'export' : undefined,
// The Docker production image opts into a self-contained `.next/standalone`
// tree (see Dockerfile) so it can ship only the traced runtime; all other
// web builds fall back to the default server output.
output: exportOutput ? 'export' : standaloneOutput ? 'standalone' : undefined,
// Monorepo: trace from the repo root so workspace packages land in the
// standalone tree. Only relevant to — and only set for — the Docker build.
outputFileTracingRoot: standaloneOutput ? path.join(__dirname, '../../') : undefined,
pageExtensions: exportOutput ? ['jsx', 'tsx'] : ['js', 'jsx', 'ts', 'tsx'],
// Note: This feature is required to use the Next.js Image component in SSG mode.
// See https://nextjs.org/docs/messages/export-image-api for different workarounds.
@@ -28,11 +39,11 @@ const nextConfig = {
},
devIndicators: false,
experimental: {
// Persist Turbopack's compilation cache to `.next/` so CI can restore it
// between runs. Dev caching is on by default since Next 16.1; build
// caching is opt-in (beta).
// Dev caching is on by default since Next 16.1. We deliberately do NOT
// enable Turbopack's build cache (turbopackFileSystemCacheForBuild, beta):
// a build interrupted mid-compile leaves a partial cache that the next
// build mishandles, fanning out workers until it exhausts RAM.
turbopackFileSystemCacheForDev: true,
turbopackFileSystemCacheForBuild: true,
},
// Configure assetPrefix or else the server won't properly resolve your assets.
assetPrefix: '',