How to host a Next.js app on your own domain

Vercel is the easy home for Next.js, made by the same people. On any other host the app is a Node.js server you build and start yourself. Here is how, and the part that catches people on small plans: building takes far more memory than running.

8 min readIntermediateUpdated

This uses Next.js 16 with the App Router; everything here also works with the Pages Router.

Do you need a server at all?

If every page can be built ahead of time, set output: "export" in next.config and next build writes plain files to out, which any static host serves; hosting a static website covers that. Anything that runs on each request needs the Node.js server: route handlers, server actions, pages rendered per visit, image optimisation, and revalidation.

1. Build, then start

A Next.js app runs in two steps: next build compiles it into .next, and next start serves that build. next dev is for your laptop only. Tell next start which port and address to use in package.json:

JSON
"scripts": {
  "dev": "next dev",
  "build": "next build",
  "start": "next start -p ${SERVER_PORT:-3000} -H 0.0.0.0"
}

-p is the port the host gives you (SERVER_PORT on SnowServers, often PORT elsewhere; 3000 if neither is set). -H 0.0.0.0 listens on every interface; an app listening only on localhost inside a server starts fine and nobody can reach it. The ${SERVER_PORT:-3000} form needs a Linux shell, which is what a server runs scripts with; on Windows, try it with npx next start -p 3000.

2. The build needs far more memory than the app

On our test machine, building a fresh create-next-app project (Next.js 16.4) used more than 1.4 GB across its processes. The built app then ran in about 80 MB. Bigger apps need more for both, but the gap stays: a host plan that runs your app comfortably may be too small to build it, and a build that runs out of memory is stopped part-way, often with nothing but "Killed" in the log.

So on a small plan, build somewhere with memory to spare, your own computer or a CI job, and upload the result. Next.js has an output made for exactly that.

3. Standalone output: build once, upload the result

With output: "standalone", next build also writes .next/standalone: a server.js and only the parts of node_modules the app really uses. For the fresh test app that was 33 MB instead of 476 MB.

JavaScript
// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  output: "standalone",
};

export default nextConfig;

Two folders are left out of it on purpose, and the site has no CSS or images without them. Copy them in after every build:

Terminal
npm run build
cp -r public .next/standalone/public
cp -r .next/static .next/standalone/.next/static

In PowerShell: Copy-Item -Recurse public .next\standalone\public and Copy-Item -Recurse .next\static .next\standalone\.next\static.

server.js reads its port from PORT and its address from HOSTNAME. Containers often set HOSTNAME to their own name, so set both yourself with a three-line start.js next to server.js:

JavaScript
// start.js: run the standalone server on the port the host gives
process.env.PORT = process.env.SERVER_PORT || process.env.PORT || "3000";
process.env.HOSTNAME = "0.0.0.0";
require("./server.js");

Test it the way the server will run it: cd .next/standalone, then node start.js, and open http://localhost:3000. Everything in .next/standalone is what you upload.

4. Environment variables

  • NEXT_PUBLIC_ variables are fixed at build time. They are written into the JavaScript sent to browsers, so set them where you build, and rebuild to change them. Never put a secret in one.
  • Everything else is read when the app runs. A .env file in the folder the server starts from is loaded, including in the standalone folder, and real environment variables take precedence. Keep .env out of Git.

5. Behind HTTPS

Server actions refuse a form posted from another site: they compare the browser's Origin with the host the request was for, which behind a proxy comes from X-Forwarded-Host or Host. With a proxy that passes the visitor's host on, as ours does, nothing needs setting. If the form and the app are on different domains, list the form's domain in experimental.serverActions.allowedOrigins.

Hosting it on SnowServers

Choose a plan on the website plans page and pick A website and Node.js at checkout. Then one of two ways:

  1. Upload a standalone build (any plan). Upload everything in .next/standalone to the top of the server in Files, or over SFTP for a quicker upload. Delete the package.json that came with it: the folder already holds the packages it needs, and without it nothing is installed again. Then set Start file on the Startup tab to start.js and restart. Repeat the upload for each new version.
  2. Build on the server (a plan with room for your build). Upload the project or set Git repository on the Startup tab, and make the start script build first: "start": "next build && next start -p $SERVER_PORT -H 0.0.0.0". Packages install when package.json changes, and every restart builds the current code, so a Git push followed by a restart deploys it.

Then add your domain on the Website tab, with the DNS records it shows, and HTTPS follows on its own. The website docs cover the rest.

Common problems

  • "Could not find a production build in the '.next' directory": next start ran before next build, or the build failed. Read the lines above it in the console.
  • The build stops with "Killed", or exit code 137: it ran out of memory. Build elsewhere and upload the standalone output, or move to a bigger plan.
  • The standalone site has no styling or images: .next/static or public was not copied into the standalone folder.
  • "This site is not answering": the app is listening on localhost or the wrong port. Check -H 0.0.0.0 and -p, or start.js.
  • A change to a NEXT_PUBLIC_ value does nothing: those are baked in when building. Rebuild.

Press / or Ctrl K on any page to search.