Rendering an environment variable to the browser in a react.js redux production build running in different environments
Asked Answered
A

4

11

The readme of the react redux realworld.io application at https://github.com/gothinkster/react-redux-realworld-example-app says to edit the src/agent.js to change the API_ROOT to point to a different backend api instance. We want to set things up so that API_ROOT can be defined by an environment variable that is different within the multiple environments (e.g., “staging” and “live”) where we run the production build.

We are running in containers on openshift kubernetes following 12factor.net principles where the code is built once then promoted through environments. We can spin up new environments with a single command so we don’t want to have a switch statement within the code that names each environment and hardcodes the backend API_ROOT for each environment. Instead, I want to be able to run an existing production build container image in a fresh environment using an environment variable change the API_ROOT to point to the correct backend API we want to test against.

I have looked at a number of different blogs, stackoverflow answers and the official documentation. The main problem is that typical solutions “bake in” the process.env.API_ROOT environment variable at build time else have a switch that hardcodes the details of all environments into the code. Neither of which are satisfactory as we want to able to take the latest stable code in an existing container and run it in a new environment using the API running there.

The closest I have got so far is to edit the code to render the process.env.API_ROOT into a <script> tag that sets it on a window.API_ROOT variable. Then check whether that exists else use a default when defining the const for API_ROOT. This feels very invasive and a bit fragile and it is not clear to me where is the best place to render such a script tag in the sample app at https://github.com/gothinkster/react-redux-realworld-example-app

Aleece answered 23/4, 2018 at 7:39 Comment(0)
A
17

Issue #578 of react-create-app has a good answer. tibdex suggested using a public/env.js that is generated with the correct properties then in the index.html add:

 <script src="%PUBLIC_URL%/env.js"></script>

That env.js script can set the API_ROOT on the window:

window.env={'API_ROOT':'https://conduit.productionready.io/api'}

And agent.js can check for the window.env.API_ROOT else default:

function apiRoot() {
  if( window.env.API_ROOT !== 'undefined') {
    return window.env.API_ROOT
  }
  else {
    return 'https://conduit.productionready.io/api'
  }
}

const API_ROOT = apiRoot();

Exactly how that file is created from an environment variable he doesn't describe but I was able to have the npm start command generate it.

Moorman then suggested simply writing an express server that serves that /env.js else index.html:

const express = require('express');
const path = require('path');

const app = express();

app.use(express.static(path.join(__dirname, 'build')));

const WINDOW_ENV = "window.env={'API_ROOT':'"+process.env.API_ROOT+"'}\n";

app.get('/env.js', function (req, res) {
  res.set('Content-Type', 'application/javascript');
  res.send(WINDOW_ENV);
});

app.get('/*', function (req, res) {
  res.sendFile(path.join(__dirname, 'build', 'index.html'));
});

app.listen(process.env.PORT);

To get that to work the start script in the package.json is simply:

"start": "PORT=8080 node server.js",

Then everything works. If API_ROOT is defined in environment variables then the server.js will generate it on window.env and the agent.js will use it.

update I set a cache time of five minutes on env.js with res.setHeader("Cache-Control", "public, max-age=300"); as the setting is rarely going to change.

update I read a lot of confusion around this topic and people answering it along the lines of ”change your workflow to align to the defaults of the tools”. The idea of 12-factor is to use a workflow that is established as best practice that the tools should follow, not vice-versa. Specifically a tagged production ready container should be configurable by environment variables and promoted through environments. Then it's "the same thing" that is debugged and tested that runs in live. In this case of a single page app it requires that the browser makes a trip to the server to load the environment variables rather than baking them into the app. IMHO this answer is a straightforward and simple way of doing that to be able to follow 12-factor best practices.

update: @mikesparr gives a good answer to this problem at https://github.com/facebook/create-react-app/issues/982#issuecomment-393601963 which is to restructure the package.json to do the webapp work of generating the SPA upon start up. We took this approach as a tactical workaround. We are using a saas openshift kubernetes that charges for memory. Building our react app with webpack needs 1.2Gb (and rising!) So this approach of moving the npm build to the container startup command we need to allocate 1.2Gb to every pod we start which is a significant amount of additional costs for a single page app whereas we can get away with 128MB as the memory allocation when the app is precompiled. The webpack step is also slow as it is a large app. Building every time we start up the app slows down rolling deployments by many minutes. If a VM crashes and kubernetes starts replacement containers on a new VM it takes minutes to start up. A precompiled app starts in a few seconds. So the solution of "webpack at startup" is not satisfactory in terms of resource consumption and speed for real business application that are tens of thousands of lines of code. IMHO this answer of fetching a configuration script from the server is superior.

