Deploying the Next.js Edition
This edition server-side renders, so it needs a Node process — not a static file host.
Overview
You have three options, in increasing order of control:
| Option | Good for | What you provide |
|---|---|---|
| Managed platform (Vercel, Netlify, Render, Railway, Fly) | Fastest path; zero infrastructure | A repo. The platform detects Next and runs build + start. |
| Container (Cloud Run, ECS, Kubernetes, any Docker host) | Portability, reproducible builds | The Dockerfile shown below. |
| Plain Node server (VPS, bare metal) | Full control, existing infrastructure | Node 20+, a process manager, and a TLS terminator in front. |
All three run the same server. Nothing about the app is host-specific — no adapters, no platform plugins, no vendor SDK.
Architecture & files
next.config.ts sets output: 'standalone', so npm run build emits a self-contained server at
.next/standalone/ that carries only the dependencies actually traced from your imports. That folder is
roughly an order of magnitude smaller than a full node_modules, and it needs no install step to run.
Two directories are deliberately excluded from the standalone output, because they are served from disk rather than imported:
| Must be copied beside the server | Why |
|---|---|
.next/static | The client bundles, hashed and immutable. |
public | Avatars, flags, illustrations, favicon. |
Forgetting either produces a site that renders but has no styling or images — the single most common standalone deployment mistake.
The build also runs codegen first (wired to prebuild), which compiles the translation
message bundles used by the localized routes.
Usage
Managed platform
Point the platform at the project, set the environment variables from the
configuration table, and let it run npm run build and npm start. Set
the Node version to 20 or newer.
Plain Node server
npm ci
npm run build
npm start # honours PORT; defaults to 3000Put a reverse proxy (nginx, Caddy) in front for TLS, and keep the process under a supervisor
(systemd, pm2) so it restarts on failure.
Container
A production Dockerfile, using the standalone output:
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
FROM node:22-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Values baked into the client bundle must be present NOW, not at runtime.
ARG NEXT_PUBLIC_SITE_URL
ARG NEXT_PUBLIC_API_BASE_URL
ENV NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL \
NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
NEXT_TELEMETRY_DISABLED=1
RUN npm run build
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production PORT=8080 HOSTNAME=0.0.0.0
RUN addgroup -g 1001 -S nodejs && adduser -u 1001 -S nextjs -G nodejs
COPY --from=build --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=build --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=build --chown=nextjs:nodejs /app/public ./public
USER nextjs
EXPOSE 8080
CMD ["node", "server.js"]Build and run it:
docker build -t luminaux-next --build-arg NEXT_PUBLIC_SITE_URL=https://example.com .
docker run -p 8080:8080 luminaux-nextHOSTNAME=0.0.0.0 matters: the default binding is not reachable from outside the container.
Configuration & customization
Copy .env.example to .env.local and fill it in. Which tier a variable belongs to decides where you
set it, and getting it wrong fails silently:
| Variable | Tier | Notes |
|---|---|---|
NEXT_PUBLIC_API_BASE_URL | Build | REST backend. Defaults to /api. |
NEXT_PUBLIC_SITE_URL | Build | Absolute public origin. Used by sitemap.xml, robots.txt and the OpenGraph tags — if it is wrong or missing, those emit localhost URLs. |
NEXT_PUBLIC_GTM_ID | Build | Google Tag Manager container. Unset means analytics is fully inert: no script injected, nothing sent. |
NEXT_PUBLIC_BASE_PATH | Build | Set only when serving from a subpath; mirror it with basePath in next.config.ts. |
AUTH_REQUIRED | Runtime | true requires a session for every in-shell route. Off by default so the app is browsable out of the box. |
Build tier means the value is compiled into the JavaScript bundle. Only the NEXT_PUBLIC_ prefix is
exposed to the browser at all, and setting one of those on an already-built image or a running container
has no effect — you must rebuild. Runtime tier is read when the server starts, so AUTH_REQUIRED can
be flipped by restarting with a different value.
Notes & gotchas
Do not cache the HTML. This is the one that will bite you. The shell's first paint is driven by the visitor's own cookies — theme, design skin, OLED mode and layout — so that the design they chose arrives in the very first byte with no flash. Two consequences:
- A CDN or reverse proxy that caches HTML without including
Cookiein the cache key will serve one visitor's design to everybody else. Treat the HTML as private and per-visitor. - A proxy that strips cookies before they reach the app breaks the feature completely: every visitor
gets the default design and then jumps to their own after hydration, and
AUTH_REQUIREDcan never see a session. Some CDN configurations do this by default in order to make responses cacheable — check before assuming, because the symptom looks like a slow theme toggle rather than a proxy problem.
/_next/static/** is content-hashed and immutable, so cache it as aggressively as you like. It is only
the HTML that must stay uncached.
Routes are dynamic on purpose. Because the shell reads cookies, its routes are marked dynamic in the build output rather than pre-rendered. That is correct for a per-visitor admin surface, not a misconfiguration to fix.
Verify a deployment in two commands. The first proves the server is up; the second proves cookie-driven rendering survived your proxy — the check worth keeping in a smoke test:
curl -sI https://your-domain.com/en | head -1
curl -s -H 'Cookie: theme=dark; design_skin=graphite' https://your-domain.com/en \
| grep -oE 'data-skin="[^"]*"'The second must report graphite, and the same response's html tag must carry the dark class. If you
get default, something between the browser and the app is dropping cookies.
Node 20 or newer is required. Older runtimes fail at build time.
Related
Server Rendering & First Paint
why the shell's routes are dynamic, and the cookies the first byte depends on
Authentication & Route Guard
AUTHREQUIRED, the Server Actions, and the httpOnly session cookie
Localized Routing
the [locale] URL segment and the message bundles the build compiles
Route Handlers & Server Data
the /api/ endpoints and the backend swap point
Getting Started
install, scripts, and the project layout
Was this page helpful?
