Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Runtime environment variables #2353

Open
kmalakoff opened this issue May 24, 2017 · 80 comments
Open

Runtime environment variables #2353

kmalakoff opened this issue May 24, 2017 · 80 comments

Comments

@kmalakoff
Copy link

Regarding this pull request around the improvements to environment variables, based on @gaearon's suggestion, I wanted to start a discussion on how to handle a docker-centric, 12 factor app-based workflow where environment variables are provided externally at runtime rather than at build time so that the exact same assets can be run in multiple environments.

Constraints / design goals mentioned is:

It's important though to note that CRA always produces static bundles, and they are expected to work in any environment regardless of server, container, etc.

In the past, I have implemented the following two solutions:

  1. render environment variables into the HTML and then hoist them into an application at initialization
  2. fetch environment variables from a server (for example, before creating the store and rendering the app with some static content during the initial fetch)

The first solution's benefit is that there is no delay before initial render, but with create react app dynamically modifying the html file, it becomes a little more tricky to implement since you would need to parse or search / replace within the rendered html before serving the file.

I'm just wondering if there is a better / best way to provide runtime environment variables to CRA applications and if we can get agreement on an approach, if it can be integrated into the CRA pipeline.

Thank you for all the great work and hoping for something awesome here! 🙏

@heyimalex
Copy link
Contributor

Environment variables have to be compiled in because things like NODE_ENV lead to tons of dead code elimination, so whatever solution you're looking for will probably need to coexist with env vars as they are today.

And I feel like you've hit the right solution: a script tag in the head of public/index.html that defines the variables, either inline or as an external js file. You've got the tradeoffs right as well; an extra request vs a slightly more complex release-creation process. Really not hard to do today, and in my opinion out of the scope of create-react-app. What's wrong with just rebuilding for configuration changes?

But if we're brainstorming...

Maybe env vars could somehow be denoted as being dynamic/runtime injected. A prefix of REACT_APP_RUNTIME or something. They get compiled into global variable references, something like window.runtimeEnvVars.<varname>, and instead of an index.html you get an index.html.template and a little script that can combine your partial build with a .env configuration into a full release.

@kmalakoff
Copy link
Author

Interesting and totally understand if this might be out scope for CRA.

The main problem is that when I consult, I need to tell clients who deploy via docker and 12 factor app principles to ignore the environment variable solution in CRA which is a bit confusing since one would expect that CRA's environment variables solution would meet this common use case. To be honest, I'm actually not sure of why anyone would want build time environment variables, but it could be that I've been using and advocating for 12 factor app principles to clients for too long...

In your brainstorming ideas, is there a way to inject code into the CRA index.html at runtime currently? If not, that sounds like an interesting line of potential solutions.

@pscanf
Copy link

pscanf commented Jun 7, 2017

What's wrong with just rebuilding for configuration changes?

@heyimalex: for instance if you deploy your apps using docker, having to rebuild the app for configuration changes has some drawbacks:

  • you have to include node and all the build toolchain in your image
  • the startup time for your container increases significantly
  • your container resource requirements increase significantly

Proposal

I suggest an approach similar to the one I used for sd-builder, a tool conceptually very similar to CRA that I've built back when CRA didn't yet exist (and that I would like to discontinue in favour of CRA).

sd-builder's approach

In apps built with sd-builder, configuration is loaded from a file - app-config.js - that you include in your app's index.html. The file defines the global variable window.APP_CONFIG, which holds the configuration object.

During development, the sd-builder dev server generates the file from the variables listed in the .env file.

sd-builder also provides a command - sd-builder config - to generate the file from (scoped) environment variables.

Creating the production bundle is therefore a two-step process:

  • first you run sd-builder build, that builds the app
  • then you run sd-builder config, which only generates the configuration file

When deploying with docker, you'd typically run the first step when building the docker image, and the second step when running the container.

This solves the last two drawbacks listed above. It doesn't solve the first one because you still need sd-builder to generate the config file, but that could be remedied by extracting the generation logic (30 lines of js) into a separate, smaller utility (maybe even with a C/bash port to cut down on runtime dependencies).

Suggestion for CRA

The above approach could be used in CRA without touching CRA. You add app-config.js in your index.html and develop a script that:

  • during development, you run before react-scripts start. The script generates app-config.js from .env, and places it in the public/ folder (I guess you should then .gitignore it)
  • at "production runtime", you run before starting your static server. The script generates app-config.js from environment variables and places it in the build/ folder

However this approach has some issues:

  • it's a bit hackish
  • it doesn't allow to use environment variables in index.html
  • it cannot be used to set process.env.PUBLIC_URL at runtime (and PUBLIC_URL feels to me like a runtime variable)

So I would suggest to bring that behaviour into CRA: in development, by making react-script start generate the config file and the configured index.html, in non-development, by providing a react-script config script that does it. (You could actually do without app-config.js, and simply embed the configuration in the index.html).


Thoughts? (@kmalakoff @gaearon @Timer)

@heyimalex
Copy link
Contributor

Here's a build script to do this. Usage:

  • Define some variables in your .env with the prefix REACT_APP_RUNTIME_
  • Run the build script. It should run the regular react-script build process and create an extra file in the build directory called gen-index.js.
  • Run that gen-index script with your runtime env vars set and it'll generate an index.html with those variables defined.

It's pretty hacky but should be a good proof-of-concept for how the experience could be.

  • ✔️ Development mode works exactly the same as today: runtime variables are only aliased in production builds.
  • ✔️ Build-time env vars are not going away, so any new runtime configuration system must coexist with what's already there. This strategy allows users to opt in without breaking the old system or creating an entirely new separate system.
  • ✔️ Rebuilding the index is fast.
  • ✔️ All of the static assets are still static.
  • ❌ Doesn't do PUBLIC_URL. Didn't try it, haven't put any thought into it.
  • ❌ It's one more thing to think about. How many people would really use this feature? How much confusion could it cause? Does this get in the way of wider uses of env vars?
  • ❌ This current take is hacky, relies on string replacement/manipulation and so may not be hygenic, messes up source maps, messes up file hashes. All of that can be fixed with enough work I think. Which brings me to...
  • ❌ With enough work, someone could do this safely as a separate package.

@hoolymama
Copy link

I ended up here as was struggling with this too but since found a different solution. I'd be interested to know if it violates the any best practices, 12 step principles or if there are any security concerns.

  • I Set up a small express server to serve the static app, plus one api route called /config. When the app is run a json object is created with the whitelisted variables pulled from the environment, or defaults.
  • In the react app I made a higher order component withConfig() that fetches from the /config route and provides the result as props to the wrapped component.
  • To make this work in dev mode I set the proxy in package.json to point to the express server. This means in dev mode I have to start both the express server (npm start) and dev server (react-scripts start).

Here's a gist

@jakubknejzlik
Copy link

@hoolymama thanks for the gist. I've been struggling with this problem in few projects and IMHO it's crucial to be able to specify environment variables for configuring the application.

Although it might not be in scope for CRA, it would be nice to unify some approach and be able to use it without and further setup needed.

@adrianblynch
Copy link

I solved this for my current project, which is ejected, by conditionally passing in config via /config/env.js:getClientEnvironment():

function getClientEnvironment(publicUrl) {
  // Code...
  NODE_ENV: process.env.NODE_ENV || "development",
  PUBLIC_URL: publicUrl,
  CONFIG: process.env.NODE_ENV === "development" ? config : "" // Only for development
  // Code...
}

This fixes it for local development.

I also inject config into a script block in index.html on the server (Express or Koa).

Then in my Config component I look for config on the window object OR in process.env.CONFIG.

I then control which config to use with CONFIG_ENV=ci|staging|whatever yarn dev|serve.

It's not perfect, but it fixes it for us and we can now promote our staging build to production on Heroku without rebuilding.

@pscanf
Copy link

pscanf commented Sep 21, 2017

Update from a user

For those who might be interested, I've built a static server "specialized" for serving and configuring at runtime create-react-app apps: staticdeploy/app-server. It also allows for an easy "dockerization" of the apps.

To allow runtime configuration, it just uses an alternative configuration mechanism. So it doesn't work out-of-the-box with create-react-app, but making it work is simply a matter of adding a script tag to public/index.html, so it's not too bad.

When serving the files, I also implemented some best-match redirects to allow the app base url (PUBLIC_URL) to be configured at runtime as well.

@sr1994lu
Copy link

sr1994lu commented Oct 8, 2017

How about rewriting withimmutable-js?

@sazzer
Copy link

sazzer commented Dec 21, 2017

My big concern with some of the solutions is - I'd like to be able to host my app on a CDN that only serves static files, whilst at the same time I'd like to know that the software deployed to Dev, QA, PreProd and Prod are all the same.

If I'm needing to dynamically generate files on startup, that fails the CDN requirement.
If I'm needing to rebuild the entire application in order to deploy into different environments, that fails the QA/PreProd/Prod requirements.

The idea of having an app-config.js file that is deployed alongside the built application is the best I can think of so far. It means that the entire application is built once, and on deployment that file is added alongside it for deployment-specific details - such as backend URLs to call. It is another moving part to think about though, but not a very big one. It can probably be done in the Docker setup using just shell scripting as well, not needing a complex setup. Just a bunch of echo statements into a well-known filename passing in environment variables.

@jakubknejzlik
Copy link

jakubknejzlik commented Dec 21, 2017

@sazzer can you be more specific how does it fails the CDN requirement? Is it the recommendation of static content, which can be change just by changing the environment variables?

@sazzer
Copy link

sazzer commented Dec 21, 2017

@jakubknejzlik The CDNs that I've looked at serve up static files. They don't let you run server processes to dynamically generate content. This would mean that the files that are served to the browsers need to be the files that are uploaded, as-is.

What I'm really hoping for - but feeling less and less hopeful about - is a process similar to:

  • Change is made to source code
  • Ci system builds the code
    • CI system automatically runs "npm test"
    • CI system automatically runs "npm run build"
    • CI system automatically puts a copy of the "build" directory into our artifact repository
    • CI system automatically deploys this latest build to the Dev system
  • At some point, an artifact is deployed to QA
    • The appropriate artifact is picked from the repository and deployed as-is to the QA systems
  • At some point, an artifact is deployed to PreProd
    • The appropriate artifact is picked from the repository and deployed as-is to the Prod systems
  • At some point, an artifact is deployed to Prod
    • The appropriate artifact is picked from the repository and deployed as-is to the Prod systems

For the backend - which is a Java app - this is really simple. The Java process can be run with system properties on a per-environment basis to provide the appropriate config - database credentials right now - but the actual files deployed are identical from one environment to the next.

