How to Deploy a React app to OpenShift
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.
Grab an example React app
If you’ve already got a React app you want to deploy, then skip ahead!
If you haven’t got an app, and you want to use an example, then feel free to fork mine here:
I’m going to use yarn in this tutorial because that’s the default that you get when you create a new app with create-react-app.
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!