Deployment
Romaji to Kana Converter: React & Next.js
Chapter 7 · Deployment
Every prior chapter has been building toward a single deployable thing: one Next.js project
holding the client-side converter, the server-comparison box, and the real
/api/convert route all together. This chapter puts that architecture to its own real
test — building it for production, confirming exactly what does and doesn't survive that build
process, and getting it running behind a real process manager and reverse proxy.
/api/convert at the same time.
Building for Production: The Standalone Output Mode
Next.js's own default production build (next build followed by next start)
still expects the full project, including every package in node_modules, to be present
on the machine that runs it. Next.js's own real, current documentation offers a leaner alternative
specifically for deployment: setting output: 'standalone' in next.config.js
makes next build trace every file each page and route actually needs and copy only
those into .next/standalone, along with a small, self-contained server.js
that can run without next start or a full node_modules install at all.
Building a real, minimal Next.js 16.3.5 project with this setting confirms the documentation's own claim directly — the standalone output holds exactly three things, nothing more:
public/ and .next/static inside the freshly-built standalone
folder confirms both are genuinely absent:
next build doesn't copy them in by default. For a deployment with no separate CDN, they
have to be copied in by hand, after the build:
200 (its HTML is generated by server.js directly), but a real static file
from public/ returns a genuine 404:
Once both folders are in place, the server runs directly with node — no
next start, no full node_modules install required on the deployment
machine:
PORT and HOSTNAME are read directly by server.js itself — a
real, documented mechanism, not something this course is bolting on.
node_modules folders side by side and
measuring their real disk size:
Environment Variables: Two Genuinely Different Lifetimes
Next.js draws a real, load-bearing line between two kinds of environment variable, and this course's
own Chapter 1 finding — that Route Handlers are dynamic by default — turns out to directly explain
which side of that line /api/convert lands on.
NEXT_PUBLIC_ Variables Are Frozen at Build Time
A variable prefixed NEXT_PUBLIC_ is inlined directly into the client-side JavaScript
bundle the moment next build runs — every reference to it is replaced with a literal,
hardcoded string, not a live lookup. Building a real client component that reads
process.env.NEXT_PUBLIC_APP_LABEL, set to "BUILD-TIME-LABEL-XYZ" at build
time, and then searching the actual built output confirms this directly — the literal string is
physically present in a static chunk file, and the variable's own name is nowhere to be found:
Changing a NEXT_PUBLIC_ variable after the build has already happened does nothing —
the value is already compiled into the shipped JavaScript. It needs a real rebuild to take effect.
Server-Only Variables in a Dynamic Route Handler Stay Live
A variable with no NEXT_PUBLIC_ prefix is never sent to the browser at all — it only
ever exists inside process.env on the server. Whether it's evaluated once at build time
or freshly on every request depends on whether the code reading it opts into dynamic rendering — and
Chapter 1 already established that a Route Handler like /api/convert does exactly that,
by default, since Next.js 15. Building a real Route Handler that reads a plain
SECRET_MESSAGE variable, building the app with one value set, then running the finished
standalone server with a genuinely different value in its own environment confirms this
concretely:
process.env read inside it re-reads the environment fresh each
time too — the exact opposite of a NEXT_PUBLIC_ variable's own build-time freeze. In
practice, this means the production server can be started with a real secret — a database URL, an
API key, or (once Chapter 6's own architecture is actually built out) a Redis connection string —
set as a genuine runtime environment variable, changeable per deployment without ever needing a
rebuild.
Process Management with PM2
node .next/standalone/server.js run directly in a terminal dies the moment that
terminal closes, and won't restart itself if it crashes. PM2, the same process manager this site's
own sibling Node-backed courses already use, solves both problems:
PM2 restarts the process automatically if it crashes, keeps it running across the terminal session
ending, and gives a real, queryable process list (pm2 list) and live log stream
(pm2 logs romaji-nextjs) without any custom tooling.
An nginx Reverse Proxy
With the app itself running on port 3000, nginx sits in front of it, handling the public-facing port and (per this site's own HTTPS/TLS Fundamentals course) real TLS termination:
Nothing about this proxy config is specific to this course's own architecture — it's the same real pattern this site has already used for every other Node-backed course — which is itself a quiet confirmation of Chapter 1's own point: this project deploys like any single Node application, because that's genuinely all it is.
Hands-On Exercises
This chapter measured a real ~22.7x reduction in node_modules size using the standalone output mode (432MB full vs. 19MB pruned). Explain, in your own words, why a real production install of this project's own future Redis client (Chapter 6) would make this pruning step matter even more than it already does for the app as it stands today.
📄 View solutionThis chapter showed that skipping the manual public/.next-static copy step produces a real 404 for a static file while the homepage itself still returns 200. Explain why these two things fail differently, tracing the difference back to where each one's own content actually comes from.
📄 View solutionA teammate suggests moving the site's public-facing "converter is temporarily under maintenance" flag into a NEXT_PUBLIC_ environment variable, so it can be toggled without redeploying the client UI. Using this chapter's own real, verified findings, explain why that plan wouldn't actually work the way they expect, and what the real fix would be.
📄 View solutionChapter 7 Quick Reference
- One deploy, not two — the direct payoff of Chapter 1's own single-project architecture, confirmed by having only one process to build and run
- output: 'standalone' — traces and copies only the files actually needed, verified ~22.7× smaller than a full node_modules install
- public/ and .next/static aren't copied automatically — verified: skipping them fails silently, per-asset, not by crashing the whole app
- NEXT_PUBLIC_ variables are frozen at build time — verified: the literal value is baked into a static chunk file; changing it needs a rebuild
- Server-only variables in /api/convert stay live — verified: because the route is dynamic by default (Chapter 1), it re-reads process.env fresh on every request