Aleece answered 23/4, 2018 at 21:8 Comment(7)
"a tagged production build in a container should be configurable by environment variables" — I'm with you there; I left some comments in that issue as well, as I don't think many people deploying React understand what makes 12fa so powerful. We shouldn't be running containers with Node.js in them and compiling things at container start, and we also shouldn't have to build multiple container images for different environments!Anesthesiology
@Anesthesiology I am really surprised by the amount of confusion on this and that there isn’t a standard boilerplate or plugin for it. As we are deploying multiple languages across multiple services it’s an obvious deficiency to see one framework break best practice. Most folks seem to be happy to apply the work around of compile upon startup. Yet for us that means our single page app needs 1G and we can run four backend server side apps in a different language in that much memory.Aleece
yeah, I just timed it on our infra, and using a production artifact and the nginx-alpine container I can get a 20 MB container to startup in about 120ms, or I can use the 'build on start' technique and the container size is ~200 MB, and takes 63,000ms to start. And uses 100% of the allocated CPU the entire startup time! Definitely a major gap in the React ecosystem, IMO.Anesthesiology
I just posted a blog post on this topic because I know I'm going to run into this again, and I'll forget the 5+ hours I sunk into trying to find any official documented way of doing it: Deploying a React single-page web app to Kubernetes. Thanks for your posts here and on GitHub!Anesthesiology
im confused, this is an api endpoint that just serves environment variables? is that secure?Pinette
@Pinette it is an endpoint that provides "public" data to the single page app that is "environment specific". the example code takes those public values from some environment variables. that is an implementation detail. if you liked you can store that public data in a database or a kubernetes secret.Aleece
Would be good to mention here that "window" is a default object in Javascript in browser environment, whereas "window.env" is a convention which everyone follows to store app-specific environment variablesScrummage
P
6

You can replace the environment variables directly in your index.html file exposing a global ENV variable. That replacement needs to be done at runtime to make sure that you have a portable image that you can run in different environments.

I have created an example repository here https://github.com/axelhzf/create-react-app-docker-environment-variables

Privy answered 12/1, 2019 at 19:18 Comment(2)
🎉 nice one that’s a lot less effort. can you add to the readme what the js looks like as reading the sh file it’s not obvious. thx!Aleece
problem is all the process.env.REACT_APP_..., needs to be updated as window.ENV.REACT_APP_...Molybdous
P
3

Take a look at Immutable Web Apps!

It is a methodology that creates a separation of concern between index.html and all other static assets:

  1. It treats index.html as a deployment manifest that contains all environment-specific values.

This is similar to the accepted answer, by including the environment variables directly in the index.html

window.env={'API_ROOT':'https://conduit.productionready.io/api'}

It also requires that the reference to other static assets are unique and versioned.

  1. It treats javascript bundles as immutable assets that are built once, published once, and used in multiple environments. Allowing the assets to be promoted through environments to production without being modified or moved.

It honors both the "build, release, run" and "config" principles of 12factor.

A great benefit of this approach is that it enables atomic live releases by simply publishing index.html.

Peevish answered 7/1, 2019 at 15:19 Comment(3)
yes, I could so that with openshift kubernetes. I have a git repo for config and that’s how I set the env vars differently in each env. I can create a k8s “secret” containing the different index.html for each env in my config repo and attach that as a file in a folder into deployments. every application image (aka pod) which can be a nginx image set to serve the index.html from the “secret” folder. then one nginx container is configured via a secret in the env that has, as they call it, a manifest if the html. at this point it seems a lot more work to setup than running a small express server.Aleece
we have other services running that use other languages and use env vars in a normal fashion. so sticking with the accepted answer would be less surprises for colleagues looking to maintain things. also if we have config shared by apps of different frameworks and languages a kubernetes configmap or secret that is set as env vars in multiple apps works for all. writing logic to have it also copied into the index.html would be another surprise for anyone maintaining the solution down the line. for a pure SPA world it looks quite elegant and well documented. if you have a CDN already even better.Aleece
@Aleece I agree that the accepted answer is the simplest solution for running in a kubernetes cluster. I thought I would add an alternative for anyone looking for a 12factor approach to running a SPA in multiple environments. Great question and answer!Peevish
D
0

It has been a while since the question was posted, but i seem to have a nice simple workaround .

create a js file that access the window.location.hostname;

function getEnvConfig(key){
  let host = '';
  if (typeof window !== 'undefined') {
    host = window.location.hostname;
  }
  console.log(`Hostname: ${host}`);

  if((host.includes('localhost') || host.includes('dev'))) {
    return envConfigs.dev[key];
  } else if(host.includes('stage')) {
    return envConfigs.stage[key];
  } else if(host.includes('prod')) {
    return envConfigs.prod[key];
  }
}

and then have the config for each env in separate js file..

const envConfigs = {
    dev: {
        NEXT_PUBLIC_API_URL: "https://test.com",
        
    },

    stage: {
        
    },
    prod: {
        
    },
};


module.exports = {
    envConfigs
};

this works ! and it should work in every env.

Dupre answered 23/2, 2023 at 17:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.