Dockerizing Next.js Project with Configurable Client-side Environment Variables
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:
- 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.
- During build step,
next build
will consume.env.production
and replace allNEXT_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 normaldocker run
fashion.
- 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 haveNEXT_PUBLIC_VARIABLE_1=PLACEHOLDER_VALUE
in.env.production
thenPLACEHOLDER_VALUE
will be embedded in HTML. So we have to set client variable asPLACEHOLDER_VALUE=REAL_VALUE
so we can replacePLACEHOLDER_VALUE
asREAL_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 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:
- Gourab Chattopadhyay for the article written about Writing Optimized Dockerfile for Next.js which acts as the base Dockerfile.
- Renato Pozzi for the article on injecting environment variable
In case you want to contact me, you can reach out to me via LinkedIn.
Happy coding!