Deploying Next.js with Server Rendering (and When to Use Static Export)

A Next.js 16 app with a server-rendered page, a prerendered page and a route handler, deployed from GitHub. Then one config line that cut the image from 201 MB to 74 MB.

Deploying Next.js with Server Rendering (and When to Use Static Export)
On this pageShow
  1. What you will build
  2. Before you start
  3. Step 1: Create the app
  4. Step 2: Add the three routes
  5. Step 3: Push it to GitHub
  6. Step 4: Deploy it
  7. Step 5: Check what runs where
  8. Step 6: Make the image smaller with standalone output
  9. When to use static export
  10. Troubleshooting

To deploy Next.js with server rendering on Light Cloud, push the project to GitHub and click Deploy from GitHub. Light Cloud detects Next.js, runs next build and serves the app with next start in a container, so dynamic pages, server components and route handlers work as they do on your machine. Add output: "standalone" to next.config.ts for a much smaller image. If your site needs no server at all, output: "export" turns it into a static site served from the edge.

In my run output: "standalone" shrank the image from 201 MB to 74 MB.

What you will build

A Next.js 16 app with the three kinds of routes most apps mix:

  • / is rendered on the server for every request, and shows when.
  • /about is prerendered once, at build time.
  • /api/time is a route handler that returns JSON.

The live Next.js home page: Hello from Next.js on Light Cloud, rendered on the server at a timestamp

Source code: github.com/light-cloud-com/tutorial-nextjs-app.

Before you start

  • Node.js 20.9 or later; I used Node.js 24.
  • Git and a GitHub account.
  • A free Light Cloud account at console.light-cloud.com with GitHub connected. The Free plan includes three server apps, enough for a server-rendered app.
  • A terminal with curl. On Windows, use PowerShell 7 and curl.exe, which ships with Windows 10 and 11.

Step 1: Create the app

terminal
$ npx create-next-app@latest tutorial-nextjs-app --yes
Initialized a git repository.
Success! Created tutorial-nextjs-app at /Users/you/tutorial-nextjs-app
$ cd tutorial-nextjs-app

--yes takes the defaults: TypeScript, Tailwind CSS, ESLint and the App Router. Output is trimmed. I got Next.js 16.3.6 and React 19.2.8.

Step 2: Add the three routes

Replace the starter home page with one that renders on every request:

app/page.tsx
tsx
import Link from "next/link";

// Render this page on the server for every request, not once at build time.
export const dynamic = "force-dynamic";

export default function Home() {
  const renderedAt = new Date().toISOString();

  return (
    <main className="mx-auto max-w-xl p-8 space-y-4">
      <h1 className="text-3xl font-bold">Hello from Next.js on Light Cloud</h1>
      <p>
        This page was rendered on the server at{" "}
        <time className="font-mono">{renderedAt}</time>. Reload it and the time changes.
      </p>
      <p>
        <Link className="underline" href="/about">About</Link> is built once, at deploy time.{" "}
        <Link className="underline" href="/api/time">/api/time</Link> is a route handler.
      </p>
    </main>
  );
}

A page with no dynamic data, which Next.js prerenders:

app/about/page.tsx
tsx
import Link from "next/link";

// No dynamic data here, so Next.js renders this page once, during `next build`.
export default function About() {
  const builtAt = new Date().toISOString();

  return (
    <main className="mx-auto max-w-xl p-8 space-y-4">
      <h1 className="text-3xl font-bold">About</h1>
      <p>
        This page was prerendered at build time, at{" "}
        <time className="font-mono">{builtAt}</time>. Reloading does not change it.
      </p>
      <p>
        <Link className="underline" href="/">Home</Link>
      </p>
    </main>
  );
}

And a route handler:

app/api/time/route.ts
ts
// A route handler: an API endpoint that lives next to the pages.
export const dynamic = "force-dynamic";

export function GET() {
  return Response.json({
    now: new Date().toISOString(),
    node: process.version,
  });
}

Build it once to see how Next.js classifies each route:

terminal
$ npm run build
Route (app)
┌ ƒ /
├ ○ /_not-found
├ ○ /about
└ ƒ /api/time

