
Handling Dynamic NEXT_PUBLIC Variables in Next.js Docker Images

When deploying a Next.js app using a Docker image, you might notice that changes to your NEXT_PUBLIC_*
environment variables don’t take effect even after rebuilding the container.
That’s because Next.js embeds all public environment variables directly into the build output. Once your Docker image is built, those values become static and can’t be changed at runtime.
We ran into this issue while working on dFlow, where several features depend on NEXT_PUBLIC
variables. After exploring different approaches, here’s what we found 👇
React Context Approach
One solution is to pass environment variables from the server to the client through React Context.
We can fetch them server-side, pass them as props to a provider, and use them across the app.
- Let's first create a React Context which we can wrap our entire layout.
1// EnvironmentVariablesProvider.tsx2'use client'34import React, { createContext, use } from 'react'56import { Branding } from '@/payload-types'78type VariablesType = {9 NEXT_PUBLIC_WEBSITE_URL: string10}1112const VariablesContext = createContext<13 {environmentVariables: VariablesType}14>(undefined)1516// showing toaster when user goes to offline & online17export const useVariablesContext = ({18 children,19 environmentVariables,20}: {21 children: React.ReactNode22 environmentVariables: EnvironmentVariablesType23}) => {24 return (25 <VariablesContext.Provider value={{ environmentVariables }}>26 {children}27 </VariablesContext.Provider>28 )29}3031export const useVariablesContext = () => {32 const context = use(VariablesContext)3334 if (context === undefined) {35 throw new Error(36 'useVariablesContext must be used within a useVariablesContext',37 )38 }3940 return context41}
- Now let's warp our
layout.tsx
with context.
1// app/layout.tsx2import { EnvironmentVariablesProvider } from '@/providers/EnvironmentVariablesProvider'34export default async function RootLayout({5 children,6}: {7 children: React.ReactNode8}) {9 return (10 <html>11 <body>12 <EnvironmentVariablesProvider environmentVariables={{13 NEXT_PUBLIC_WEBSITE_URL: process.env.NEXT_PUBLIC_WEBSITE_URL14 }}>{children}</EnvironmentVariablesProvider>15 </body>16 </html>1718 )19}
This makes your environment variables accessible across the app
1// use client2import { useVariablesContext } from '@/providers/EnvironmentVariablesProvider'3import Logo from '@/components/Logo'45const Navbar = () => {6 const { environmentVariables } = useVariablesContext()78 return (9 <nav>10 <Link href={environmentVariables.NEXT_PUBLIC_WEBSITE_URL}>11 <Logo />12 </Link>13 </nav>14 )15}1617export default Navbar
However, there’s a catch — using React Context forces components to be client-side, which means you lose server-side rendering (SSR) benefits.
Shell Script Approach (Recommended)
After trying several methods, we found a cleaner and more scalable solution (inspired by the Nhost repo).
- We can replace environment variables dynamically at container startup using a simple shell script.
1# Dockerfile23FROM node:22.12.0-alpine AS base45# Install dependencies only when needed6FROM base AS deps7# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.8RUN apk add --no-cache libc6-compat9WORKDIR /app1011# Install dependencies based on the preferred package manager12COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./13RUN \14 if [ -f yarn.lock ]; then yarn --frozen-lockfile; \15 elif [ -f package-lock.json ]; then npm ci; \16 elif [ -f pnpm-lock.yaml ]; then npm install -g corepack@latest && corepack enable && corepack prepare pnpm@10.2.0 --activate && pnpm i --frozen-lockfile; \17 else echo "Lockfile not found." && exit 1; \18 fi1920# Rebuild the source code only when needed21FROM base AS builder22WORKDIR /app23COPY --from=deps /app/node_modules ./node_modules24COPY . .2526ARG NEXT_PUBLIC_WEBSITE_URL2728ENV NEXT_PUBLIC_WEBSITE_URL=__NEXT_PUBLIC_WEBSITE_URL__2930RUN \31 if [ -f yarn.lock ]; then yarn run build; \32 elif [ -f package-lock.json ]; then npm run build; \33 elif [ -f pnpm-lock.yaml ]; then corepack enable && COREPACK_INTEGRITY_KEYS=0 corepack prepare pnpm@10.2.0 --activate && pnpm run build; \34 else echo "Lockfile not found." && exit 1; \35 fi3637# Production image, copy all the files and run next38FROM base AS runner39WORKDIR /app4041ENV NODE_ENV=production42# Uncomment the following line in case you want to disable telemetry during runtime.43ENV NEXT_TELEMETRY_DISABLED=14445RUN addgroup --system --gid 1001 nodejs46RUN adduser --system --uid 1001 nextjs474849COPY --from=builder /app/public ./public5051# Automatically leverage output traces to reduce image size52# https://nextjs.org/docs/advanced-features/output-file-tracing53COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./54COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static55COPY scripts/entrypoint.sh /app/entrypoint.sh56RUN chmod +x /app/entrypoint.sh5758USER root59EXPOSE 30006061ENV HOSTNAME="0.0.0.0"62ENTRYPOINT ["/app/entrypoint.sh"]
- Now let's create entrypoint.sh which will replace the variables with dynamic values
1// scripts/entrypoint.sh2#!/bin/sh3set -e45# 🔁 Replace placeholders in built output6NEXT_PUBLIC_WEBSITE_URL="${NEXT_PUBLIC_WEBSITE_URL}"78# 🪄 Replace values in built static files9find .next -type f -exec sed -i "s~__NEXT_PUBLIC_WEBSITE_URL__~${NEXT_PUBLIC_WEBSITE_URL}~g" {} +1011# Run your Next.js app12exec node server.js
Now, whenever you run the container, the shell script will dynamically inject your latest environment variables into the built files before starting the app.
✅ Why This Works Well
- No code changes are required in your app
- Environment variables are updated each time the container starts
- Works perfectly in CI/CD environments
- Keeps your Next.js build process unchanged
That’s it for this post!
Thanks for reading — stay tuned and peace ✌️