For the frontend, because it's CRA which produces static files, this is not so easy.

The best I've been able to come up with so far is for the frontend deployment to be:

  • The frontend artifact contents
  • Another file dropped next to index.html that contains the appropriate configuration needed.

This technically means that the deployment onto each environment is not the same, but it's as close as I can make it right now. This means that I can't guarantee that the deployment to Prod will work just because it passed all of the tests at the earlier stages. (And no, technically I can't guarantee that with the backend with its environment properties, but it's a lot easier to manage and verify that. Start service, call healthcheck endpoint, on failure rollback.)

@gaearon
Copy link
Contributor

gaearon commented Jan 8, 2018

Is there anything we need to do on our side for this? I don't understand from reading this issue.

@The-Loeki
Copy link

Put bluntly: If we start a container/webserver w/some static js compiled through CRA, we would like e.g. PUBLIC_URL to be able to be set at runtime by the orchestration/webserver, not at build time.

@josephfrazier
Copy link
Contributor

Hi everyone, I just wanted to chime in with my current workaround, adapted from some comments above (pscanf's and heyimalex's):

  • Create build-env.js with the following contents:
// adapted from https://github.com/facebook/create-react-app/issues/2353#issuecomment-306949558
if (!process.env.NODE_ENV) {
  process.env.NODE_ENV = 'development'
}

// clientEnv is generated this way because we don't want to expose all of process.env,
// since it contains secrets (private API keys, etc)
const clientEnv = require("react-scripts/config/env")().raw;

console.log(`
// Auto-generated by build-env.js, DO NOT EDIT
window.process = { env: ${
  JSON.stringify(clientEnv, null, 2)
}}`.trim())
  • Add <script src="%PUBLIC_URL%/env.js"></script> to the <head> of public/index.html
  • If you're using a dynamic server, ensure it serves the /env.js path. For Express, this looks something like:
app.get('/env.js', function (req, res) {
  res.sendFile(path.join(__dirname, 'build', 'env.js'))
})
  • Configure your server to run the following on startup (e.g. the prestart script in package.json:
node ./build-env.js > build/env.js
  • Finally, in your component/etc files, replace process.env with __process.env and add the following at the top:
const __process = (typeof global !== 'undefined' ? global : window).process

This has the same downsides @pscanf mentioned:

  • it's a bit hackish
  • it doesn't allow to use environment variables in index.html
  • it cannot be used to set process.env.PUBLIC_URL at runtime (and PUBLIC_URL feels to me like a runtime variable)

as well as the __process variable thing, but it works well enough for me.

@The-Loeki
Copy link

We set the PUBLIC_URL and some other stuffs we know are configurable to e.g. PUBLIC_URL_REPLACE_ME and do some good old fashioned sed to 'fix' it in the init script ;)

Very Dirty (c) but functional

@heyimalex
Copy link
Contributor