○  (Static)   prerendered as static content
ƒ  (Dynamic)  server-rendered on demand

Output is trimmed. Routes marked Dynamic need a server; that is what decides between server rendering and static export (see When to use static export).

Step 3: Push it to GitHub

create-next-app already made the repository and a first commit:

terminal
$ git add -A
$ git commit -m "Server-rendered home, prerendered about, route handler"
# Create an empty repository named tutorial-nextjs-app on github.com/new first.
$ git remote add origin https://github.com/YOUR-USERNAME/tutorial-nextjs-app.git
$ git push -u origin main
To https://github.com/YOUR-USERNAME/tutorial-nextjs-app.git
 * [new branch]      main -> main
branch 'main' set up to track 'origin/main'.

Step 4: Deploy it

In the Light Cloud console, click New..., then Deploy from GitHub, and pick tutorial-nextjs-app. Light Cloud shows Next.js / Fullstack, with SSR mode (like Vercel) in the reasons, and Runs on a server:

The detection banner Next.js Fullstack, SSR mode like Vercel, with Runs on a server highlighted

Below it the console notes that server-rendered frontends run on the Micro size, scale from 0 to 3 instances, and are served through the edge. Click Deploy. The first build installs everything and runs next build; in my run the site was live about 4 and a half minutes after the click.

The Production overview of tutorial-nextjs-app with the Deployed badge and the URL card highlighted

Step 5: Check what runs where

terminal
$ curl https://main-tutorial-nextjs-app-yourworkspace.light-cloud.io/api/time
{"now":"2026-09-25T14:21:44.274Z","node":"v24.21.0"}

The route handler runs on Node.js 24 on the server. Reload / twice and the time changes each time (14:21:44.511 and 14:21:47.055 in my run). Reload /about and it stays at the moment the site was built (14:18:39.014), because it is a prerendered file.

Next.js puts JavaScript and CSS under /_next/static, with content hashes in the file names. Light Cloud treats them specially: the first request is answered by your app, and after that the file comes from the edge store, marked cacheable for a year.

To see it, open your site, open the browser's developer tools (F12, or Cmd+Option+I on a Mac), go to the Network tab and reload the page. Click one of the .js files, for example 310vm2bl3xxpt.js (your names will differ), and look at its Response Headers:

text
cache-control: public, max-age=31536000, immutable
x-lc-asset: r2

x-lc-asset: r2 means the file came from the edge store, not from your app. Your server only renders pages; the heavy static files stop reaching it.

Step 6: Make the image smaller with standalone output

By default the container holds the whole project, including every package in node_modules. output: "standalone" makes next build trace which files the server really needs and copy just those into .next/standalone:

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

const nextConfig: NextConfig = {
  // Build a self-contained server with only the files it needs.
  output: "standalone",
};

export default nextConfig;

Commit and push. Light Cloud reads the setting and builds a leaner image that runs server.js directly. From my two deploys:

Defaultoutput: "standalone"
Image size201 MB74 MB
New instance to ready2.6 s1.2 to 1.9 s

Smaller images start faster on a cold start, and with Min instances at 0 that is the delay a first visitor feels. There is no downside for a typical app, so use standalone for server-rendered Next.js on Light Cloud.

When to use static export

If npm run build lists only Static routes and no Dynamic ones, nothing needs a server. Then static export is simpler and cheaper:

next.config.ts
ts
const nextConfig: NextConfig = {
  output: "export",
};

Light Cloud's detection reads next.config and, with output: "export", builds the out folder and serves it from the edge like any static site: no container, no cold start, and it counts as a static site on your plan. Static export cannot use per-request rendering, route handlers that run at request time, server actions, or the built-in image optimisation.

Your app hasUse
Only prerendered pagesoutput: "export"
Dynamic pages, route handlers, server actions, middlewareserver rendering with output: "standalone"

Troubleshooting

The build fails after adding output: "export"

Static export needs every page, and every value of a dynamic route such as [slug], to be known at build time. Either add generateStaticParams to dynamic routes, or remove output: "export" and deploy with server rendering.

More tutorials