Skip to content

adapter-nextjs: all public/ assets 404 in production with webpack standalone builds (monorepo) — no bundler-independent public copy #630

Description

@Agent-Aurelius

Summary

With the webpack builder (Next 16, output: 'standalone', pnpm monorepo, app at apps/web), App Hosting deploys serve every public/ asset as a Next 404 in production — images, fonts, manifest.json, all of it — while /_next/static/* serves fine. The same app built with turbopack (same adapter version) serves public/ correctly.

We shipped this to production unknowingly during a builder cutover (turbopack → webpack) and every public asset on the site 404'd until we diagnosed it.

Mechanism (adapter-nextjs 14.0.21, src/utils.ts)

The adapter has no bundler-independent public/ copy:

  • .next/static is copied explicitly and unconditionally (utils.ts ~148-150: copy(staticDirectory, opts.outputStaticDirectoryPath, ...)) — which is why /_next/static always works.
  • public/ is only handled by the generic copyResources() (utils.ts ~163-181), copying top-level app-dir entries into standaloneAppPath gated by existsInOutputBundle — i.e. it relies on Next's standalone output already containing (or correctly receiving) public/.
  • Turbopack standalone output includes public/; webpack standalone documentedly does not ("This minimal server does not copy the public or .next/static folders by default" — Next docs). In a monorepo layout (.next/standalone/apps/web/..., driven by outputFileTracingRoot), the fallback copy does not land where the standalone server.js resolves public/ from, so the runtime ends up with an empty/near-empty public/.

The diagnostic tell: the only public file that served in our production deploys was the single file NFT-tracing happened to pull into standalone (a route reads it at runtime) — proving the standalone serving path works and the directory simply wasn't populated.

Repro context

  • @apphosting/adapter-nextjs 14.0.21 (current latest; buildpack default via npm view), Next.js 16.1.6, next build --webpack, output: 'standalone', pnpm workspace + turbo.json at repo root (monorepo mode, GOOGLE_BUILDABLE=apps/web).
  • Result: .next/standalone/apps/web/public/ contains only NFT-traced files; all other public assets 404 from the deployed Cloud Run service.

Workaround we ship

postbuild npm hook that performs the documented Next standalone copy:
copy public/.next/standalone/<monorepo-path>/public and .next/static.next/standalone/<monorepo-path>/.next/static. With public/ pre-placed, the adapter packages it and assets serve.

Suggested fix

Copy public/ explicitly and unconditionally into standaloneAppPath/public (mirroring the existing explicit .next/static copy), independent of bundler. The e2e suite currently tests neither public-asset serving nor a monorepo layout — both would have caught this.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions