Skip to content

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

@Agent-Aurelius

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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions