How to Deploy a React app to OpenShift

OpenShift JavaScript

OpenShift isn’t just for hosting backend applications, you can also use it to host your frontend apps too. React is probably the hottest 🔥 frontend framework right now. So how do you deploy your React app onto OpenShift?

What we’re going to build

Let’s see what we’re going to create, before we build it. Here’s what we’re aiming for:

  • Build a Docker image for the React app inside OpenShift, using a Docker build. For this, we’ll create a BuildConfig with a Dockerfile.

  • Create a trigger to redeploy our app whenever a new image is built. For this, we’ll also need an ImageStream.

  • Deploy our application. For this, we’ll create a DeploymentConfig which will manage some Pods for us.

  • Make the application available to the outside world with a Service and a Route. (You could also use an Ingress on OpenShift if you prefer.)

  • Finally we’ll provide some runtime config to the application with a ConfigMap.

Set up the app

How to create a Dockerfile for the React app

I’m going to use a Docker build here. Why? Because it gives a bit more flexibility than OpenShift’s Source-to-Image build – it means that you could actually build this image anywhere with Docker, not just inside OpenShift.

In my Docker build I’m going to use images available from public registries. But if you’ve got an OpenShift subscription, you can use the supported images from the Red Hat registry.

The Docker build will run in two stages:

Build the app using the node image → Inject the compiled code into an nginx container

So here’s my Dockerfile, which I added into the root of my Git repository:

# Stage 1: Use yarn to build the app
FROM node:14 as builder
WORKDIR /usr/src/app
COPY package.json yarn.lock ./
RUN yarn
COPY . ./
RUN yarn build

# Stage 2: Copy the JS React SPA into the Nginx HTML directory
FROM bitnami/nginx:latest
COPY --from=builder /usr/src/app/build /app
EXPOSE 8080
CMD ["nginx", "-g", "daemon off;"]

Note it’s a multi-stage Docker build. I’m doing this so I can keep my final image small. The final image only needs to contain my web server and minified code, instead of Node, npm and the rest of the kitchen sink!

The first stage builds my production distribution (minified JavaScript and all that stuff). The second stage copies the production distribution into an nginx container.

This line might seem a bit strange:

COPY --from=builder /usr/src/app/build /app

But it basically tells the Docker build to copy the output from Stage 1 into the /app dir in the Stage 2 container.

Finally we expose port 8080 and set the startup command for Nginx. I’m using the free Nginx image from Bitnami.

Now that we’ve defined how we’ll build a container image for the application, the next thing to tackle is how we’ll provide runtime config to the app, like API URLs or environment details.

How to provide runtime configuration to the React app

This is an interesting problem. How do you provide configuration to a static web app?

(If you don’t need to configure your React app, you can skip this section!)

For back-end apps, we can usually use environment variables. But with front-end, there is no “runtime” (like Node.js) which can receive env vars.

A React app is just static code (HTML and JavaScript) that will be hosted on a basic web server. So what do you do, if you need to give some runtime variables, like the URL to your backend API?

Dynamic variables can be declared in a file, which gets served to the browser separately

Instead of giving environment variables to our “app”, we give the environment variables to the client (the web browser).

We serve an additional JS file to the browser, which contains these environment-specific variables.

When the browser loads the HTML, it will fetch the minified JavaScript for the React app AND an extra JavaScript source file, which contains the environment config.

I learned this trick from this awesome blog post on React environment variables by Javaad Patel. Thanks, Javaad! :)

And you’re probably asking how we can add extra files to an app in Kubernetes/OpenShift:

To add any kind of dynamic or environment-specific file into a container, we can use a ConfigMap.

We don’t need to build any configuration files into our image at all. We can inject the file at runtime using a ConfigMap.

This is good, because it means we don’t to have to build a new image for each environment’s config (building a different image for each environment is an anti-pattern).

So let’s see how to do it with React.

Setting up environment variables in the app

First we define environment variables in a file, like static/environment.js. These will be our default variable values, which we’ll override later:

// set some defaults here
window.FOO = "Some value goes here";
window.BAR = "Another value goes there";

Add another script, ./src/config.js. This tells JavaScript to create a configuration const which will hold all of our environment variables:

// Fetch config variables defined in window.*
const envSettings = window;

export const configuration = {
  foo: envSettings.FOO,
  bar: envSettings.BAR
}

Finally you can refer to the variables inside your App.js, or anywhere else you like. Just import the configuration then use braces { } to refer to the values:

import { configuration } from './config.js';

//...
function App() {
    return (
      <p>
        <strong>Welcome to my app! { configuration.foo }</strong>
      </p>
    );
}

Finally, we need to make sure that the user’s web browser fetches the configuration variables.

So, add a script tag to include your environment variable overrides. Now my HTML looks like this:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <script src="%PUBLIC_URL%/environment.js"></script>
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
...

So we’ve figured out how to:

  • Build the app, with a Dockerfile

  • Provide runtime config for the app

Let’s put this all together, into a Template.

Use this OpenShift Template to deploy it all

