ReactJS + Docker – Environment Variables

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
  • 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

Leave A Reply

Your email address will not be published. Required fields are marked *