Deploying the Next.js App Your AI Built — Three Things That Will Bite

·6 min read·Updated
nextjsvibe-codingaidockerdeploymenthow-to

Ask v0, Lovable, Bolt, or Cursor for a web app and you'll almost certainly get Next.js. It runs beautifully with npm run dev and then meets three specific problems on the way to being hosted: the production build is memory-hungry and is the first thing that fails, NEXT_PUBLIC_ variables are frozen into the bundle at build time — so setting them as app environment variables does nothing, and the default start command binds in a way that won't be reachable. None of these are the model's fault; they're facts about Next.js meeting a container. All three are fixable in about twenty minutes.

1. The build is what fails first

Builds here run in their own container with roughly 0.75 GB of RAM and half a vCPU — less than your app gets at runtime. next build type-checks, compiles, bundles, and pre-renders pages, and on a non-trivial app it wants more than that.

The failure is unhelpful: the build stops partway through, often during "Creating an optimized production build", with no explicit out-of-memory message. It looks like a hang or a mysterious crash.

Three ways out, in order of how much I'd recommend them:

  • Build in CI and push the image. GitHub Actions runners have plenty of memory, and this also fixes problem #2 below. It's the answer for anything beyond a small app.
  • Trim what the build has to do. Remove unused dependencies, and check you aren't pre-rendering hundreds of pages you don't need yet.
  • Cap Node's heap with NODE_OPTIONS=--max-old-space-size=512 in the build stage. This can help Node garbage-collect rather than grow into a wall, but it can't give you memory the container doesn't have.

More on the build limit and when to move off-platform.

2. NEXT_PUBLIC_ variables never reach the browser

This is the one that produces a truly baffling afternoon, and it's the interaction of two facts.

Fact one, from Next.js. A NEXT_PUBLIC_ variable isn't read at runtime. Next.js "can 'inline' a value, at build time, into the js bundle that is delivered to the client, replacing all references to process.env.[variable] with a hard-coded value" (Next.js docs). The docs are explicit about the consequence: "After being built, your app will no longer respond to changes to these environment variables… all NEXT_PUBLIC_ variables will be frozen with the value evaluated at build time."

Fact two, about the platform. The image build doesn't receive your app's environment variables, and there's no mechanism to pass build arguments.

Put them together: if you set NEXT_PUBLIC_API_URL as an environment variable on your app and the platform builds your Dockerfile, that variable is not present during next build. It gets inlined as undefined. Your app starts fine, the server side works, and the browser makes requests to undefined/api/....

There's no error. The environment variable is right there in the panel. It simply wasn't there at the moment that mattered.

What to do instead, in order of preference:

  • Build in CI, where you can set build-time variables, and push the resulting image. One change fixes this and the memory problem.
  • Read the value on the server at runtime and pass it down. Non-public variables are read at runtime normally, so a server component or route handler can read process.env.API_URL and hand the value to the client as a prop or a response. The Next.js docs point at exactly this: "If you need access to runtime environment values, you'll have to setup your own API to provide them to the client."
  • Hardcode genuinely static values. If it's a public API base URL that never changes between environments, putting it in next.config is honest and simpler than pretending it's configurable.

Worth saying plainly: NEXT_PUBLIC_ is for values that are safe in a browser. Never put an API key behind that prefix — it ends up in JavaScript anyone can read. If your AI wrote NEXT_PUBLIC_OPENAI_API_KEY, that's a key to rotate, not a variable to fix.

3. Bind the server properly, and don't ship the dev server

Generated Dockerfiles often end with the command from the README:

CMD ["npm", "run", "dev"]

That's the development server — hot reload, file watchers, unoptimised output. Use the production one, bound to all interfaces:

CMD ["npm", "start", "--", "-H", "0.0.0.0", "-p", "3000"]

Then bind container port 3000 as HTTPS in the panel. Since nothing probes your app, a server listening on the wrong interface deploys "successfully" and never answers — the same silent failure as the localhost problem generally.

Use standalone output

Next.js can emit a self-contained server bundle that includes only the files and dependencies actually needed to run. In next.config.js:

module.exports = {
  output: 'standalone',
}

Then your runtime stage copies that output instead of the whole node_modules tree, which is usually the single biggest reduction available for a Next.js image. That matters here because image size is deploy downtime — every redeploy pulls the image before your app starts.

A multi-stage Dockerfile in outline:

FROM node:22-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:22-slim
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/.next/standalone ./
COPY --from=build /app/.next/static ./.next/static
COPY --from=build /app/public ./public
CMD ["node", "server.js", "-H", "0.0.0.0"]

Dependencies installed before source is copied, so a code change reuses the install layer. Remember the build stage still runs on the platform unless you build in CI — the memory limit applies to it either way.

Does it need to be Next.js at all?

Worth asking, because AI tools reach for it by default. If what you built is a static marketing page or a small dashboard with no server-side logic, next build && next export — or a plain Vite build served by any tiny static server — sidesteps most of this. The full Next.js server is worth running when you're using server components, route handlers, or server-side rendering, and is overhead when you aren't.

FAQ

Why is my NEXT_PUBLIC_ variable undefined in the browser?

It's inlined during next build, and the platform's build doesn't receive your app's environment variables. Build in CI, or pass the value from the server at runtime.

Why does my Next.js build fail on the platform but work locally?

The build container has around 0.75 GB of RAM — less than your laptop and less than your app gets. Build in CI and push the image if you exceed it.

What start command should I use?

The production server bound to all interfaces: npm start -- -H 0.0.0.0 -p 3000. Never npm run dev.

Is NEXT_PUBLIC_ safe for API keys?

No. Those values are inlined into JavaScript sent to the browser. Anything with that prefix is public.

How do I make my Next.js image smaller?

Set output: 'standalone' and copy that output in a multi-stage build instead of the full dependency tree.

Can I change a NEXT_PUBLIC_ value without rebuilding?

No. It's compiled in. Read the value server-side at runtime and pass it to the client if it needs to vary.

Wrapping up

Next.js is a good target for AI-generated apps, and everything above is a one-time cost. Two of the three problems disappear together if you build in CI and push an image, which is the setup I'd reach for on anything that isn't tiny. If you'd rather let the platform build, use standalone output, keep the build lean, and remember the rule that catches almost everyone: anything with NEXT_PUBLIC_ in front of it is decided when the build runs, not when the app starts.