-
-
Notifications
You must be signed in to change notification settings - Fork 26.9k
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
Comments
Environment variables have to be compiled in because things like And I feel like you've hit the right solution: a script tag in the head of But if we're brainstorming... Maybe env vars could somehow be denoted as being dynamic/runtime injected. A prefix of |
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. |
@heyimalex: for instance if you deploy your apps using docker, having to rebuild the app for configuration changes has some drawbacks:
ProposalI 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).
|
Here's a build script to do this. Usage:
It's pretty hacky but should be a good proof-of-concept for how the experience could be.
|
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.
|
@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. |
I solved this for my current project, which is ejected, by conditionally passing in config via
This fixes it for local development. I also inject config into a script block in Then in my Config component I look for config on the window object OR in I then control which config to use with It's not perfect, but it fixes it for us and we can now promote our staging build to production on Heroku without rebuilding. |
Update from a userFor 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 When serving the files, I also implemented some best-match redirects to allow the app base url ( |
How about rewriting withimmutable-js? |
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. The idea of having an |
@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? |
@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:
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:
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.) |
Is there anything we need to do on our side for this? I don't understand from reading this issue. |
Put bluntly: If we start a container/webserver w/some static js compiled through CRA, we would like e.g. |
Hi everyone, I just wanted to chime in with my current workaround, adapted from some comments above (pscanf's and heyimalex's):
// 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())
app.get('/env.js', function (req, res) {
res.sendFile(path.join(__dirname, 'build', 'env.js'))
})
node ./build-env.js > build/env.js
const __process = (typeof global !== 'undefined' ? global : window).process This has the same downsides @pscanf mentioned:
as well as the |
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 Very Dirty (c) but functional |
@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 |
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. |
@The-Loeki Everything in EDIT: But thinking about it more, if |
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 ;) |
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. |
@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: |
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 |
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 Note: I'm not JS developer, our guys hacked together this solution in our environment. Hope it helps someone. |
@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 |
All of the proposed solutions do seem to work for user-defined variables. However, and if I understood correctly, none of them allow for If this is the case, has anyone reached a solution for it? Is the only option to have distinct builds per environment currently? |
@PedroGuerraPT : app-server also allows you to set You just need to set |
@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. |
@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. |
@jakubknejzlik, maybe the "runtime" term is not appropriate for my objective. The intent is really just to separate the app config ( As far as I'm aware, and other comments are mentioning the same, the point is that The proposed solutions on this issue do work for user-defined variables, but not for Having a distinct build per environment just because of one variable doesn't seem right. |
You could try setting the |
One of the reasons why This means you have to use relative values.
becomes something like this:
|
Hello, You can check my solution here: |
I came here looking for runtime environment variable in Docker, and The one thing that seems unsettled though is how to define I have solved this by building upon #
# 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 Note that Along with this, I need to configure 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 |
What about
I have sample repo (https://github.com/zaverden/frontend-env) where you can find more details and working example. |
Great solution by @ashconnell. I had to use |
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? |
We took a bit convoluted solution, but it has some benefits:
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 serverBuild with 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:
cons:
|
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 Just generally wondering if there are any drawbacks for my solution or is there a different reason no one used this approach? |
@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 I'll come back with details if everything goes well (or more importantly if it doesn't haha) |
Same challenge - following! |
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:
We wrote about it here: https://serverless-stack.com/chapters/setting-serverless-environments-variables-in-a-react-app.html |
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>
... |
We ended up by mounting a config.js file inside the public folder, like this config.js
Then mount this config.js on this path where apache (or nginx) expects it: Inside index.html (public folder) reference the
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
For local development (without Apache/Nginx), you can build the docker image and provide the config.js file manually inside the public folder. |
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:
Try it out and feel free to leave feedback! |
- 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 } ```
[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
Hope this package helps someone looking for this. Feel free to give your feedback. |
what helped me to serve the same CRA build from different domains (prod vs stage) and at the same time serve
|
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. |
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:
In the past, I have implemented the following two solutions:
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! 🙏
The text was updated successfully, but these errors were encountered: