From 824a9ca4525db0068a0fdd005ffa2edc06588bf3 Mon Sep 17 00:00:00 2001 From: Kyle McLaren Date: Thu, 22 Oct 2026 11:07:07 +0200 Subject: [PATCH 1/2] Fix Next.js standalone images and build script detection - Read next.config.ts, .mts and .cjs when checking for standalone output, not only .mjs and .js. - Do not use experimental build mode for standalone apps. The standalone image does not contain docker-entrypoint.js, the next CLI or the sources, so the runtime generate step cannot run. - Fix the scripts.build check in nextjsGeneration, which always passed, so custom build scripts were replaced. - Compare the Next.js version numerically. Fixes #158 Fixes #159 --- gdf.js | 22 +++++---- .../next-custom-build/.dockerignore | 36 ++++++++++++++ test/frameworks/next-custom-build/Dockerfile | 45 ++++++++++++++++++ .../next-custom-build/package-lock.json | 6 +++ .../frameworks/next-custom-build/package.json | 27 +++++++++++ .../next-standalone-ts/.dockerignore | 36 ++++++++++++++ test/frameworks/next-standalone-ts/Dockerfile | 47 +++++++++++++++++++ .../next-standalone-ts/next.config.ts | 7 +++ .../next-standalone-ts/package-lock.json | 6 +++ .../next-standalone-ts/package.json | 27 +++++++++++ test/frameworks/next-standalone/Dockerfile | 5 +- .../next-standalone/docker-entrypoint.js | 28 ----------- 12 files changed, 251 insertions(+), 41 deletions(-) create mode 100644 test/frameworks/next-custom-build/.dockerignore create mode 100644 test/frameworks/next-custom-build/Dockerfile create mode 100644 test/frameworks/next-custom-build/package-lock.json create mode 100644 test/frameworks/next-custom-build/package.json create mode 100644 test/frameworks/next-standalone-ts/.dockerignore create mode 100644 test/frameworks/next-standalone-ts/Dockerfile create mode 100644 test/frameworks/next-standalone-ts/next.config.ts create mode 100644 test/frameworks/next-standalone-ts/package-lock.json create mode 100644 test/frameworks/next-standalone-ts/package.json delete mode 100755 test/frameworks/next-standalone/docker-entrypoint.js diff --git a/gdf.js b/gdf.js index 9964ecf..1c453c6 100755 --- a/gdf.js +++ b/gdf.js @@ -203,23 +203,27 @@ export class GDF { // Experimental nextjs generation? get nextjsGeneration() { if (!this.nextjs) return false - if (!this.#pj.scripts.build === 'next build') return false - if (!this.#pj.dependencies.next) return false + if (this.#pj.scripts?.build !== 'next build') return false + // standalone images contain neither the next CLI nor the sources + if (this.standaloneNextjs) return false const version = this.#pj.dependencies.next.match(/\d[.\d]*/) - if (!version || version[0] < '14.2') return false + if (!version) return false + const [major, minor = 0] = version[0].split('.').map(Number) + if (major < 14 || (major === 14 && minor < 2)) return false return true } get standaloneNextjs() { if (!this.nextjs) return false - if (fs.existsSync(path.join(this._appdir, 'next.config.mjs'))) { - const config = fs.readFileSync(path.join(this._appdir, 'next.config.mjs'), 'utf-8') + for (const ext of ['mjs', 'js', 'cjs', 'ts', 'mts']) { + const file = path.join(this._appdir, `next.config.${ext}`) + if (!fs.existsSync(file)) continue + const config = fs.readFileSync(file, 'utf-8') return /output\s*:\s*(["'`])standalone\1/.test(config) - } else if (fs.existsSync(path.join(this._appdir, 'next.config.js'))) { - const config = fs.readFileSync(path.join(this._appdir, 'next.config.js'), 'utf-8') - return /output\s*:\s*(["'`])standalone\1/.test(config) - } else return false + } + + return false } // Does this application use trpc? diff --git a/test/frameworks/next-custom-build/.dockerignore b/test/frameworks/next-custom-build/.dockerignore new file mode 100644 index 0000000..fd3dbb5 --- /dev/null +++ b/test/frameworks/next-custom-build/.dockerignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/test/frameworks/next-custom-build/Dockerfile b/test/frameworks/next-custom-build/Dockerfile new file mode 100644 index 0000000..c06933a --- /dev/null +++ b/test/frameworks/next-custom-build/Dockerfile @@ -0,0 +1,45 @@ +# syntax = docker/dockerfile:1 + +# Adjust NODE_VERSION as desired +ARG NODE_VERSION=xxx +FROM node:${NODE_VERSION}-slim AS base + +LABEL fly_launch_runtime="Next.js" + +# Next.js app lives here +WORKDIR /app + +# Set production environment +ENV NODE_ENV="production" + + +# Throw-away build stage to reduce size of final image +FROM base AS build + +# Install packages needed to build node modules +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y build-essential node-gyp pkg-config python-is-python3 + +# Install node modules +COPY package-lock.json package.json ./ +RUN npm ci --include=dev + +# Copy application code +COPY . . + +# Build application +RUN npm run build + +# Remove development dependencies +RUN npm prune --omit=dev + + +# Final stage for app image +FROM base + +# Copy built application +COPY --from=build /app /app + +# Start the server by default, this can be overwritten at runtime +EXPOSE 3000 +CMD [ "npm", "run", "start" ] diff --git a/test/frameworks/next-custom-build/package-lock.json b/test/frameworks/next-custom-build/package-lock.json new file mode 100644 index 0000000..0985ee8 --- /dev/null +++ b/test/frameworks/next-custom-build/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "standalone-next-js-test", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/test/frameworks/next-custom-build/package.json b/test/frameworks/next-custom-build/package.json new file mode 100644 index 0000000..540d510 --- /dev/null +++ b/test/frameworks/next-custom-build/package.json @@ -0,0 +1,27 @@ +{ + "name": "standalone-next-js-test", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build && node scripts/postbuild.js", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "next": "14.2.1", + "react": "^18", + "react-dom": "^18" + }, + "devDependencies": { + "@flydotio/dockerfile": "^0.5.5", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "eslint": "^8", + "eslint-config-next": "14.2.1", + "postcss": "^8", + "tailwindcss": "^3.4.1", + "typescript": "^5" + } +} diff --git a/test/frameworks/next-standalone-ts/.dockerignore b/test/frameworks/next-standalone-ts/.dockerignore new file mode 100644 index 0000000..fd3dbb5 --- /dev/null +++ b/test/frameworks/next-standalone-ts/.dockerignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/test/frameworks/next-standalone-ts/Dockerfile b/test/frameworks/next-standalone-ts/Dockerfile new file mode 100644 index 0000000..a6afec8 --- /dev/null +++ b/test/frameworks/next-standalone-ts/Dockerfile @@ -0,0 +1,47 @@ +# syntax = docker/dockerfile:1 + +# Adjust NODE_VERSION as desired +ARG NODE_VERSION=xxx +FROM node:${NODE_VERSION}-slim AS base + +LABEL fly_launch_runtime="Next.js" + +# Next.js app lives here +WORKDIR /app + +# Set production environment +ENV NODE_ENV="production" + + +# Throw-away build stage to reduce size of final image +FROM base AS build + +# Install packages needed to build node modules +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y build-essential node-gyp pkg-config python-is-python3 + +# Install node modules +COPY package-lock.json package.json ./ +RUN npm ci --include=dev + +# Copy application code +COPY . . + +# Build application +RUN npm run build + +# Remove development dependencies +RUN npm prune --omit=dev + + +# Final stage for app image +FROM base + +# Copy built application +COPY --from=build /app/.next/standalone /app +COPY --from=build /app/.next/static /app/.next/static +COPY --from=build /app/public /app/public + +# Start the server by default, this can be overwritten at runtime +EXPOSE 3000 +CMD [ "node", "server.js" ] diff --git a/test/frameworks/next-standalone-ts/next.config.ts b/test/frameworks/next-standalone-ts/next.config.ts new file mode 100644 index 0000000..bff4933 --- /dev/null +++ b/test/frameworks/next-standalone-ts/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + output: "standalone" +}; + +export default nextConfig; diff --git a/test/frameworks/next-standalone-ts/package-lock.json b/test/frameworks/next-standalone-ts/package-lock.json new file mode 100644 index 0000000..0985ee8 --- /dev/null +++ b/test/frameworks/next-standalone-ts/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "standalone-next-js-test", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/test/frameworks/next-standalone-ts/package.json b/test/frameworks/next-standalone-ts/package.json new file mode 100644 index 0000000..90a413d --- /dev/null +++ b/test/frameworks/next-standalone-ts/package.json @@ -0,0 +1,27 @@ +{ + "name": "standalone-next-js-test", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "next": "14.2.1", + "react": "^18", + "react-dom": "^18" + }, + "devDependencies": { + "@flydotio/dockerfile": "^0.5.5", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "eslint": "^8", + "eslint-config-next": "14.2.1", + "postcss": "^8", + "tailwindcss": "^3.4.1", + "typescript": "^5" + } +} diff --git a/test/frameworks/next-standalone/Dockerfile b/test/frameworks/next-standalone/Dockerfile index f22ed4e..a6afec8 100644 --- a/test/frameworks/next-standalone/Dockerfile +++ b/test/frameworks/next-standalone/Dockerfile @@ -28,7 +28,7 @@ RUN npm ci --include=dev COPY . . # Build application -RUN npx next build --experimental-build-mode compile +RUN npm run build # Remove development dependencies RUN npm prune --omit=dev @@ -42,9 +42,6 @@ COPY --from=build /app/.next/standalone /app COPY --from=build /app/.next/static /app/.next/static COPY --from=build /app/public /app/public -# Entrypoint sets up the container. -ENTRYPOINT [ "/app/docker-entrypoint.js" ] - # Start the server by default, this can be overwritten at runtime EXPOSE 3000 CMD [ "node", "server.js" ] diff --git a/test/frameworks/next-standalone/docker-entrypoint.js b/test/frameworks/next-standalone/docker-entrypoint.js deleted file mode 100755 index 91b89a3..0000000 --- a/test/frameworks/next-standalone/docker-entrypoint.js +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env node - -const { spawn } = require('node:child_process') - -const env = { ...process.env } - -;(async() => { - // If running the web server then prerender pages - if (process.argv.slice(-2).join(' ') === 'node server.js') { - await exec('npx next build --experimental-build-mode generate') - } - - // launch application - await exec(process.argv.slice(2).join(' ')) -})() - -function exec(command) { - const child = spawn(command, { shell: true, stdio: 'inherit', env }) - return new Promise((resolve, reject) => { - child.on('exit', code => { - if (code === 0) { - resolve() - } else { - reject(new Error(`${command} failed rc=${code}`)) - } - }) - }) -} From 8d15449972309e75abb231ab27fd69a92ae68984 Mon Sep 17 00:00:00 2001 From: Kyle McLaren Date: Thu, 22 Oct 2026 11:45:04 +0200 Subject: [PATCH 2/2] Keep Next.js build generation when build script only adds flags Treat `next build` followed only by flags (for example `next build --turbopack`) as a plain Next.js build, so these apps keep the deferred prerender step. Fall back to the app's build script only when it does more than `next build`. --- gdf.js | 3 +- .../frameworks/next-build-flags/.dockerignore | 36 ++++++++++++++ test/frameworks/next-build-flags/Dockerfile | 48 +++++++++++++++++++ .../next-build-flags/docker-entrypoint.js | 28 +++++++++++ .../next-build-flags/package-lock.json | 6 +++ test/frameworks/next-build-flags/package.json | 27 +++++++++++ 6 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 test/frameworks/next-build-flags/.dockerignore create mode 100644 test/frameworks/next-build-flags/Dockerfile create mode 100755 test/frameworks/next-build-flags/docker-entrypoint.js create mode 100644 test/frameworks/next-build-flags/package-lock.json create mode 100644 test/frameworks/next-build-flags/package.json diff --git a/gdf.js b/gdf.js index 1c453c6..e02405e 100755 --- a/gdf.js +++ b/gdf.js @@ -203,7 +203,8 @@ export class GDF { // Experimental nextjs generation? get nextjsGeneration() { if (!this.nextjs) return false - if (this.#pj.scripts?.build !== 'next build') return false + // only `next build`, optionally with flags; anything more is a custom build + if (!/^next build(\s+-[-\w=.]+)*$/.test(this.#pj.scripts?.build?.trim() ?? '')) return false // standalone images contain neither the next CLI nor the sources if (this.standaloneNextjs) return false const version = this.#pj.dependencies.next.match(/\d[.\d]*/) diff --git a/test/frameworks/next-build-flags/.dockerignore b/test/frameworks/next-build-flags/.dockerignore new file mode 100644 index 0000000..fd3dbb5 --- /dev/null +++ b/test/frameworks/next-build-flags/.dockerignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/test/frameworks/next-build-flags/Dockerfile b/test/frameworks/next-build-flags/Dockerfile new file mode 100644 index 0000000..07ada70 --- /dev/null +++ b/test/frameworks/next-build-flags/Dockerfile @@ -0,0 +1,48 @@ +# syntax = docker/dockerfile:1 + +# Adjust NODE_VERSION as desired +ARG NODE_VERSION=xxx +FROM node:${NODE_VERSION}-slim AS base + +LABEL fly_launch_runtime="Next.js" + +# Next.js app lives here +WORKDIR /app + +# Set production environment +ENV NODE_ENV="production" + + +# Throw-away build stage to reduce size of final image +FROM base AS build + +# Install packages needed to build node modules +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y build-essential node-gyp pkg-config python-is-python3 + +# Install node modules +COPY package-lock.json package.json ./ +RUN npm ci --include=dev + +# Copy application code +COPY . . + +# Build application +RUN npx next build --experimental-build-mode compile + +# Remove development dependencies +RUN npm prune --omit=dev + + +# Final stage for app image +FROM base + +# Copy built application +COPY --from=build /app /app + +# Entrypoint sets up the container. +ENTRYPOINT [ "/app/docker-entrypoint.js" ] + +# Start the server by default, this can be overwritten at runtime +EXPOSE 3000 +CMD [ "npm", "run", "start" ] diff --git a/test/frameworks/next-build-flags/docker-entrypoint.js b/test/frameworks/next-build-flags/docker-entrypoint.js new file mode 100755 index 0000000..49e5199 --- /dev/null +++ b/test/frameworks/next-build-flags/docker-entrypoint.js @@ -0,0 +1,28 @@ +#!/usr/bin/env node + +const { spawn } = require('node:child_process') + +const env = { ...process.env } + +;(async() => { + // If running the web server then prerender pages + if (process.argv.slice(-3).join(' ') === 'npm run start') { + await exec('npx next build --experimental-build-mode generate') + } + + // launch application + await exec(process.argv.slice(2).join(' ')) +})() + +function exec(command) { + const child = spawn(command, { shell: true, stdio: 'inherit', env }) + return new Promise((resolve, reject) => { + child.on('exit', code => { + if (code === 0) { + resolve() + } else { + reject(new Error(`${command} failed rc=${code}`)) + } + }) + }) +} diff --git a/test/frameworks/next-build-flags/package-lock.json b/test/frameworks/next-build-flags/package-lock.json new file mode 100644 index 0000000..0985ee8 --- /dev/null +++ b/test/frameworks/next-build-flags/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "standalone-next-js-test", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/test/frameworks/next-build-flags/package.json b/test/frameworks/next-build-flags/package.json new file mode 100644 index 0000000..349e21f --- /dev/null +++ b/test/frameworks/next-build-flags/package.json @@ -0,0 +1,27 @@ +{ + "name": "standalone-next-js-test", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build --turbopack", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "next": "14.2.1", + "react": "^18", + "react-dom": "^18" + }, + "devDependencies": { + "@flydotio/dockerfile": "^0.5.5", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "eslint": "^8", + "eslint-config-next": "14.2.1", + "postcss": "^8", + "tailwindcss": "^3.4.1", + "typescript": "^5" + } +}