Repository: kunokdev/cra-runtime-environment-variables Branch: master Commit: 78732b3a9ab0 Files: 17 Total size: 24.3 KB Directory structure: gitextract_z_pgf2i6/ ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── conf/ │ └── conf.d/ │ ├── default.conf │ └── gzip.conf ├── docker-compose.yml ├── env.sh ├── package.json ├── public/ │ ├── index.html │ └── manifest.json └── src/ ├── App.css ├── App.js ├── App.test.js ├── index.css ├── index.js └── serviceWorker.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js # testing /coverage # production /build # misc .DS_Store .env.local .env.development.local .env.test.local .env.production.local npm-debug.log* yarn-debug.log* yarn-error.log* # Temporary env files /public/env-config.js env-config.js ================================================ FILE: Dockerfile ================================================ # => Build container FROM node:alpine as builder WORKDIR /app COPY package.json . COPY yarn.lock . RUN yarn COPY . . RUN yarn build # => Run container FROM nginx:1.15.2-alpine # Nginx config RUN rm -rf /etc/nginx/conf.d COPY conf /etc/nginx # Static build COPY --from=builder /app/build /usr/share/nginx/html/ # Default port exposure EXPOSE 80 # Copy .env file and shell script to container WORKDIR /usr/share/nginx/html COPY ./env.sh . COPY .env . # Make our shell script executable RUN chmod +x env.sh # Start Nginx server CMD ["/bin/sh", "-c", "/usr/share/nginx/html/env.sh && nginx -g \"daemon off;\""] ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) Facebook, Inc. and its affiliates. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Runtime Environment Variables with Create React App, Docker (and Nginx) This repository shows how to implement **runtime** environment variables. Unlike traditional solutions, this allows you to configure your React application via environment variables without need to build once again. This repository is explained deeply within Medium blog post: https://medium.com/free-code-camp/how-to-implement-runtime-environment-variables-with-create-react-app-docker-and-nginx-7f9d42a91d70 --- There are many ways to configure your React application, in this post I will aim to show you approach which respects [Twelve-Factor App methodology](https://en.wikipedia.org/wiki/Twelve-Factor_App_methodology), meaning that it enforces reconfiguration during runtime, therefore no build per environment would be required. ![](https://cdn-images-1.medium.com/max/1600/0*X2czIkbrJQuKpgM5.jpeg) ### 🤔 What do we want to achieve? We want to be able to run our React application as Docker container that is built once and runs everywhere. We want to reconfigure our container **during runtime. **The output should be lightweight and performant container which serves our React application as static content, which we achieve by using Ngnix Alpine. Our application should allow configuration within docker-compose file such as this: ``` version: "3.2" services: my-react-app: image: my-react-app ports: - "3000:80" environment: - "API_URL=production.example.com" ``` We should be able configure our React application using ` -e`` flag (environment variables) when using `Docker run` command. > Basic users might not need this approach and can be satisfied with buildtime > configuration which is easier to reason about on the short run, but if you are > targeting dynamic environments that might change or you are using some kind of > orchestration system, this approach is something that you might consider. ### 🧐 The problem First of all, it must be clear that there is no such thing as environment variables inside browser environment. Whichever solution we use nowadays, is nothing but a fake abstraction. But, then you might ask, what about `.env` files and `REACT_APP` prefixed environment variables which come [straight from documentation](https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables)? Even inside source code these are used as `process.env` just like we use environment variables inside Node.js. In reality, object `process` does not exist inside browser environment, it's Node specific. CRA by default doesn't do server-side rendering, so it can't inject environment variables during content serving (like [Next.js](https://github.com/zeit/next.js) does), it doesn't include server as such, so in this case, ** during transpiling**, Webpack process replaces all occurrences of `process.env` with string value that was given. This means **it can only be configured during build time**. ### 👌 Solution There is specific moment when it is still possible to inject environment variables, it happens when we start our container. Then we can read environment variables from inside container and write them into file which can be served via Nginx (which also serves our React app) and imported into our application using ` ``` > index.html Let's display our environment variable within application: ```

API_URL: {window._env_.API_URL}

``` > src/App #### 🛠 Development During development, if we don't want to use Docker, we can run bash script via npm script runner by modifying package.json: ``` "scripts": { "dev": "chmod +x ./env.sh && ./env.sh && cp env-config.js ./public/ && react-scripts start", "test": "react-scripts test", "eject": "react-scripts eject", "build": "react-scripts build'" }, ``` > package.json And if we run `yarn dev` we should see output like this: ![](https://cdn-images-1.medium.com/max/1600/1*e4ugnbph1YnN3uVbH2QNZA.png) There are two ways to reconfigure environment variables within development; either change default value inside `.env` file or override defaults by running `yarn dev`command with environment variables prepended: ``` API_URL=https://my.new.dev.api.com yarn dev ``` ![](https://cdn-images-1.medium.com/max/1600/1*MHnRJn_JkV33mmK6Yh1raw.png) And finally edit `.gitignore` so that we exclude environment configurations out of source code: ``` # Temporary env files /public/env-config.js env-config.js ``` As for development environment, that's it! We are half-way there. However, We didn't make a huge difference at this point compared to what CRA offered by default for development environment, however, the true potential of this approach shines in production. #### 🌎 Production Now we are going to create minimal Nginx configuration so that we can build optimized image which serves production-ready application. ``` # Create directory for Ngnix configuration mkdir -p conf/conf.d touch conf/conf.d/default.conf conf/conf.d/gzip.conf ``` Main configuration file should look somewhat like this: ``` server { listen 80; location / { root /usr/share/nginx/html; index index.html index.htm; try_files $uri $uri/ /index.html; expires -1; # Set it to different value depending on your standard requirements } error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } } ``` > conf/conf.d/default.conf It's also useful to enable gzip compression so that our assets are more lightweight during network transition: ``` gzip on; gzip_http_version 1.0; gzip_comp_level 5; # 1-9 gzip_min_length 256; gzip_proxied any; gzip_vary on; # MIME-types gzip_types application/atom+xml application/javascript application/json application/rss+xml application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/svg+xml image/x-icon text/css text/plain text/x-component; ``` > conf/conf.d/gzip.conf Now that our Nginx configuration is ready, we can finally create Dockerfile and docker-compose files: ``` touch Dockerfile docker-compose.yml ``` Initially, we use `node:alpine` image to create optimized production build of our application. Then, we build runtime image on top of `nginx:alpine` . ``` # => Build container FROM node:alpine as builder WORKDIR /app COPY package.json . COPY yarn.lock . RUN yarn COPY . . RUN yarn build # => Run container FROM nginx:1.15.2-alpine # Nginx config RUN rm -rf /etc/nginx/conf.d COPY conf /etc/nginx # Static build COPY --from=builder /app/build /usr/share/nginx/html/ # Default port exposure EXPOSE 80 # Copy .env file and shell script to container WORKDIR /usr/share/nginx/html COPY ./env.sh . COPY .env . # Make our shell script executable RUN chmod +x env.sh # Start Nginx server CMD ["/bin/sh", "-c", "/usr/share/nginx/html/env.sh && nginx -g \"daemon off;\""] ``` Now our container is ready. We can do all standard stuff with it. We can build container, run it with inline configurations and push it to repository provided by services such as [Dockerhub](https://hub.docker.com/). ``` docker build . -t kunokdev/cra-runtime-environment-variables docker run -p 3000:80 -e API_URL=https://staging.api.com -t kunokdev/cra-runtime-environment-variables docker push -t kunokdev/cra-runtime-environment-variables ``` Above `docker run` command should output application like so: ![](https://cdn-images-1.medium.com/max/1600/1*kK7Ss5ODlukXgsLNuYh0Lg.png) Lastly, let's create our docker-compose file. You will usually have different docker-compose files depending on environment and you will use `-f` flag to select which file to use. ``` version: "3.2" services: cra-runtime-environment-variables: image: kunokdev/cra-runtime-environment-variables ports: - "5000:80" environment: - "API_URL=production.example.com" ``` and if we do `docker-compose up` we should see output like so: ![](https://cdn-images-1.medium.com/max/1600/1*7TBDwzS_otshjMhQqvycmg.png) Great! We have now achieved our goal, we can reconfigure our application easily in both development and production environments in a very convenient way. We can now finally build only once and run everywhere! #### 💅 Next steps Current implementation of shell script will print all variables included within .env file, but most of the time we don't really want to expose all of them. You could implement filters for variables you don't want to expose using prefixes or similar technique. #### 🧩 TypeScript You may run into [Type errors as mentioned in this issue](https://github.com/kunokdev/cra-runtime-environment-variables/issues/12). To solve this, extend `window` object or rewrite global window type as suggested in issue comments. #### 🐓 Alternative solutions As noted above, buildtime configuration will satisfy most use cases and you can rely on default approach using .env file per environment and build container for each environment, inject values via CRA Webpack provided environment variables. You could also have a look at [CRA Github repository issue](https://github.com/facebook/create-react-app/issues/2353) which covers this problem. By now, there should be more posts and issues which cover this topic and each offers similar solution as above, it's up to you to decide how are you going to implement specific details, you as well might use Node.js to serve your application which means that you can also replace shells script with Node.js script, but note that Nginx is more convenient to serve static content. ================================================ FILE: conf/conf.d/default.conf ================================================ server { listen 80; add_header Cache-Control no-cache; location / { root /usr/share/nginx/html; index index.html index.htm; try_files $uri $uri/ /index.html; expires -1; } error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } } ================================================ FILE: conf/conf.d/gzip.conf ================================================ # Enable Gzip compressed. gzip on; # Enable compression both for HTTP/1.0 and HTTP/1.1 (required for CloudFront). gzip_http_version 1.0; # Compression level (1-9). # 5 is a perfect compromise between size and cpu usage, offering about # 75% reduction for most ascii files (almost identical to level 9). gzip_comp_level 5; # Don't compress anything that's already small and unlikely to shrink much # if at all (the default is 20 bytes, which is bad as that usually leads to # larger files after gzipping). gzip_min_length 256; # Compress data even for clients that are connecting to us via proxies, # identified by the "Via" header (required for CloudFront). gzip_proxied any; # Tell proxies to cache both the gzipped and regular version of a resource # whenever the client's Accept-Encoding capabilities header varies; # Avoids the issue where a non-gzip capable client (which is extremely rare # today) would display gibberish if their proxy gave them the gzipped version. gzip_vary on; # Compress all output labeled with one of the following MIME-types. gzip_types application/atom+xml application/javascript application/json application/rss+xml application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/svg+xml image/x-icon text/css text/plain text/x-component; # text/html is always compressed by HttpGzipModule ================================================ FILE: docker-compose.yml ================================================ version: "3.2" services: cra-runtime-environment-variables: image: kunokdev/cra-runtime-environment-variables ports: - "5000:80" environment: - "API_URL=production.example.com" ================================================ FILE: env.sh ================================================ #!/bin/sh # line endings must be \n, not \r\n ! echo "window._env_ = {" > ./env-config.js awk -F '=' '{ print $1 ": \"" (ENVIRON[$1] ? ENVIRON[$1] : $2) "\"," }' ./.env >> ./env-config.js echo "}" >> ./env-config.js ================================================ FILE: package.json ================================================ { "name": "cra-runtime-environment-variables", "version": "0.1.0", "license": "MIT", "dependencies": { "react": "^16.6.3", "react-dom": "^16.6.3", "react-scripts": "2.1.1" }, "scripts": { "dev": "chmod +x ./env.sh && ./env.sh && cp env-config.js ./public/ && react-scripts start", "test": "react-scripts test", "eject": "react-scripts eject", "build": "sh -ac '. ./.env; react-scripts build'" }, "eslintConfig": { "extends": "react-app" }, "browserslist": [ ">0.2%", "not dead", "not ie <= 11", "not op_mini all" ] } ================================================ FILE: public/index.html ================================================ React App
================================================ FILE: public/manifest.json ================================================ { "short_name": "React App", "name": "Create React App Sample", "icons": [ { "src": "favicon.ico", "sizes": "64x64 32x32 24x24 16x16", "type": "image/x-icon" } ], "start_url": ".", "display": "standalone", "theme_color": "#000000", "background_color": "#ffffff" } ================================================ FILE: src/App.css ================================================ .App { text-align: center; } .App-logo { animation: App-logo-spin infinite 20s linear; height: 40vmin; } .App-header { background-color: #282c34; min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: calc(10px + 2vmin); color: white; } .App-link { color: #61dafb; } @keyframes App-logo-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } ================================================ FILE: src/App.js ================================================ import React, { Component } from "react"; import logo from "./logo.svg"; import "./App.css"; class App extends Component { render() { return (
logo

API_URL: {window._env_.API_URL}

); } } export default App; ================================================ FILE: src/App.test.js ================================================ import React from "react"; import ReactDOM from "react-dom"; import App from "./App"; const window = global; window._env_ = { API_URL: "https://github.com" }; it("renders without crashing", () => { const div = document.createElement("div"); ReactDOM.render(, div); ReactDOM.unmountComponentAtNode(div); }); ================================================ FILE: src/index.css ================================================ body { margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } code { font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; } ================================================ FILE: src/index.js ================================================ import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import * as serviceWorker from './serviceWorker'; ReactDOM.render(, document.getElementById('root')); // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. // Learn more about service workers: http://bit.ly/CRA-PWA serviceWorker.unregister(); ================================================ FILE: src/serviceWorker.js ================================================ // This optional code is used to register a service worker. // register() is not called by default. // This lets the app load faster on subsequent visits in production, and gives // it offline capabilities. However, it also means that developers (and users) // will only see deployed updates on subsequent visits to a page, after all the // existing tabs open on the page have been closed, since previously cached // resources are updated in the background. // To learn more about the benefits of this model and instructions on how to // opt-in, read http://bit.ly/CRA-PWA const isLocalhost = Boolean( window.location.hostname === 'localhost' || // [::1] is the IPv6 localhost address. window.location.hostname === '[::1]' || // 127.0.0.1/8 is considered localhost for IPv4. window.location.hostname.match( /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ ) ); export function register(config) { if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { // The URL constructor is available in all browsers that support SW. const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); if (publicUrl.origin !== window.location.origin) { // Our service worker won't work if PUBLIC_URL is on a different origin // from what our page is served on. This might happen if a CDN is used to // serve assets; see https://github.com/facebook/create-react-app/issues/2374 return; } window.addEventListener('load', () => { const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; if (isLocalhost) { // This is running on localhost. Let's check if a service worker still exists or not. checkValidServiceWorker(swUrl, config); // Add some additional logging to localhost, pointing developers to the // service worker/PWA documentation. navigator.serviceWorker.ready.then(() => { console.log( 'This web app is being served cache-first by a service ' + 'worker. To learn more, visit http://bit.ly/CRA-PWA' ); }); } else { // Is not localhost. Just register service worker registerValidSW(swUrl, config); } }); } } function registerValidSW(swUrl, config) { navigator.serviceWorker .register(swUrl) .then(registration => { registration.onupdatefound = () => { const installingWorker = registration.installing; if (installingWorker == null) { return; } installingWorker.onstatechange = () => { if (installingWorker.state === 'installed') { if (navigator.serviceWorker.controller) { // At this point, the updated precached content has been fetched, // but the previous service worker will still serve the older // content until all client tabs are closed. console.log( 'New content is available and will be used when all ' + 'tabs for this page are closed. See http://bit.ly/CRA-PWA.' ); // Execute callback if (config && config.onUpdate) { config.onUpdate(registration); } } else { // At this point, everything has been precached. // It's the perfect time to display a // "Content is cached for offline use." message. console.log('Content is cached for offline use.'); // Execute callback if (config && config.onSuccess) { config.onSuccess(registration); } } } }; }; }) .catch(error => { console.error('Error during service worker registration:', error); }); } function checkValidServiceWorker(swUrl, config) { // Check if the service worker can be found. If it can't reload the page. fetch(swUrl) .then(response => { // Ensure service worker exists, and that we really are getting a JS file. const contentType = response.headers.get('content-type'); if ( response.status === 404 || (contentType != null && contentType.indexOf('javascript') === -1) ) { // No service worker found. Probably a different app. Reload the page. navigator.serviceWorker.ready.then(registration => { registration.unregister().then(() => { window.location.reload(); }); }); } else { // Service worker found. Proceed as normal. registerValidSW(swUrl, config); } }) .catch(() => { console.log( 'No internet connection found. App is running in offline mode.' ); }); } export function unregister() { if ('serviceWorker' in navigator) { navigator.serviceWorker.ready.then(registration => { registration.unregister(); }); } }