Next.js and Docker

Bake-time NEXT_PUBLIC_* values, runtime server secrets, and what to do when public env must change without a rebuild.

Edit on GitHub

ArkEnv validates and types your Next.js environment variables. It does not replace Next.js’s build-time model for NEXT_PUBLIC_*. In Docker that means public client values are baked at next build, while server secrets stay injectable at container start.

Nuxt can swap public config without rebuilding because Nitro’s runtimeConfig is a host feature. Next.js freezes NEXT_PUBLIC_* in the client bundle. @arkenv/nextjs respects that boundary instead of shipping a parallel browser global.

Default path: bake public values at build

Enable Next.js standalone output so the image can copy .next/standalone. withArkEnv still validates the schema at build time, so every required key — including server secrets — needs a value during npm run build. Pass a build-only placeholder for secrets; inject the real value when the container starts.

next.config.ts
import type { NextConfig } from "next";
import { withArkEnv } from "@arkenv/nextjs/config";

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

export default withArkEnv(nextConfig);
Dockerfile
FROM node:22-alpine AS build
WORKDIR /app
COPY . .
ARG NEXT_PUBLIC_API_URL
# Placeholder for build-time withArkEnv validation — not the runtime secret
ARG DATABASE_URL=postgres://build:build@127.0.0.1:5432/build
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
ENV DATABASE_URL=$DATABASE_URL
RUN npm ci && npm run build

FROM node:22-alpine
WORKDIR /app
COPY --from=build /app/.next/standalone ./
COPY --from=build /app/.next/static ./.next/static
COPY --from=build /app/public ./public
CMD ["node", "server.js"]
Terminal
# Staging image — public URL frozen into the client bundle
docker build \
  --build-arg NEXT_PUBLIC_API_URL=https://api.staging.example.com \
  -t app:staging .

docker run -e DATABASE_URL=postgres://… app:staging

# Production — different public URL means a different build
docker build \
  --build-arg NEXT_PUBLIC_API_URL=https://api.example.com \
  -t app:prod .

Keep the schema ordinary. withArkEnv codegen maps client keys to process.env.* so Next can inline them:

env.ts
import arkenv from "@/.arkenv";

export const env = arkenv({
  DATABASE_URL: "string.url",
  NEXT_PUBLIC_API_URL: "string.url",
});

DATABASE_URL can change on docker run. NEXT_PUBLIC_API_URL cannot — it was replaced at build time.

Skipping validation at build

If you refuse to pass even a placeholder secret into the build stage, set withArkEnv(nextConfig, { validate: false }). You lose fail-fast checks during next build; runtime (or a separate arkenv check in CI) must catch missing keys instead.

Dynamic public config: keep it off NEXT_PUBLIC_*

When a value must vary per deploy without rebuilding the client bundle, do not put it on a NEXT_PUBLIC_* key. Read it on the server and pass it as props (or serve it from a Route Handler).

env.ts
import arkenv from "@/.arkenv";

export const env = arkenv({
  DATABASE_URL: "string.url",
  // Server-only — not inlined into the client chunk
  API_URL: "string.url",
});
app/page.tsx
import { env } from "../env";
import { ClientWidget } from "./client-widget";

export default function Page() {
  return <ClientWidget apiUrl={env.API_URL} />;
}
app/client-widget.tsx
"use client";

export function ClientWidget({ apiUrl }: { apiUrl: string }) {
  return <p>API: {apiUrl}</p>;
}

One image can then swap API_URL at container start. The browser never needed a build-time public literal. Build still needs placeholders (or validate: false) for every required server key, same as above.

Runtime public injection without rebuilds

If you need client-side NEXT_PUBLIC_* values to change without a rebuild, that transport is outside ArkEnv. Use a dedicated ecosystem library such as next-runtime-env (or a maintained fork), or an entrypoint that rewrites the built JS.

ArkEnv still validates whatever lands in process.env on the server. It does not inject globalThis.__arkenv_env__ or export <ArkEnvScript />.

Why ArkEnv does not ship this

Shipping a runtime public-env global would tax every Next user — including Vercel and other build-per-environment deploys — for a Docker escape hatch that fights Next’s compiler. Nuxt gets no-rebuild public env from Nitro; Next does not, and polyfilling that gap belongs to a dedicated tool, not the validation adapter.

Next steps