Skip to main content
LinkGro

Deploying Next.js on shared hosting: what actually works

Shared hosting can run Next.js, but not the way the docs assume. What Passenger expects, why the build succeeds and the app still will not boot, and the monorepo trap.

7 min read

Most guides assume you are deploying Next.js to Vercel, a container, or a VPS you control. Managed shared hosting is none of those, and the failure it produces is unhelpful: the build passes, the deploy reports success, and the site returns 403. This is what is actually happening.

Shared hosting runs Passenger, not `next start`

Hosts that offer Node.js on shared plans generally run Phusion Passenger behind LiteSpeed or Apache. Passenger does not run your `start` script. It reads an `.htaccess`, finds an application root, and `require()`s a single entry file — almost always `server.js`.

That is the whole problem. Next has no `server.js`. A standard `next build` produces `.next`, which contains no entry point Passenger can require. So the build genuinely succeeds, and startup fails with a module-not-found error naming a file that was never going to exist.

If your deploy logs show a successful build and the site still 403s, read the generated .htaccess before changing anything else. It names the exact path Passenger is looking for.

`output: "standalone"` is what produces an entry point

Setting `output: "standalone"` in `next.config.ts` makes the build emit `.next/standalone/server.js` along with a traced, minimal `node_modules`. That is the directory the host should publish — not `.next`. Getting this one setting wrong accounts for most of the time people lose here.

The monorepo trap

In a workspaces monorepo the file tracer roots at the repository, because that is where the lockfile is. The output nests accordingly: the server lands at `.next/standalone/apps/web/server.js` while the traced `node_modules` stays at `.next/standalone/node_modules`.

Publishing either directory alone fails. The outer one hides `server.js` a level too deep for Passenger; the inner one loses the dependencies. The fix is a post-build step that flattens the nested app directory up into the standalone root so the entry file and its modules are siblings.

Standalone leaves your assets behind

Standalone output deliberately excludes `public/` and `.next/static/` — Next expects the deployer to copy them, on the assumption a CDN serves them. On shared hosting there is no CDN in front, so the app boots and every stylesheet, script and image 404s.

There is a second copy needed that nobody mentions. Passenger conventions expect the web server's document root to be the app's `public` directory, but managed hosts usually point it at the app root instead. A request for `/logo.svg` then resolves against the wrong directory and 404s before Node ever sees it. Mirroring `public/*` up to the app root fixes it, and is faster anyway — the web server answers without waking the Node process.

A checklist that works

  • Set `output: "standalone"` and publish `.next/standalone`, not `.next`
  • Flatten the nested app directory in a post-build step if you use workspaces
  • Copy `public/` and `.next/static/` into the standalone output
  • Mirror `public/*` at the app root as well, for the document-root mismatch
  • Create `tmp/restart.txt` — Passenger watches it to trigger reloads
  • Set every `NEXT_PUBLIC_*` variable before building; they are inlined at build time and cannot be fixed afterwards

What shared hosting still cannot do

Getting Next.js running is not the same as it being the right home. Shared plans generally do not offer MongoDB or Redis, so those move to managed services. More importantly, in-process schedulers and queue workers stop silently when a managed process is recycled — there is no error, the jobs simply stop running. If your backend depends on either, that belongs somewhere with a process you control.

Working on something this touches?

If one of these matched a problem you are having, the conversation is usually shorter than the article.