A core tenant of application promotion is that you are using the same version of your application between environments. If you have a version of your code running on
staging, or in a Preview environment, and you want to promote that code to
production, you simply deploy same image to production, with the production set of environment variables. With a populate-on-build system like Reacts', you can not do this. The staging environment variables are baked in to the image, so it can not be deployed to production.
I use dynamic, ephemeral environments for a lot of my development and review work. These are dynamic environments that spin up with a unique environment, so being able to provide run time configuration was extremely important.
This post focuses on serving a React App from a container image. The same issues apply to other deployment mechanisms.
I found 3 solutions:
- Refactor my React application to fetch the current environment’s configuration on start up.
- Serve the React app non-statically, and template the file on each request.
- Template the environment into the application on container start up.
Option 3 is the closest to the default behaviour, and I was lucky enough to find this post, where the author tool a similiar approach, and had open-sourced a tool for injecting the environment variables. All I needed to do was run this tool before serving my files, and update my
process.env calls to
window._jsenv with a fallback (There is a more detailed explanation of this here).
I serve my applications via a populated Nginx image deployed in Kubernetes, and was already using a multi-stage build for this purpose:
# Build the React app with Yarn FROM node:13.12.0-alpine as build WORKDIR /app ENV PATH /app/node_modules/.bin:$PATH COPY package.json ./ RUN npm install yarn RUN yarn install COPY . ./ RUN yarn run build # Copy the built application into Nginx for serving FROM nginx:stable-alpine COPY --from=build /app/build /usr/share/nginx/html COPY --from=go-downloader /go/bin/runtime-js-env / COPY docker-nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]
I need to extend this to do the following:
runtime-js-envevery time the container starts
Getting the binary to execute is straight forward:
FROM golang:1.15.6-alpine3.12 as go-downloader RUN apk update && apk upgrade && \ apk add --no-cache bash git openssh RUN go get github.com/nrmitchi/runtime-js-env
github.com/nrmitchi/runtime-js-envis forked from the original version at
github.com/lithictech/runtime-js-env. This was due to a bug that I need to fix. More on that below.
I can add this in to my existing multi-stage build, and copy the built file in to my final image.
Regarding executing the tool on start up, the nginx image has an entrypoint which will execute any scripts under
/docker-entrypoint.d/. This is perfect for our use case.
We can drop the following script in to that directory, and it will execute any time the container starts:
/runtime-js-env -i /usr/share/nginx/html/index.html && \ chmod 644 /usr/share/nginx/html/index.html
chmodcommand is necessary due to a bug with
runtime-js-envmangling file permissions. This can be removed when this bug is fixed.
We’ll also need to ensure the script is executable:
chmod a+x /docker-entrypoint.d/docker-nginx-startup-runtime-env.sh
# Build the React app with Yarn FROM node:13.12.0-alpine as build WORKDIR /app ENV PATH /app/node_modules/.bin:$PATH COPY package.json ./ RUN npm install yarn RUN yarn install COPY . ./ RUN yarn run build # Download and build our environment injector FROM golang:1.15.6-alpine3.12 as go-downloader RUN apk update && apk upgrade && \ apk add --no-cache bash git openssh RUN go get github.com/nrmitchi/runtime-js-env # Copy the built application into Nginx for serving FROM nginx:stable-alpine COPY --from=build /app/build /usr/share/nginx/html # Copy the runtime-js-env binary COPY --from=go-downloader /go/bin/runtime-js-env / COPY docker-nginx.conf /etc/nginx/conf.d/default.conf # Add our startup script RUN echo "/runtime-js-env -i /usr/share/nginx/html/index.html && chmod 644 /usr/share/nginx/html/index.html" > /docker-entrypoint.d/docker-nginx-startup-runtime-env.sh RUN chmod a+x /docker-entrypoint.d/docker-nginx-startup-runtime-env.sh EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]
Now we have an image that we can deploy into multiple environments, and configure with environment variables at run time. The exact set of environment variables which will be injected into the React app is configurable, but defaults to anything matching
Hopefully this works out for you, or was at least helpful. If you have any questions, don’t hesitate to shoot me an email, or follow me on twitter @nrmitchi.