System environment variables in Next.js with Docker

Raphael Pralat
4 min readNov 29, 2021

--

Next.js is an excellent open-source development framework, one of its features is to use environment files for configuration. Unfortunately it seems difficult to create an app, Dockerize it and then use custom system environment variables when the image is run. Indeed at the build phase, config values are written in the .next/ folder, and it is difficult to change them without rebuilding the application.

This article presents a way to replace build environments variables with custom system environment variables.

This solution is inspired by this GitHub example.

Next.js application

Create application

Create a Next.js application:

npx create-next-app@latest

Set next-with-system-env for the project name:

Need to install the following packages:                                                                                                                                                                                                                                                                                                                              
create-next-app@latest
Ok to proceed? (y)
✔ What is your project named? … next-with-system-env

Go to the application folder:

cd next-with-system-env

Runtime configuration

To allow config to be accessible to both client and server-side, edit next.config.js, in module.exports add:

publicRuntimeConfig: {
name: process.env.NAME,
description: process.env.DESCRIPTION,
}

To load environment variables, create an .env file with:

NAME=Next with system env
DESCRIPTION=Next with system env description

To display config values, edit /page/index.js and add:

import getConfig from 'next/config'
const { publicRuntimeConfig: config } = getConfig()
console.log('config:', JSON.stringify(config))

Run the application:

npm run dev

The browser console should display:

config: {"app":{"name":"Next with system env","description":"Next with system env description"}}

At this point, if you Dockerize the application with the recommended Dockerfile example, the config will be built in the image without any way to change values with system environment variables.

Replace build config with system environment

Generic config for production

Create another config file for production .env.production and add:

NAME=APP_NEXT_NAME
DESCRIPTION=APP_NEXT_DESCRIPTION

The APP_NEXT_… will be matched and replaced in the next steps

Shell script to match and replace config

Create a entrypoint.sh file and add:

#!/bin/bash# no verbose
set +x
# config
envFilename='.env.production'
nextFolder='./.next/'
function apply_path {
# read all config file
while read 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
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
}apply_path
echo "Starting Nextjs"
exec "$@"

This script find all the APP_NEXT_… occurent in .next/ folder and replace them with system environment variables.

Dockerize

Create a Dockerfile and add:

# Install dependencies only when needed
FROM node:alpine AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json ./
RUN yarn install --frozen-lockfile
# Rebuild the source code only when needed
FROM node:alpine AS builder
WORKDIR /app
COPY . .
COPY --from=deps /app/node_modules ./node_modules
RUN yarn build && yarn install --production --ignore-scripts --prefer-offline# Production image, copy all the files and run next
FROM node:alpine AS runner
WORKDIR /app
ENV NODE_ENV productionRUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
# You only need to copy next.config.js if you are NOT using the default configuration
#COPY --from=builder /app/next.config.js ./
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
COPY entrypoint.sh .
COPY .env.production .
# Execute script
RUN apk add --no-cache --upgrade bash
RUN ["chmod", "+x", "./entrypoint.sh"]
ENTRYPOINT ["./entrypoint.sh"]
USER nextjsEXPOSE 3000ENV PORT 3000# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry.
# ENV NEXT_TELEMETRY_DISABLED 1
CMD ["node_modules/.bin/next", "start"]

The .env.production will be copied to allow the script to identify all the configuration values to replace thanks to theAPP_NEXT_… prefix.

And as explained previously, the entrypoint.sh will replace this built config values in the .next/ folder with values from the system. It needs bash to be executed.

Test

To test is, first build an image:

docker build -t next-with-system-env .

Then start the image with --env-file .env option to load system environment values:

docker run --env-file .env next-with-system-env

Finally go to http://localhost:3000

The browser console should display values from .env.

Example

A full example can be found here: https://github.com/factorim/next-with-system-env

Conclusion

Maybe there is a more straightforward solution the add system environment configuration in a Dockerized Nest.js application, I would be glad to know it. In the meantime, here is a way to do it.

--

--