Repository: vercel/next.js
Pull request: Allow inspecting server with `next start --inspect`

Same as https://github.com/vercel/next.js/pull/85037 but for `next start`

`NODE_OPTIONS=--inspect` is way too convoluted with package.json scripts. 

This implements `--inspect` for `next start` which will forward the value for the Node.js process  of the server (which is where user code will be executed). `NODE_OPTIONS=--inspect next start` will still work like before. Debug options from `NODE_OPTIONS` have precedence over the `--inspect` option of `next start`.

## Test plan

- [x] `pnpm next start --inspect` works
   ```
   Debugger listening on ws://127.0.0.1:9229/51c56bce-89f3-45ab-a12b-9a6241403e9c
   For help, see: https://nodejs.org/en/docs/inspector
      ▲ Next.js 16.0.0-canary.19 (Turbopack)
      - Local:         http://localhost:3000
      - Network:       http://192.168.178.36:3000
      - Debugger port: 9229
- [x] our own `pnpm debug start` still works
   ```
   Debugger listening on ws://127.0.0.1:9229/29dbdf5e-9c31-40dd-9690-bf85f801dc84
   For help, see: https://nodejs.org/en/docs/inspector
   ⚠ The Node.js debugger port is already open at 9229. Ignoring '--inspect="true"'.
   ▲ Next.js 16.1.1-canary.30
   - Local:         http://localhost:3000
   - Network:       http://192.168.178.36:3000
   - Debugger port: 9229
   ✓ Ready in 175ms
   ```

Changed files:
filename: docs/01-app/02-guides/debugging.mdx
patch: @@ -115,13 +115,16 @@ To debug server-side Next.js code with browser DevTools, you need to pass the `-
 
 ```bash filename="Terminal"
 next dev --inspect
+# or
+next start --inspect
+# next build --inspect is currently not supported
 ```
 
 The value of `--inspect` is passed to the underlying Node.js process. Check out the [`--inspect` docs for advanced use cases](https://nodejs.org/api/cli.html#--inspecthostport).
 
 > **Good to know**: Use `--inspect=0.0.0.0` to allow remote debugging access outside localhost, such as when running the app in a Docker container.
 
-Launching the Next.js dev server with the `--inspect` flag will look something like this:
+Launching the Next.js server with the `--inspect` flag will look something like this:
 
 ```bash filename="Terminal"
 Debugger listening on ws://127.0.0.1:9229/0cf90313-350d-4466-a748-cd60f4e47c95

filename: packages/next/src/bin/next.ts
patch: @@ -88,7 +88,11 @@ class NextRootCommand extends Command {
       ;(process.env as any).NODE_ENV = process.env.NODE_ENV || defaultEnv
       ;(process.env as any).NEXT_RUNTIME = 'nodejs'
 
-      if (commandName !== 'dev' && event.getOptionValue('inspect') === true) {
+      if (
+        commandName !== 'dev' &&
+        commandName !== 'start' &&
+        event.getOptionValue('inspect') === true
+      ) {
         console.error(
           `\`--inspect\` flag is deprecated. Use env variable NODE_OPTIONS instead: NODE_OPTIONS='--inspect' next ${commandName}`
         )
@@ -380,6 +384,12 @@ program
     '-H, --hostname <hostname>',
     'Specify a hostname on which to start the application (default: 0.0.0.0).'
   )
+  .addOption(
+    new Option(
+      '--inspect [[host:]port]',
+      'Allows inspecting server-side code. See https://nextjs.org/docs/app/guides/debugging#server-side-code'
+    ).argParser(parseValidInspectAddress)
+  )
   .addOption(
     new Option(
       '--keepAliveTimeout <keepAliveTimeout>',

filename: packages/next/src/cli/next-start.ts
patch: @@ -10,7 +10,12 @@ if (!process.env.NEXT_PRIVATE_START_TIME) {
 import '../server/lib/cpu-profile'
 import { saveCpuProfile } from '../server/lib/cpu-profile'
 import { startServer } from '../server/lib/start-server'
-import { printAndExit } from '../server/lib/utils'
+import {
+  getParsedDebugAddress,
+  formatDebugAddress,
+  printAndExit,
+  type DebugAddress,
+} from '../server/lib/utils'
 import { getProjectDir } from '../lib/get-project-dir'
 import {
   getReservedPortExplanation,
@@ -21,6 +26,8 @@ import * as Log from '../build/output/log'
 export type NextStartOptions = {
   port: number
   hostname?: string
+  // Commander is not putting `--inspect` through the arg parser
+  inspect?: DebugAddress | true
   keepAliveTimeout?: number
   experimentalNextConfigStripTypes?: boolean
   experimentalCpuProf?: boolean
@@ -35,13 +42,38 @@ export type NextStartOptions = {
 const nextStart = async (options: NextStartOptions, directory?: string) => {
   const dir = getProjectDir(directory)
   const hostname = options.hostname
+  const inspect = options.inspect
   const port = options.port
   const keepAliveTimeout = options.keepAliveTimeout
 
   if (isPortIsReserved(port)) {
     printAndExit(getReservedPortExplanation(port), 1)
   }
 
+  if (inspect) {
+    const inspector = await import('inspector')
+    const isInspecting = inspector.url() !== undefined
+    if (isInspecting) {
+      Log.warn(
+        `The Node.js debugger port is already open at ${process.debugPort}. Ignoring '--inspect${inspect === true ? '' : `="${formatDebugAddress(inspect)}"`}'.`
+      )
+    } else {
+      const inspectAddress: DebugAddress =
+        inspect === true ? getParsedDebugAddress(true) : inspect
+      // TODO: Implement --inspect-wait
+      const wait = false
+      try {
+        inspector.open(inspectAddress.port, inspectAddress.host, wait)
+      } catch (error) {
+        console.error(
+          `Failed to start the Node.js inspector with --inspect="${formatDebugAddress(inspectAddress)}":`,
+          error
+        )
+        return process.exit(1)
+      }
+    }
+  }
+
   if (options.experimentalCpuProf) {
     Log.info(`CPU profiling enabled. Profile will be saved on exit (Ctrl+C).`)
     // Save CPU profile on shutdown signals, but let start-server.ts handle graceful exit

filename: test/integration/cli/test/index.test.ts
patch: @@ -281,6 +281,41 @@ describe('CLI Usage', () => {
             `Bad port: "${reservedPort}" is reserved for npp`
           )
         })
+
+        test('--inspect', async () => {
+          await nextBuild(dirBasic)
+          const port = await findPort()
+
+          let output = ''
+          let errOutput = ''
+          const app = await runNextCommandDev(
+            ['start', dirBasic, '--port', '' + port, '--inspect'],
+            undefined,
+            {
+              onStdout(msg) {
+                output += stripAnsi(msg)
+              },
+              onStderr(msg) {
+                errOutput += stripAnsi(msg)
+              },
+            }
+          )
+
+          try {
+            await retry(() => {
+              expect(output).toMatch(new RegExp(`http://localhost:${port}`))
+            })
+            await retry(() => {
+              expect(output).toMatch(/- Debugger port:\s+9229/)
+            })
+            await retry(() => {
+              expect(errOutput).toMatch(/Debugger listening on/)
+            })
+            expect(errOutput).not.toContain('address already in use')
+          } finally {
+            await killApp(app)
+          }
+        })
       })
 
       describe('telemetry', () => {