How to Deploy a React app to OpenShift
In this article, we’re going to look at how to build and deploy your React application on OpenShift, using your source code, and a Dockerfile.
What we’re going to build
I think it’s helpful to see what we’re going to create before we build it. So let’s look at what we’re aiming for:
-
Building a Docker image in the OpenShift cluster, with a Dockerfile. For this, we’ll create a BuildConfig with a Dockerfile.
-
Creating a trigger that will redeploy our app when a new image is built. For this, we’ll create an ImageStream.
-
Deploying our application. For this, we’ll create a DeploymentConfig which will manage some Pods for us.
-
We’ll also make the application available to the outside world with a Service and a Route.
-
Finally we’ll provide some runtime config to the application with a ConfigMap.
An example app to use
If you’ve already got a React app to use, then skip ahead.
If you haven’t got an app, then you can use mine here:
https://github.com/monodot/container-up/tree/master/react-hello-world
I’m going to use yarn in this tutorial because that’s the default when you create a new app with create-react-app.
Creating a Dockerfile
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.
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 to keep my final image small. The final image only needs to contain my web server and minified code, instead of Node, npmm 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.
The COPY --from=builder /usr/src/app/build /app
is a strange line which describes copying 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 here.
Configuring the app with a ConfigMap
This is an interesting conundrum. How do you provide configuration to a static web app? There is no “runtime” to receive your env vars.
For back-end apps, we can usually use environment variables.
But this is static code (HTML and JavaScript) that will be hosted on a dumb web server. So how do we make the configuration dynamic?
It took me a while to get my head around this:
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 JS for the React app plus an extra JavaScript source file which contains environment config.
I learned this trick from this awesome blog post on React environment variables by Javaad Patel. Thanks, Javaad! :)
And here’s the light bulb moment:
To add any kind of dynamic or environment-specific file into a container, we can use a ConfigMap.
This means we don’t need to build any configuration files into our image at all.
This is ideal, because we don’t want to have to build a new image for each environment.
Giving runtime config to your React app
Define environment variables in a file static/environment.js
. These will be our default variables, 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
:
// Fetch config variables defined in window.*
const envSettings = window;
export const configuration = {
foo: envSettings.FOO,
bar: envSettings.BAR
}
Finally you can refer to them 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 (
Value of foo = { configuration.foo }
);
}
Finally, we need to give the configuration variables to the client’s web browser. So add a script
tag to include your environment variable overrides. So 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" />
...
An OpenShift template to deploy it all
And now here’s an OpenShift template to bring it all together:
-
A BuildConfig which refers to a Dockerfile
-
An ImageStream to track our application’s Docker image
-
A DeploymentConfig to deploy the application and inject configuration into the container, plus a Service and a Route.
-
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
Now you can apply this using:
oc process -f openshift-template | oc apply -f -
To see and run a completed example, check out the GitHub repo.
Thanks for reading!