@The-Loeki I think that potentially breaks sourcemaps and caching in weird ways. Even if you take out the service worker stuff, you have to remember to not set far future cache headers on /static/{css,js}/*, since your find-replace could make them not-so-static (which also means you're not getting the benefits of static asset caching). There are ways to fix this but they just add more complexity...

@The-Loeki
Copy link

We only serve the statically generated files, no nodejs & such; for PUBLIC_URL we traverse all builded files to fix them.

The one thing you have to be aware of regarding caching besides the headers is to not use relative URLs; they warn about that somewhere in the code.

@heyimalex
Copy link
Contributor

heyimalex commented Feb 28, 2018

@The-Loeki Everything in /static/ has a content hash appended to the filename so that you can safely set far-future caching headers. When you modify those files using sed, the contents change but the hashes don't. That means if you accidentally release with the wrong PUBLIC_URL, fix it and then release again, everyone who already got the bad build will continue to see that build until their cache expires or is cleared. Same idea with the service worker I think. Just something to consider!

EDIT: But thinking about it more, if PUBLIC_URL is wrong, the files won't be loaded in the first place. So maybe this is not actually an issue? 🤷‍♂️

@The-Loeki
Copy link

The-Loeki commented Mar 1, 2018

héhéhé I'm actually pretty sure that that is an issue, albeit a very minor one; our devs have had to force-reload their browsers regularly while moving the app around.

OTOH while I do strongly support it being a runtime configurable, it's not like it's going to move every day.

So thanks for pointing it out because we've been a bit suprised by the behaviour, but not overly bothered by it.

I might cook up some nasty-ass fix for it as well, but don't wait up for it ;)

@SpacePotatoBear
Copy link

I solved this using a hackish solution here https://stackoverflow.com/questions/51653931/react-configuration-file-for-post-deployment-settings

ideally there would be a clean native way (i.e import a js file that doesn't get bundled) but this is a good work around.

@lirbank
Copy link

lirbank commented Aug 3, 2018

@SpacePotatoBear I like your model/workaround. I currently do it based on the URL the client is accessed from - posted an example in your thread:

https://stackoverflow.com/a/51663697/1959584

@alexvicegrab
Copy link

A native way of having runtime environmental variables would be very helpful when deploying in Dockerised environments (e.g. Kubernetes) and following a 12-factor app approach, specifically https://12factor.net/build-release-run

@shinebayar-g
Copy link

Create React App runtime environment variables are much needed for containerized environment. Currently once its built there is no way to change environment variables. However there are some workaround like using additional config.js file and read from there. Even it's still file that need to be modified. In containerized environment everything should be setup by environment variables, not through some file. We've come very close to better solution.
Idea is instead of reading from process.env, actually you can read from external url with get request. In response for get request there would be little nodejs server that returns its own process.env file's value which can be configured at runtime.

Note: I'm not JS developer, our guys hacked together this solution in our environment. Hope it helps someone.

@jakubknejzlik
Copy link

jakubknejzlik commented Nov 9, 2018

@shinebayar-g that's exactly where we've ended up too but in slightly different way. We created docker wrapper image that just during the container start creates js file with values from process.env ("REACT_APP_*" only; assigned to window.ENV) and starts nginx. Not sure if it works, but maybe it could help someone :-)
https://github.com/inloop/cra-docker

@PedroGuerraPT
Copy link

PedroGuerraPT commented Feb 27, 2020

All of the proposed solutions do seem to work for user-defined variables. However, and if I understood correctly, none of them allow for PUBLIC_URL to be overwritten after building the app, which renders all options unsuitable when hosting the app through a CDN on multiple environments.

If this is the case, has anyone reached a solution for it? Is the only option to have distinct builds per environment currently?

@pscanf
Copy link

pscanf commented Feb 27, 2020

@PedroGuerraPT : app-server also allows you to set PUBLIC_URL dynamically.

You just need to set PUBLIC_URL to ./ during the build stage, so that all generated links are relative. At serve time, the asset-serving algorithm is smart enough to figure out which asset to serve, even if requests do not perfectly match its path (detailed explaination).

@PedroGuerraPT
Copy link

@pscanf : The problem is that the application is already being served by the CDN, which only hosts static content and does not allow any command to be executed (therefore, not allowing app-server to be started). Any solution that requires a custom server to be launched is not suitable for our scenario, I'm afraid.

@jakubknejzlik
Copy link

@PedroGuerraPT from my understanding You don't have any runtime (as CDN is serving static files) and You expect to propagate runtime environments...which don't make much sense :) . Unless your CDN provider offer any sort of code execution (eg. Cloudfront's Lambda@Edge) you need to build your app or change uploaded static files during deployment anyway.

@PedroGuerraPT
Copy link

@jakubknejzlik, maybe the "runtime" term is not appropriate for my objective. The intent is really just to separate the app config (.env) from the code, not change the values during runtime.

As far as I'm aware, and other comments are mentioning the same, the point is that PUBLIC_URL cannot be overwritten any other way besides changing its value in the .env file before building the project.

The proposed solutions on this issue do work for user-defined variables, but not for PUBLIC_URL or other Advanced Configuration variables.

Having a distinct build per environment just because of one variable doesn't seem right.

@heyimalex
Copy link
Contributor

You could try setting the __webpack_public_path__ magic variable.

@Vanuan
Copy link

Vanuan commented Mar 3, 2020

One of the reasons why PUBLIC_URL can't be set in run-time is that CSS url function doesn't support dynamic values: https://stackoverflow.com/questions/858782/dynamic-urls-in-css-js

This means you have to use relative values.
But relative paths can't be used because CRA/Webpack replaces relative URIs with root-defined URIs. So that this:

.myClass { background-image: url('../images/image.png'); }

becomes something like this:

.myClass { background-image: url('__PUBLIC_URL__/static/media/images/image.png'); }

@mkazinauskas
Copy link

Hello,
I also had this problem. I have solved it with one example from this page.

You can check my solution here:
https://github.com/modestukasai/dynamic-environment-variable-react-docker

@jfairley
Copy link

jfairley commented May 12, 2020

I came here looking for runtime environment variable in Docker, and react-env did not disappoint.

The one thing that seems unsettled though is how to define PUBLIC_URL at runtime. Of course, it's not trivial, considering how that value needs to be written in index.html and other static/ output files.

I have solved this by building upon react-env and adding my own sed command to do more heavy lifting on boot.

#
# Stage 1
#
FROM node:12-alpine AS build

# Define `PUBLIC_URL` as variable to find-and-replace later
ENV PUBLIC_URL="{{base_url}}"

# Create app directory
WORKDIR /usr/src/app

# Bundle app source
COPY . .

# Install dependencies and build the app
RUN npm install --production && npm run build


#
# Stage 2
#
FROM beamaustralia/react-env:2.1.2

# Update the `react-env` entrypoint.sh file with my own command.
# The command finds `{{base_url}}` and replaces it with the value of
# `$REACT_APP_BASE_URL`, which is a runtime environment variable.
RUN sed -i "5ifind \/var\/www\/ -type f -exec sed -i'' -e 's|{{base_url}}|'\"\$REACT_APP_BASE_URL\"'|g' {} \\\;" /var/entrypoint.sh
RUN cat /var/entrypoint.sh

WORKDIR /var/www

# Copy app files from Stage 1
COPY --from=build /usr/src/app/build /var/www

I admit it's a bit of a rough job, but it solves the problem for me and hopefully others.

A caveat worth noting, because the files are mutated, if you need to change the value of $REACT_APP_BASE_URL, simply restarting the container is not enough. This is because the files are already rewritten inside the paused container. If you need to change the env, you'll have to fully rm the container. Small price to pay, IMO, especially since the PUBLIC_URL shouldn't change often. In my case, this solution allows me to support separate dev, qa, staging, and production environments without needing to build up to 4 images.

Note that $REACT_APP_BASE_URL is fully optional in this solution. If it goes undefined, its value would be empty string, in which case, you'd be left with the CRA default of files on the base routes (ex: /favicon.ico).


Along with this, I need to configure react-router. I do that like this.

import env from "@beam-australia/react-env";
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter as Router } from "react-router-dom";
import * as url from "url";
import App from "./App";

const baseUrl = env("BASE_URL");
const basePath = url.parse(baseUrl).pathname ?? "/";

const AppWithRouter = (
  <Router basename={basePath}>
    <App />
  </Router>
);
ReactDOM.render(AppWithRouter, document.getElementById("root"));

EDIT

I've created an issue on react-env about this to gauge interest in this becoming part of react-env.
andrewmclagan/react-env#27

@zaverden
Copy link

What about envsubst? In short, suggestion is following:

  1. You define your environment in index.html following way:
    <script>
        const API_BASE_URL = "http://localhost:8080"; // <- default value
        window.$$env = {
            apiBaseUrl: `${API_BASE_URL}`,
        };
    </script>
  2. Run envsubst to replace ${API_BASE_URL} with variable value
    defined_envs=$(printf '${%s} ' $(env | cut -d= -f1))
    envsubst "$defined_envs" < "$template_path" > "$output_path"
  3. All values in window.$$env are strings, you need some code to convert them to expected types

I have sample repo (https://github.com/zaverden/frontend-env) where you can find more details and working example.

@larsblumberg
Copy link

@andrewmclagan's library is almost perfect but I used it slightly different to how the docs recommend (I wanted this to be as lean as possible):

  1. Add <script src="%PUBLIC_URL%/env.js"></script> to public/index.html
  2. Install andrew's lib yarn add @beam-australia/react-env
  3. Modify the start script to be react-env --dest public && react-scripts start
  4. Modify your serve script (we use serve) to be react-env --dest build && serve -s build
  5. Use window._env.REACT_APP_* variables in your app instead of process.env.REACT_APP_*

I had to forgo using the "prestart" script because development/production have different destinations, and I really didn't want to use the docker image provided as we have our own.

I also didn't want to use a library to get the variables since all it really does is put them on window._env.

All and all this is working really well. I'm using it in multiple environments:

  • Local dev using docker-compose
  • Remote dev using k8s + skaffold
  • and in Production using k8s

Great solution by @ashconnell. I had to use window.__env though, using two underscores instead of one.

@Janjko
Copy link

Janjko commented Apr 29, 2021

Is there a way of having a 12 factor app, but in the official nginx-unprivileged image? Before in the default nginx image, I had an envsubst script, but now I don't have enough privileges to create a file in the /usr/share/nginx/html folder. What are my options?

@jackieli-tes
Copy link

We took a bit convoluted solution, but it has some benefits:

  1. yarn add -D react-app-rewired
  2. add "build:dynamic-env": "react-app-rewired build", to package.json script section
  3. add .env.production and add the env vars that needs runtime injection. e.g.
    REACT_APP_OAUTH_CLIENT_ID="--runtime-inject--"
  4. add config-overrides.js:
const HtmlWebpackPlugin = require("html-webpack-plugin");
const DefinePlugin = require("webpack").DefinePlugin;

const REACT_APP = /^REACT_APP_/i;

// reference: https://github.com/jantimon/html-webpack-plugin#events
class InjectEnvJsToIndex {
	constructor(htmlWebpackPlugin) {
		this.htmlWebpackPlugin = htmlWebpackPlugin;
	}
	apply(compiler) {
		if (!this.htmlWebpackPlugin) {
			console.warn(
				"not injecting _env.js to index.html because htmlWebpackPlugin not found"
			);
		}
		compiler.hooks.compilation.tap("InjectEnvJsToIndex", (compilation) => {
			const hooks = this.htmlWebpackPlugin.getHooks(compilation);
			hooks.alterAssetTagGroups.tap("InjectEnvJsToIndex", (data) => {
				data.headTags.push({
					tagName: "script",
					voidTag: false,
					attributes: {
						src: "/_env.js",
					},
				});
			});
			// console.log("inject env.js to index.html", hooks.alterAssetTagGroups);
		});
	}
}

module.exports = function override(config, _env) {
	if (process.env.NODE_ENV !== "production") {
		return;
	}
	let replaceRuntimeEnv;
	let htmlWebpackPlugin;
	for (let p of config.plugins) {
		// inject env var
		if (p instanceof DefinePlugin) {
			const env = p.definitions["process.env"];
			const updated = Object.keys(env)
				.filter((key) => REACT_APP.test(key))
				.filter((key) => env[key] === '"--runtime-inject--"')
				.reduce((env, key) => {
					const value = `window.${key}`;
					console.log(`injecting ${key} as ${value} in production build`);
					env[key] = value;
					return env;
				}, {});
			replaceRuntimeEnv = {
				"process.env": updated,
			};
		}
		if (p.constructor.name === "HtmlWebpackPlugin") {
			// only use react-scripts's plugin class, in case there are multiple versions of htmlWebpackPlugin
			htmlWebpackPlugin = p.constructor;
		}
	}
	if (replaceRuntimeEnv) {
		config.plugins.unshift(new DefinePlugin(replaceRuntimeEnv));
	}
	// inject script src=_env.js tag
	config.plugins.push(new InjectEnvJsToIndex(htmlWebpackPlugin));
	return config;
};

Then you could either have an init script that creates the env.js or have a simple http server that serves env.js

Example go server

Build with CGO_ENABLED=0 go build -o build/serve main.go

package main

import (
  "bytes"
  "flag"
  "fmt"
  "hash/maphash"
  "io"
  "log"
  "net/http"
  "os"
  "path/filepath"
  "strings"
  "time"
)

var BaseDir = "build"

var vars bytes.Buffer
var varsHash string

func init() {
  h := maphash.Hash{}
  w := io.MultiWriter(&vars, &h)
  for _, v := range os.Environ() {
  	if strings.HasPrefix(v, "REACT_APP_") {
  		i := strings.Index(v, "=")
  		if i < 0 {
  			log.Fatalf("env not correct: %v", v)
  		}
  		key := v[:i]
  		value := v[i+1:]
  		value = strings.ReplaceAll(value, "\"", "\\\"")
  		fmt.Fprintf(w, `window.%s="%s";`, key, value)
  	}
  }
  varsHash = fmt.Sprintf("%x", h.Sum(nil))
}

func env(w http.ResponseWriter, r *http.Request) {
  w.Header().Set("content-type", "application/javascript")
  // sensible cache expires: 10 min
  if !*debug {
  	w.Header().Set("expires", time.Now().Add(time.Minute*10).Format(http.TimeFormat))
  }
  w.Header().Set("etag", varsHash)
  if r.Header.Get("if-none-match") == varsHash {
  	w.WriteHeader(http.StatusNotModified)
  	return
  }
  w.Write(vars.Bytes())
}

func notfound(w http.ResponseWriter, _ *http.Request, err string) {
  w.WriteHeader(http.StatusNotFound)
  w.Write([]byte(err))
}

func internal(w http.ResponseWriter, _ *http.Request, err string) {
  w.WriteHeader(http.StatusInternalServerError)
  w.Write([]byte(err))
}

func denied(w http.ResponseWriter, _ *http.Request) {
  w.WriteHeader(http.StatusUnauthorized)
  w.Write([]byte("not authorised"))
}

func serve(w http.ResponseWriter, r *http.Request) {
  path := r.URL.Path
  if path == "/_env.js" {
  	env(w, r)
  	return
  }
  if path == "/index.html" || path == "/" {
  	// good idea, but always need a https server to work: http/2 needs https
  	if pusher, ok := w.(http.Pusher); ok {
  		// Push is supported.
  		options := &http.PushOptions{
  			Header: http.Header{
  				"Accept-Encoding": r.Header["Accept-Encoding"],
  			},
  		}
  		if err := pusher.Push("/_env.js", options); err != nil {
  			log.Printf("Failed to push: %v", err)
  		}
  	}
  }
  fn := filepath.Join(BaseDir, path)
  indexFn := filepath.Join(BaseDir, "index.html")
  fi, err := os.Stat(fn)
  if err != nil {
  	if os.IsNotExist(err) {
  		fn = indexFn
  	} else {
  		internal(w, r, err.Error())
  		return
  	}
  }
  if fi == nil || fi.IsDir() {
  	fn = indexFn
  }
  if path == "/service-worker.js" {
  	w.Header().Set("Cache-Control", "no-cache")
  }

  f, err := os.Open(fn)
  if err != nil {
  	internal(w, r, err.Error())
  	return
  }
  defer f.Close()

  fi, err = f.Stat()
  if err != nil {
  	internal(w, r, err.Error())
  	return
  }
  http.ServeContent(w, r, f.Name(), fi.ModTime(), f)
}

var debug = flag.Bool("debug", false, "debug mode")

func main() {
  port := flag.String("port", ":8000", "listen addr")
  cert := flag.String("cert", "", "cert for tls")
  key := flag.String("key", "", "key for tls")
  help := flag.Bool("help", false, "help")
  flag.Parse()
  addr := os.Getenv("PORT")
  if addr == "" {
  	addr = *port
  }
  if *help {
  	flag.Usage()
  	return
  }
  if flag.NArg() < 1 {
  	BaseDir = "build"
  }
  http.HandleFunc("/", serve)
  if *cert != "" && *key != "" {
  	log.Println("serving", BaseDir, "and listening tls on", addr)
  	log.Fatal(http.ListenAndServeTLS(addr, *cert, *key, nil))
  }
  log.Println("serving", BaseDir, "and listening on", addr)
  http.ListenAndServe(addr, nil)
}
Example nginx based dockerfile that creates _env.js
# Nginx based web server
FROM nginx:alpine
COPY --from=build /source/dist /usr/share/nginx/html
COPY ./nginx.conf /etc/nginx/nginx.conf
COPY ./nginx_proxy.conf /etc/nginx/nginx_proxy.conf
RUN echo -e "#!/bin/sh\n"\
  "for i in \$(env); do\n"\
  "  PREFIX=\`echo \$i | cut -c1-10\`\n"\
  "  if [ \"\$PREFIX\" = 'REACT_APP_' ]; then\n"\
  "    VAR=\`echo \$i | cut -d= -f1\`\n"\
  "    eval \"VAL=\\\${\$VAR}\"\n"\
  "    OLD=\"{\$VAR}\"\n"\
  "    echo \"replacing \$OLD with \$VAL\"\n"\
  "    sed -i \"s:\$OLD:\${VAL//:/\\:}:g\" /usr/share/nginx/html/_env.js\n"\
  "  fi\n"\
  "done\n"\
  "gzip -kc /usr/share/nginx/html/index.html > /usr/share/nginx/html/index.html.gz\n"\
  "nginx -g 'daemon off;'\n" > /init.sh
EXPOSE 80
CMD ["sh", "/init.sh"]

pros:

  • same dev workflow as react-scripts
  • injection only happens in production build

cons:

  • depending on webpack
  • a bit convoluted

@mario-subo
Copy link

Is anyone using a solution of fetching the config.js file created by docker via a http client on app startup from your assets folder instead of adding the script to the html or adding the variables to the window object?

My app will be served in a shell so only the <div id="my-root-id"></div> will be used so I can't really mess with the HTML head or anything like that. Also I'm not sure if adding things to the window object would be allowed.

Just generally wondering if there are any drawbacks for my solution or is there a different reason no one used this approach?

@ChristianJacobsen
Copy link

@mario-subo At my former company we had an express http server hosting our CRA. We had a /config route to fetch out the runtime config before we started sending out other requests (GraphQL, Auth0, etc...) and just stuck the config data in a Context.

@mario-subo
Copy link

@mario-subo At my former company we had an express http server hosting our CRA. We had a /config route to fetch out the runtime config before we started sending out other requests (GraphQL, Auth0, etc...) and just stuck the config data in a Context.

Yeah I will be trying a similar approach. One config file and just fetch it at runtime and stick it in a context. But instead of an express server I am thinking of just using a normal fetch("./config.json") and injecting the said file in docker for each environment.

I'll come back with details if everything goes well (or more importantly if it doesn't haha)

@MurrayFoxcroft
Copy link

Same challenge - following!

@jayair
Copy link
Contributor

jayair commented Jul 21, 2021

FWIW, we looked at this problem while using Create React App with a serverless backend in SST: https://github.com/serverless-stack/serverless-stack

Here's roughly how we solved it:

  1. On the backend, allow defining React environment variables using the outputs from the backend.

    // Create a React.js app
    const site = new sst.ReactStaticSite(this, "Site", {
      path: "frontend",
      environment: {
        // Pass in the API endpoint to our app
        REACT_APP_API_URL: api.url,
      },
    });
  2. Spit out a config file while starting the local environment for the backend.

  3. Then start React using sst-env -- react-scripts start, where we have a simple CLI that reads from the config file and loads them as build-time environment variables in React.

  4. While deploying, replace these environment variables based on the outputs from the backend.

We wrote about it here: https://serverless-stack.com/chapters/setting-serverless-environments-variables-in-a-react-app.html

@mstelz
Copy link

mstelz commented Aug 6, 2021

I found this article to be useful for docker images. Where we ran a script prior to build:

env.sh

#!/bin/bash

# https://create-react-app.dev/docs/adding-custom-environment-variables We should utilize these instead of the custom logic here

if [[ -z "${SPA_CONTENT_DIR}" ]]; then
  ENV_JS="./env-config.js"
else
  ENV_JS="./${SPA_CONTENT_DIR}/env-config.js"
fi

if [[ -z "${SPA_ENVIRONMENT}" ]]; then
  environmentFile=".env"
  echo "Creating default env-config.js"
else
  environmentFile=".env.${SPA_ENVIRONMENT}"
  echo "Creating env-config.js for ${SPA_ENVIRONMENT}"
fi

if [ ! -f "$environmentFile" ]; then
  echo "ERROR: Environment file $environmentFile does not exist!"
  exit 1;
fi

# Recreate config file
rm -rf ${ENV_JS}
touch ${ENV_JS}

# Add assignment 
echo "window._env_ = {" >> ${ENV_JS}

# Read each line in .env file
# Each line represents key=value pairs
while read -r line || [[ -n "$line" ]];
do
  # Split env variables by character `=`
  if printf '%s\n' "$line" | grep -q -e '='; then
    varname=$(printf '%s\n' "$line" | sed -e 's/=.*//')
    varvalue=$(printf '%s\n' "$line" | sed -e 's/^[^=]*=//')
  fi

  # Read value of current variable if exists as Environment variable
  value=$(printf '%s\n' "${!varname}")
  # Otherwise use value from .env file
  [[ -z $value ]] && value=${varvalue}
  
  # Append configuration property to JS file
  echo "  $varname: \"$value\"," >> ${ENV_JS}
