React Environment Variables
Packaging your ReactJS application as a Docker container provides all of the usual Docker benefits: portability, ease of maintenance, cloud support, standardization, and on and on and on.
One pitfall of using client-side rendering with React is with the use of environment variables. Lets look at a quick example:
// Use environment vars for DOMAIN and API_KEY
const DOMAIN = process.env.REACT_APP_DOMAIN;
const API_KEY = process.env.REACT_APP_API_KEY;
fetch(`${DOMAIN}/api?key=${API_KEY}).then(r => console.log(r.json()))
In this example, environment variables are used to set the domain and key for some imaginary service. Most software engineers experienced writing server-side software in languages like Java, NodeJS, or Python might think that if we build this application and deploy it on a server with environment variables REACT_APP_DOMAIN=http://mysite.com
and REACT_APP_API_KEY=ABC12
, that the variables would be set to these values.
However, since client-side rendered (CSR) ReactJS applications, this is not the case! Why? CSR applications static javascript files. These files are compiled at build time. In order for our environment variables to be set, we need to specify them at build time.
One way we can achieve this is to set the environment variables in the build environment prior to running our build commands. For example:
export REACT_APP_DOMAIN='http://mysite.com'
export REACT_APP_API_KEY='ABC123'
npm run build
Note: I typically use create-react-app for my ReactJS applications. Your build commands may differ.
My preferred approach is to set the environment variables as a part of the build command.
REACT_APP_DOMAIN=http://mysite.com REACT_APP_API_KEY='ABC123' npm run build
CSR ReactJS Apps with Docker
By now, you might be wondering what this might mean about how we build our application to run with Docker. I build all of my CSR ReactJS applications using a multi-stage build process with two stages:
- The build stage
- Requires
node
- Runs the build commands
- Requires
- The runtime stage
- Can use a minimalistic base image
- Requires a web server or proxy like nginx
The build stage is where we need to specify our environment variables in order to build our application with the proper configuration. Lets start with this minimalistic Dockerfile
:
FROM node:12.14.0 as build
WORKDIR /app
COPY ./package.json .
COPY ./package-lock.json .
RUN npm install
COPY ./public ./public/
COPY ./src ./src/
RUN npm run build
This file will build your ReactJS CSR application and stores the static source files in the /app
directory. It provides no mechanism for running it — that responsibility will fall to the runtime stage.
The problem we are trying to solve in this post is how to get environment variables set for our application. The naive approach to this is to use Docker’s built-in ENV
directive. For example:
FROM node:12.14.0 as build
ARG DOMAIN
ARG API_KEY
...
# Warning: doesn't work!
ENV REACT_APP_DOMAIN=${DOMAIN}
ENV REACT_APP_API_KEY=${API_KEY}
RUN npm run build
To run the build, you would run
docker build \
--build-arg DOMAIN=http://mysite.com \
--build-arg API_KEY=ABC123 \
.
Although this approach looks like it would work, it unfortunately does not. The catch is that Docker’s ENV
directive sets environment variables for a running container. Since we never actually run this container but just use it for build purposes, this approach does not work. To get this to work as desired, we must use my preferred approach of specifying the variables as a part of the build command.
FROM node:12.14.0 as build
ARG DOMAIN
ARG API_KEY
...
# Hooray!
RUN REACT_APP_DOMAIN=${DOMAIN} \
REACT_APP_API_KEY=${API_KEY} \
npm run build
Yay! Our CSR ReactJS application can now be built with environment variables! We now just need a simple runtime container with the static files copied into the folder which will be used to host our application. I prefer the nginx-alpine image, but you can choose whatever works best for you.
Here is the complete Dockerfile
:
FROM node:12.14.0 as build
ARG DOMAIN
ARG API_KEY
WORKDIR /app
COPY ./package.json .
COPY ./package-lock.json .
RUN npm install
COPY ./public ./public/
COPY ./src ./src/
RUN REACT_APP_DOMAIN=${DOMAIN} \
REACT_APP_API_KEY=${API_KEY} \
npm run build
FROM nginx:1.17-alpine
COPY --from=build /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
docker-compose
If you are using docker-compose
to build your images or run your containers, we can specify the build arguments in our file like so:
version: "3"
services:
react:
build:
context: .
args:
- DOMAIN=${DOMAIN}
- API_KEY=${API_KEY}
container_name: react
ports:
- 80
I like to use a .env
file in my projects to avoid having to set these variables every time I run a compose command. This file is a simple key=value
file. For example,
DOMAIN=http://mysite.com
API_KEY=ABC123
The application can then be built and run like so:
docker-compose build && docker-compose up
And thats it! We are building our CSR ReactJS application with environment variables by specifying them in our .env
file. Where’s that Easy Button when you need it?
Complete Code
Dockerfile
FROM node:12.14.0 as build
ARG DOMAIN
ARG API_KEY
WORKDIR /app
COPY ./package.json .
COPY ./package-lock.json .
RUN npm install
COPY ./public ./public/
COPY ./src ./src/
RUN REACT_APP_DOMAIN=${DOMAIN} \
REACT_APP_API_KEY=${API_KEY} \
npm run build
FROM nginx:1.17-alpine
COPY --from=build /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
docker-compose.yml
version: "3"
services:
react:
build:
context: .
args:
- DOMAIN=${DOMAIN}
- API_KEY=${API_KEY}
container_name: react
ports:
- 80
.env
DOMAIN=http://mysite.com
API_KEY=ABC123