Dockerizing Next.js Project with Configurable Client-side Environment Variables

Nonthapat Kaewamporn
5 min readNov 8, 2023

--

NPM download statistics of the top meta-web framework

Next.js in undoubtedly the king of the meta-framework for modern web development based on the number of downloads compared to other frameworks and Vercel has been doing amazing things to keep the developer experience (DX) of frontend world going.

However, deploying on Vercel might not be suitable for every use-case. There might be some scenarios where you want to deploy the project on your private cloud (AWS, Azure, GCP, etc.) or somewhere else. Lets face it, writing Dockerfile for Next.js sucks. I’ve done side projects on dockerizing Next.js for deploying to Amazon ECS (I know there’s also AWS Amplify option available, I have my reasons to do it this way so don’t bash at me.) and also a few commercial projects to deploy Next.js to Google Cloud Run. I’ve spent roughly 5–10 hours just debugging all the issues related to Next.js and I almost went mental when things just straight up does not work in the container but works perfectly when local hosting or deploying to Vercel. One of the most frustrating issue I’ve faced is working with client-side variables.

In a typical build process, Next.js will bundle all client-side environments on build time as a static HTML. This makes client environment non-configurable after build process.

There’s some case that we don’t want this to happen. Maybe we want to have different environments sharing the same base image such as same frontend pointing to different backend environment. Then how would we make that possible?

  • One way we could do it is to have multiple build pipeline on the same code to produce environment-specific image. (But that’s just asking for loads of maintenance on every process in the workflow.)
  • The better way is just to create a base image and replace all static values of environment variables to your preferred values during run time.

Lets walk step by step on how to do it:

.env.production file
  • Create .env.production in the project that holds the same environment name as your .env.local file. You can set any values to the .env.production as it’ll be replaced later on. For simplicity sake I’ll just repeat the name of the variable.
(Left) Example of using client side variable. (Right) Resultant HTML in browser source after bundling.
  • During build step, next build will consume .env.production and replace all NEXT_PUBLIC_<NAME> into the value referenced from the environment file and bundle into static HTML. Hence the Docker image content will be filled with the arbitrary values you set in .env.production
// entrypoint.sh

#!/bin/bash
envFilename='./.env.production'
nextFolder='./standalone/.next/'

while read -r line; do
# no comment or not empty
if [ "${line:0:1}" == "#" ] || [ "${line}" == "" ]; then
continue
fi

# split
configName="$(cut -d'=' -f1 <<<"$line")"
configValue="$(cut -d'=' -f2 <<<"$line")"
# get system env
envValue=$(env | grep "^$configName=" | grep -oe '[^=]*$')

# if config found && configName starts with NEXT_PUBLIC
if [ -n "$configValue" ] && [ -n "$envValue" ]; then
# replace all
echo "Replace: ${configValue} with ${envValue}"
find $nextFolder \( -type d -name .git -prune \) -o -type f -print0 | xargs -0 sed -i "s#$configValue#$envValue#g"
fi
done <$envFilename

echo "Starting Nextjs"
exec "$@"
  • Add entrypoint.sh to project.
// Dockerfile

FROM node:18-alpine AS dependencies
RUN apk add --no-cache libc6-compat
WORKDIR /home/app
COPY package.json ./
COPY package-lock.json ./
RUN npm i

FROM node:18-alpine AS builder
WORKDIR /home/app
COPY --from=dependencies /home/app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED 1
ARG NODE_ENV
ENV NODE_ENV=”${NODE_ENV}”
RUN npm run build

FROM node:18-slim AS runner

WORKDIR /home/app
ENV NEXT_TELEMETRY_DISABLED 1
ENV NODE_ENV=production
COPY --from=builder /home/app/.next/standalone ./standalone
COPY --from=builder /home/app/public /home/app/standalone/public
COPY --from=builder /home/app/.next/static /home/app/standalone/.next/static
COPY --from=builder /home/app/scripts/entrypoint.sh ./scripts/entrypoint.sh
COPY --from=builder /home/app/.env.production ./.env.production

ENV HOSTNAME "0.0.0.0"

RUN chmod +x ./scripts/entrypoint.sh

ENTRYPOINT [ "./scripts/entrypoint.sh" ]

CMD ["node", "./standalone/server.js"]
  • Add Dockerfile to project. Credit to this article by for best practice on Docker image optimization in Next.js
// docker-compose.yaml

version: '3.1'

services:
web:
build:
context: .
dockerfile: Dockerfile
ports:
- 3000:3000
environment:
NEXT_PUBLIC_ENV_1: REAL_VALUE_1
NEXT_PUBLIC_ENV_2: REAL_VALUE_2
  • For simplicity sake, I created a docker-compose.yaml file and inject environment variable that way. But you can also do in a normal docker run fashion.
Entry point script replacing temporary variable with real variable
  • On running Docker image, it’ll run entrypoint.sh script on container creation. This script will be the responsible for replacing the arbitrary values with real environment variable. The shell script will look for values which corresponds to the .env.production and replace it with value of the environment name of the template env value. (It’s quite hard to explain but I’ll give you a simple example. if you have NEXT_PUBLIC_VARIABLE_1=PLACEHOLDER_VALUE in .env.production then PLACEHOLDER_VALUE will be embedded in HTML. So we have to set client variable as PLACEHOLDER_VALUE=REAL_VALUE so we can replace PLACEHOLDER_VALUE as REAL_VALUE . That’s why initially I set the placeholder value as the name itself to make things simpler. I hope this explanation is clear)
Client Side Env Variable
  • Client Environment is now set on run time!

With these simple steps, we now have a docker image that could inject client-side environment variable during run time! One thing to note is that we don’t have to do anything on server environments since server environment variable will be fetched on run time so normal environment injection will work. You can find all the source code related to this article in this Github Repository.

If you find this article helpful then feel free to share this article to others!

I would like to credit:

In case you want to contact me, you can reach out to me via LinkedIn.

Happy coding!

--

--