done < $environmentFile

echo "}" >> ${ENV_JS}

package.json

...
scripts: {
  "build": "react-scripts build && npm run preserve",
  "preserve": "chmod +x ./env.sh && ./env.sh && cp env-config.js ./build/",
}
...

index.html

...
    <script src="/env-config.js?d=20210806"></script>
...

@justpolidor
Copy link

justpolidor commented Sep 13, 2021

We ended up by mounting a config.js file inside the public folder, like this

config.js

 window.REACT_APP_API_URL="https://base-url-here.domain.com/api";  
 window.REACT_APP_PUBLIC_URL="https://base-url-here.domain.com/login";
 window.REACT_APP_REDIRECT_URL="http://test-environment.domain.com/";
 window.REACT_APP_REDIRECT_REG="https://domain.pre.domain.com/registerPopup.html";
 window.REACT_APP_REDIRECT_FORGOT="https://domain.pre.domain.com/forgotPassword.html";

Then

mount this config.js on this path where apache (or nginx) expects it: /usr/local/apache2/htdocs/module_name/config.js

Inside index.html (public folder) reference the config.js file

<!DOCTYPE html>
<html lang="es">

<head>
  <script src="%PUBLIC_URL%/config.js"></script>
  <meta charset="utf-8" />
  <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
 [...]

