System environment variables in Next.js with Docker
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_modulesRUN yarn build && yarn install --production --ignore-scripts --prefer-offline# Production image, copy all the files and run next
FROM node:alpine AS runner
WORKDIR /appENV 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 1CMD ["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.