How to actually configure front end environment variables
- Published on
7 min read
- Authors
- Name
- Isaac Lamb
- @devisaac
Hello friends.
Deploying frontend applications these days is relatively straight-forward. All the major frameworks provide some variation on npm run build
that packages your application ready to be deployed to your web host of choice.
Most frameworks also give you a way to use environment variables with something like process.env.YOUR_VARIABLE
and using a .env
file to configure these variables locally. This makes for a great local development experience, but what about when you need to deploy your application to a dev/test/production environment? At this point, the recommendation is generally to use a .env.production
file which will substitutes to process.env.YOUR_VARIABLE
values at build time.
This is where things get messy.
There are a couple of issues here, but the main one is that in order to deploy your application to multiple environments, you will need to build it multiple times, using a different .env
file for each.
At this point there are 3 options:
- Build application for each environment
- Hand roll something else - e.g.
config.js
or get config via an API - Use tokens in
.env.production
that can be substituted with environment specific variables at runtime
Option 1 is not great as it means we have to cop whatever our build time is each time we want to deploy the application to a new environment.
Option 2 is fine and it is something I've done in the past. The main problem is that it is another 'thing' that your application has to do on each new site visitor before it can start being usable.
I like option 3 as it lets us keep the nicer developer experience of .env files and we can quickly deploy to new environments or change config settings without having to rebuild the app.
The process works like this:
- Create an
.env
file for local development - e.g.
API_URL=https://localhost:7443
- Create
.env.production
file with tokens for each variable - e.g.
API_URL=#{API_URL}
- Build your application with
npm run build
(or the appropriate command for the framework you're using) - Push the produced package somewhere appropriate (e.g. Azure DevOps Artifact, Docker Registry, etc.)
Then whenever you want to deploy the app:
- Pull down the package
- Substitute the tokens for environment specific variables
- Deploy the package
The way you substitute the tokens depends on the tool you're using:
For AzureDevops pipelines, there is Replace Tokens
For GitHub actions, there is Replace tokens
For Docker, you'll need to set your environment variables then execute an entrypoint.sh
script that does it... like this:
#!/bin/sh
# Directory to process
target_directory="/app"
# Function to replace tokens in a file
replace_tokens() {
local file="$1"
local content_before=$(cat "$file")
local content=$content_before
local regex='#{[A-Za-z_][A-Za-z0-9_]*}'
while echo "$content" | grep -q "$regex"; do
local token="$(echo "$content" | grep -o "$regex" | head -n 1)"
local var_name="$(echo "$token" | cut -c 3- | rev | cut -c 2- | rev)"
local var_value
eval "var_value=\$$var_name"
if [ -n "$var_value" ]; then
# Use a custom delimiter and escape it
local delimiter="|"
local token_escaped="$(echo "$token" | sed 's/[\&/]/\\&/g')"
local var_value_escaped="$(echo "$var_value" | sed 's/[\&/]/\\&/g')"
echo "Replacing token '$token' with '$var_value' in $file"
content="$(echo "$content" | sed "s${delimiter}${token_escaped}${delimiter}${var_value_escaped}${delimiter}")"
else
echo "Environment variable '$var_name' not found for token '$token' in $file (Removing)"
content="$(echo "$content" | sed "s/$token//")"
fi
done
if [ "$content" != "$content_before" ]; then
echo "Saving $file"
echo -n "$content" > "$file"
echo ''
fi
}
# Use find with null-terminated output and a while read loop
find "$target_directory" -type f -print0 | while IFS= read -r -d '' file; do
replace_tokens "$file"
done
echo 'Token variable substitution complete'
exec "$@"
Your Dockerfile then needs to call the entrypoint.sh
file on startup: e.g. ENTRYPOINT ["/app/entrypoint.sh"]
Here's a complete Dockerfile example for Nextjs:
FROM node:18-alpine AS base
# Install dependencies only when needed
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install dependencies based on the preferred package manager
COPY package.json package-lock.json* ./
RUN npm ci
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY /app/node_modules ./node_modules
COPY . .
RUN npm run build
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY /app/public ./public
# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY /app/.next/standalone ./
COPY /app/.next/static ./.next/static
COPY entrypoint.sh ./
RUN chmod +x entrypoint.sh
USER nextjs
EXPOSE 3000
ENV PORT 3000
# set hostname to localhost
ENV HOSTNAME "0.0.0.0"
ENTRYPOINT ["/app/entrypoint.sh"]
CMD ["node", "server.js"]
Happy deployments!