So now here’s an OpenShift Template to bring it all together:

  • A BuildConfig which uses a Dockerfile.

  • An ImageStream to track our application’s Docker image.

  • A DeploymentConfig to deploy the application, inject configuration into the container, and redeploy it when the image changes.

  • A Service and a Route to make the app available.

  • A ConfigMap containing some environment overrides.

Here it is:

apiVersion: template.openshift.io/v1
kind: Template
objects:
- apiVersion: v1
  kind: ConfigMap
  metadata:
    name: ${NAME}
  data:
    environment.js: |
      // set some defaults here
      window.FAVOURITE_SONG = "The Locomotion"
      window.FAVOURITE_ERA = "Mid-90s Nick Cave Kylie"
- apiVersion: build.openshift.io/v1
  kind: BuildConfig
  metadata:
    labels:
      app: ${NAME}
      app.kubernetes.io/component: react-hello-world
      app.kubernetes.io/instance: ${NAME}
    name: ${NAME}
  spec:
    output:
      to:
        kind: ImageStreamTag
        name: ${NAME}:latest
    source:
      contextDir: ${CONTEXT_DIR}
      git:
        uri: ${SOURCE_REPOSITORY_URL}
        ref: ${SOURCE_REPOSITORY_REF}
      type: Git
    strategy:
      dockerStrategy:
        from:
          kind: ImageStreamTag
          name: nginx:latest
      type: Docker
    triggers:
    - type: ConfigChange
    - type: ImageChange
- apiVersion: apps.openshift.io/v1
  kind: DeploymentConfig
  metadata:
    labels:
      app: ${NAME}
      app.kubernetes.io/component: react-hello-world
      app.kubernetes.io/instance: ${NAME}
    name: ${NAME}
  spec:
    replicas: 1
    selector:
      deploymentconfig: ${NAME}
    template:
      metadata:
        labels:
          deploymentconfig: ${NAME}
      spec:
        containers:
        - image: ${NAME}
          imagePullPolicy: Always
          name: react-hello-world
          ports:
          - containerPort: 8080
            protocol: TCP
          volumeMounts:
            - name: app-config-volume
              mountPath: /app/environment.js
              subPath: environment.js
        volumes:
          - name: app-config-volume
            configMap:
              name: ${NAME}
    triggers:
    - type: ConfigChange
    - type: ImageChange
      imageChangeParams:
        automatic: true
        containerNames:
        - react-hello-world
        from:
          kind: ImageStreamTag
          name: ${NAME}:latest
- apiVersion: v1
  kind: Service
  metadata:
    labels:
      app: ${NAME}
      app.kubernetes.io/component: react-hello-world
      app.kubernetes.io/instance: ${NAME}
    name: ${NAME}
  spec:
    ports:
    - name: 8080-tcp
      port: 8080
      protocol: TCP
      targetPort: 8080
    selector:
      deploymentconfig: ${NAME}
    type: ClusterIP
- apiVersion: image.openshift.io/v1
  kind: ImageStream
  metadata:
    labels:
      app: ${NAME}
      app.kubernetes.io/component: react-hello-world
      app.kubernetes.io/instance: ${NAME}
    name: nginx
  spec:
    lookupPolicy:
      local: false
    tags:
    - name: latest
      from:
        kind: DockerImage
        name: bitnami/nginx:latest
      annotations:
        openshift.io/imported-from: bitnami/nginx:latest
      referencePolicy:
        type: Source
- apiVersion: image.openshift.io/v1
  kind: ImageStream
  metadata:
    labels:
      app: ${NAME}
      app.kubernetes.io/component: react-hello-world
      app.kubernetes.io/instance: ${NAME}
    name: ${NAME}
  spec:
    lookupPolicy:
      local: false
- apiVersion: route.openshift.io/v1
  kind: Route
  metadata:
    labels:
      app: ${NAME}
      app.kubernetes.io/component: react-hello-world
      app.kubernetes.io/instance: ${NAME}
    name: ${NAME}
  spec:
    port:
      targetPort: 8080-tcp
    to:
      kind: Service
      name: ${NAME}
      weight: 100
    wildcardPolicy: None
    tls:
      termination: Edge
parameters:
- description: The name assigned to all of the frontend objects defined in this template.
  displayName: Name
  name: NAME
  required: true
  value: react-hello-world
- description: The URL of the repository with your application source code.
  displayName: Git Repository URL
  name: SOURCE_REPOSITORY_URL
  required: true
  value: https://github.com/monodot/container-up.git
- description: Set this to a branch name, tag or other ref of your repository if you
    are not using the default branch.
  displayName: Git Reference
  name: SOURCE_REPOSITORY_REF
  value: master
- description: Set this to the relative path to your project if it is not in the root
    of your repository.
  displayName: Context Directory
  name: CONTEXT_DIR
  value: react-hello-world

Put this into a file openshift-template.yml, make any changes to suit your environment, and then apply it using:

oc process -f openshift-template.yml | oc apply -f -

Don’t forget that if you want to see and run a completed example, check out the GitHub repo!

Next: Deploy a Node.js application to OpenShift

Now you’ve got your frontend deployed, check out how to deploy a Node.js application (like Express.js) onto OpenShift..

Thanks for reading!