How to set NEXT_PUBLIC_* environment variables in Docker

Spoiler alert! Its a toxic relationship!

docker, nextjs, react

Nextjs and Docker represented by wojacksNextjs and Docker represented by wojacks

Picture the scenario: you have finished developing a web app using your favourite react framework.

Its a pretty simple web app, all it does is display a greeting message.

This greeting message may change depending on which environment you deploy it in, for example, in your development environment you want it to say:

"Hello from development!"

...and when its deployed in your staging environment you want it to say:

"Hello from staging!".

So naturally you put the greeting message in an environment variable so that you do not need to build a different version of the code for each environment.

// .env
...
NEXT_PUBLIC_GREETING="Hello from development!"
...

Thats too easy tho

You, being the good soydev you are, decide to deploy it using docker.

So you write a simple dockerfile to build an image but quickly realise something.

1
We have to set this at build time!!!
DOCKER
...
RUN npm i
ARG1NEXT_PUBLIC_GREETING="Hello from development!"
RUN next build
...
1
We have to set this at build time!!!

Since we have to set this environment variable when we are building the next app, our image will forever have this value hardcoded into its very essence.

This means that even if we try to set the value of the variable at runtime like so:

docker run -e NEXT_PUBLIC_GREETING="rekt m8" my-image/latest

It won't work because NEXT_PUBLIC_GREETING has already been inlined to be Hello from development! in the compiled code.

So what can we do?

My solution

Like many a brave soul that came before me, my approach to tackling this problem was to set the value of the environment variable to a placeholder that can be switched out at runtime.

To achieve this I created a shell script that can be broken into two parts. The first part is to create the sed commands needed to replace each placeholder with the correct value:

1
The placeholder is just the name of the environment variable prefixed by APP_
BASH
# Get all the environment variables currently loaded
printenv | \
# Filter for ones that start with NEXT_PUBLIC
grep '^NEXT_PUBLIC' | \
# Replace the = sign with a space
sed -r "s/=/ /g" | \
# Feed as arguments to sed so they can be used in a find and replace command
xargs -n 2 bash -c 1'echo "sed -i \"s#APP_$0#$1#g\""'
1
The placeholder is just the name of the environment variable prefixed by APP_

Lets test it!

~>> export NEXT_PUBLIC_GREETING=HELLO!
~>> export NEXT_PUBLIC_THEME=Dark
~>> printenv | \
grep '^NEXT_PUBLIC' | \
sed -r "s/=/ /g" | \
xargs -n 2 bash -c 'echo "sed -i \"s#APP_$0#$1#g\""'
sed -i "s#APP_NEXT_PUBLIC_GREETING#HELLO!#g"
sed -i "s#APP_NEXT_PUBLIC_THEME#Dark#g"

As we can see it spits out a sed search and replace command for each environment variable we have.

Now for the second part, to execute these commands against the compiled code files.

BASH
#!/usr/bin/env bash
# The first part wrapped in a function
makeSedCommands() {
printenv | \
grep '^NEXT_PUBLIC' | \
sed -r "s/=/ /g" | \
xargs -n 2 bash -c 'echo "sed -i \"s#APP_$0#$1#g\""'
}
# Set the delimiter to newlines (needed for looping over the function output)
IFS=$'\n'
# For each sed command
for c in $(makeSedCommands); do
# For each file in the .next directory
for f in $(find .next -type f); do
# Execute the command against the file
COMMAND="$c $f"
eval $COMMAND
done
done
echo "Starting Nextjs"
# Run any arguments passed to this script
exec "$@"

Ok cool, we have our script ready! Now how do we use it with docker?

1
We set the variable with a placeholder
DOCKER
...
ARG1NEXT_PUBLIC_GREETING=APP_NEXT_PUBLIC_GREETING
RUN next build
# Copy our script somewhere into the image
COPY entrypoint.sh .
# Make it executable
RUN ["chmod", "+x", "/app/entrypoint.sh"]
EXPOSE 3000
ENTRYPOINT ["/app/entrypoint.sh"]
CMD npm run start
1
We set the variable with a placeholder

Caveats

Lets remind ourselves that we are literally doing a search and replace on some compiled code so don't expect everything to be plain sailing.

One thing I have discovered is that because NextJS splits your code into chunks during a production build, and we are simply doing a find and replace on those chunks, obviously the names of those chunks will not change. This means the browser can't tell that you have changed the chunks so it will continue to serve the cached version unless you explicitely clear the cache in your browser. I haven't had the time to see if there is a workaround for this but presumably just renaming the chunk files without breaking everything somehow would fix this problem.

And voila! Bit of a hacky solution but what can you do? If you know of a less gross horrible hacky way of doing this then please tell me!

Subscribe

No Webmentions for this post yet!