Then, since we didn't want to modify our code and pollute it with different references to the window object, we created this file that does the binding:

config.ts


const REACT_APP_DEPLOY_ENV: string = window.REACT_APP_DEPLOY_ENV || '';
const REACT_APP_MAIN_SITE_HOMEPAGE: string = window.REACT_APP_MAIN_SITE_HOMEPAGE || '';
const REACT_APP_API_URL: string = window.REACT_APP_API_URL || '';
const PUBLIC_URL: string = window.PUBLIC_URL || '';
const REACT_APP_REDIRECT_URL: string = window.REACT_APP_REDIRECT_URL || '';
const REACT_APP_REDIRECT_REG: string = window.REACT_APP_REDIRECT_REG || '';
const REACT_APP_REDIRECT_FORGOT: string = window.REACT_APP_REDIRECT_FORGOT || '';

export {
  REACT_APP_DEPLOY_ENV,
  REACT_APP_MAIN_SITE_HOMEPAGE,
  REACT_APP_API_URL,
  PUBLIC_URL,
  REACT_APP_REDIRECT_URL,
  REACT_APP_REDIRECT_REG,
  REACT_APP_REDIRECT_FORGOT,
};

For local development (without Apache/Nginx), you can build the docker image and provide the config.js file manually inside the public folder.

