During my career happened to face this issue: how to build once a react-based application and run it through different environments (staging, qa, dev, production) with a single docker image?

Usually to change a variable (for example a URL) you need to rebuild the application and redeploy on the target environment; as you may know, this isn’t very flexible and it may require other actions such as CDN cache clear, coordinate between teams etc.

This is a common issue with different ways to solve it. The one that I use and that I want to show you today respects the Twelve-Factor App methodoloy.

Twelve factor app: config principle

twelve_factor_app

The Config principle is about the configuration management: it states that an application uses its configuration by reading it during runtime as environment variables or other configuration files. So for example it can be a Kubernetes configmap/secret, property file, etc. The benefits of injecting configuration files during runtime (hence having it separated from the application itself) is that you can change settings without rebuild the app. So you end up having a configuration file(s) for the dev environment, other files for the test environment and so on.

Now that we know this principle, let’s get back to how to achieve it.

React application

The main idea is to strip off any .env files from the source code; instead let’s do the following:

Create the configmap that we will mount as runtime configuration file and populate it with the variables we want to use:

apiVersion: v1
kind: ConfigMap
metadata:
  name: my-applicaton-configmap
  namespace: my-namespace
data:
  config.js: |
    window.REACT_APP_MAIN_SITE_HOMEPAGE="https://my-website.my-domain.it";
    window.REACT_APP_API_URL="https://my-website.my-domain.it/api";
    window.REACT_APP_PUBLIC_URL="https://my-website.my-domain.it/login";
    window.REACT_APP_REDIRECT_URL="http://my-redirect-url.com/";
    window.REACT_APP_REDIRECT_REG="https://my-domain.other-domain.com/registerPopup.html";
    window.REACT_APP_REDIRECT_FORGOT="https://my-domain.other-domain.com/forgotPassword.html";    

As you can see the configuration file is a simple config.js file. Mount this file inside your application server (nginx/tomcat for example).

Inside the public folder, add this script section

<!DOCTYPE html>
<html lang="it">

<head>
  <script src="%PUBLIC_URL%/config.js"></script>
  [...]

Then, since we don’t want to pollute the code with different reference to the window object, let’s create a src/config.ts file where we do the binding and export the variables

const REACT_APP_DEPLOY_ENV: string = window.REACT_APP_DEPLOY_ENV || '';
const REACT_APP_MAIN_SITE_HOMEPAGE: string = window.REACT_APP_MAIN_SITE_HOMEPAGE || '';
const REACT_APP_API_URL: string = window.REACT_APP_API_URL || '';
const PUBLIC_URL: string = window.PUBLIC_URL || '';
const REACT_APP_REDIRECT_URL: string = window.REACT_APP_REDIRECT_URL || '';
const REACT_APP_REDIRECT_REG: string = window.REACT_APP_REDIRECT_REG || '';
const REACT_APP_REDIRECT_FORGOT: string = window.REACT_APP_REDIRECT_FORGOT || '';

export {
  REACT_APP_DEPLOY_ENV,
  REACT_APP_MAIN_SITE_HOMEPAGE,
  REACT_APP_API_URL,
  PUBLIC_URL,
  REACT_APP_REDIRECT_URL,
  REACT_APP_REDIRECT_REG,
  REACT_APP_REDIRECT_FORGOT,
};

So in our code we just import this file and reference the exported variables, for example:


import React, { useCallback } from 'react';
import styled from 'styled-components';
import colors from 'style-utils/colors';

import { Translate } from 'react-localize-redux';
import { withPadding, withMargin } from 'style-utils/dimensionsMixins';
import { IButtonProperties } from '../../style-utils/ElementsForm/button.styled';
import { useDispatch } from 'react-redux';
import { showLoader } from '../../store/loginSubmit';
import { REACT_APP_REDIRECT_REG } from 'config'; /* IMPORT */

[OMITTED_CODE]

export const ButtonRegisterToMemoize: React.FC = () => {
  const dispatch = useDispatch();
  const memoizedCallback = useCallback(() => {
    dispatch(showLoader());
  }, [dispatch]);

  return (
    <A href={REACT_APP_REDIRECT_REG}> /* USE */
      <Button
        onClick={memoizedCallback}
        color="#222"
        border={`2px solid ${colors.registrateButtonBorder}`}
        background="#fff"
      >
        <Translate id="lbl.login.registerButton" />
        {/* Regìstrate */}
      </Button>
    </A>
  );
};

export const ButtonRegister = React.memo(ButtonRegisterToMemoize);

Bonus #1: Dockerfile (w/ Apache)

A Dockerfile that we can use for a react based application is for example

ARG BUILD_IMAGE=node:14.15-stretch-slim 
ARG RUNTIME_IMAGE=apache:2.4-alpine

FROM ${BUILD_IMAGE} as builder
WORKDIR /output
ENV PATH /output/node_modules/.bin:$PATH
COPY package.json ./
RUN npm install --silent #--cache-folder /image-cache
COPY . ./
FROM builder as final
RUN npm run build

FROM ${RUNTIME_IMAGE} as production 
COPY --from=final /output/build /usr/local/apache2/htdocs/my_application
EXPOSE 3000
ENTRYPOINT ["apachectl","-D","FOREGROUND"]

Bonus #2: K8S Deployment (w/ Apache)

Then deploy the application on a Kubernetes cluster using this deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my_application
  namespace: my_namespace
spec:
  replicas: 1
  revisionHistoryLimit: 2
  selector:
    matchLabels:
      app: my_application
      app.kubernetes.io/component: fe
  strategy:
    type: Recreate
  template:
    metadata:
      annotations:
        ci-last-build: 2021-09-13_08:34:12
        ci-last-commit: d32d4f52af8bbf15ce977ce0a553fba1088fc9a2
        hash: "5"
      labels:
        app: my_application
        app.kubernetes.io/component: fe
    spec:
      containers:
      - envFrom:
        - configMapRef:
            name: spring-config
        image: my_application:0.0.2
        imagePullPolicy: Always
        name: my_application
        ports:
        - containerPort: 8080
          name: http
        resources:
          limits:
            cpu: "0.6"
            memory: 1.0Gi
          requests:
            cpu: "0.2"
            memory: 0.5Gi
        volumeMounts:
        - mountPath: /usr/local/apache2/htdocs/my_application/config.js
          name: config-js
          subPath: config.js
        - mountPath: /usr/local/apache2/conf/httpd.conf
          name: apache-config
          subPath: httpd.conf
      volumes:
      - configMap:
          name: my_application-config-js
        name: config-js
      - configMap:
          name: apache-config
        name: apache-config

Here you can see the usage of the config.js file: we mount it inside the react application folder inside htdocs.

        - mountPath: /usr/local/apache2/htdocs/my_application/config.js
          name: config-js
          subPath: config.js

Conclusion

In this way we are able to run our React application as a Docker container that is build only once and runs everywhere since it’s configurable during runtime time.

Moreover: we can still work locally by specifing a config.js inside the public folder, just create it and remember to not commit this file (add it to the .dockerignore file).