How to set NEXT_PUBLIC_* environment variables in Docker
Spoiler alert! Its a toxic relationship!
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.
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:
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.
#!/usr/bin/env bash# The first part wrapped in a functionmakeSedCommands() {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 commandfor c in $(makeSedCommands); do# For each file in the .next directoryfor f in $(find .next -type f); do# Execute the command against the fileCOMMAND="$c $f"eval $COMMANDdonedoneecho "Starting Nextjs"# Run any arguments passed to this scriptexec "$@"
Ok cool, we have our script ready! Now how do we use it with docker?
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