@es-lynn
Copy link

es-lynn commented Sep 16, 2021

Hello, my team has just published a package react-inject-env that allows you to build the base files > inject environment variables > deploy app.

It works on a 2 step process:

  1. First it builds the static files with a placeholder variables.
  2. Then it takes in your actual environment variables, looks through your build files, and substitutes the placeholders variables with the real variables.

Try it out and feel free to leave feedback!

https://www.npmjs.com/package/react-inject-env

jvalkeal added a commit to jvalkeal/spring-graphql that referenced this issue Nov 26, 2021
- Drop existing single page cdn integration from boot project.
- Create new spring-graphql-graphiql module containing graphiql integration.
- Makes graphiql optional which user can pull in as a dependency.
- spring-graphql-graphiql is a basic npm module which packages itself as
  a jar where boot autoconfig can integrate to.
- In a npm module graphiql itself is handled as a plain react app which allows
  some customisation like setting logo name to demonstrate how things are passed
  from boot properties into a react app itself.
- GraphiQlHandler's are changed to handle all traffic into `/graphiql` order to:
  - Handling main html in `/graphiql/explorer`
  - Redirect to `/graphiql/explorer` to get context path under `/graphiql/`
  - Handle `main.js` from classpath to get html to load it under `/graphiql/`
  - Handle `config.js` as a way to pass configuration options from server side
    and load those into react app which is based on long discussion in
    facebook/create-react-app#2353 to overcome issues
    not hardcoding things on a compile time.
- Samples webmvc-http and webflux-websocket is changed to use this module.
  - webmvc-http is as it used to be.
  - webflux-websocket can now use subcription which gets first greeting instead
    of subscribtion request reply.

This is a draft POC, so tests and more work to npm project would be added
later to polish things a bit. Bundle via webpack is way too big right now
and didn't yet figure out why tree shaking don't work better.

This is a based on some of my old hacks I experimented with graphiql and
if looking promising would then give better foundation to think about
security and other things we'd like to have on this layer. Having a full
blown module and react code in typescript makes it easier to tweak things
instead of trying to rely on public stuff on cdn as a static app relying on
an internet access.

With `webmvc-http` you can use:

```
query {
  greeting
}
```

With `webflux-websocket` you can use both:

```
query {
  greeting
}

subscription {
  greetings
}
```
@soc221b
Copy link

soc221b commented Mar 11, 2022

[UPDATE]

I've build a simpler and more approachable way to solve this issue, for example you could even interpolate runtime environment variables in HTML:

https://github.com/runtime-env/runtime-env#runtime-env


Hi all, I just wrote some packages related to this problem.

This approach is similar to the react-inject-env above, but also works to:

  1. Webpack, Vite, Rollup projects (powered by unplugin),
  2. CSR, SSR, SSG modes,
  3. and testing environment such as Jest and Mocha (based on babel plugin).

Hope this package helps someone looking for this.

Feel free to give your feedback.

https://iendeavor.github.io/import-meta-env/

@oleg-rdk
Copy link

oleg-rdk commented Jun 29, 2022

what helped me to serve the same CRA build from different domains (prod vs stage) and at the same time serve index.html and all the rest assets from different domains (use CDN):

  1. Stop providing PUBLIC_URL in build time.
  2. Start serving env.js per environment (e.g. with window.ASSETS_PUBLIC_URL = 'production.com')
  3. Replace CRA's HtmlWebpackPlugin instance with mine that uses templateContent prop so I can construct src attribute for script tags by myself (so I can use window.ASSETS_PUBLIC_URL there). I used Craco for that.
  4. Patch loading dynamic js chunks by setting __webpack_public_path__ = window.PREPLY_ASSETS_PUBLIC_HOST in my app js entrypoint.

@iamsmruti
Copy link

I am building the image and saving it to ECR and then pulling it in a chef recipe and then I want to have control over the env variables. So, is this a good approach of handling the dockerization or I should be building the docker image right in the chef recipe instead of pulling it from the ECR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests