Recreate config file

Environment-specific configuration is essential for deploying web applications across multiple environments like development, staging, and production. However, hard-coding config values makes your application less portable and more brittle. A better approach is to use environment variables that can be easily changed between deploys without modifying the code.

While Node.js provides access to environment variables via process.env, browser-based apps like those made with Create React App (CRA) don‘t have this option. CRA supports build-time environment variables using .env files, but that requires rebuilding for each environment.

Ideally, we‘d like to inject environment variables at runtime when the container starts up. This allows us to build a single Docker image that can be reconfigured on the fly for different environments. By following the 12 Factor App methodology, we can make our application more portable and scalable.

In this guide, I‘ll show you how to implement runtime environment variables in a Create React App project using Docker and Nginx. We‘ll cover the following topics:

  • Generating an example CRA project
  • Writing a bash script to convert .env to JavaScript
  • Modifying the Dockerfile to inject environment variables
  • Configuring Nginx to serve the React app
  • Running the container with docker-compose
  • Deploying to multiple environments

By the end, you‘ll have a React app that can be easily configured at runtime using environment variables. Let‘s get started!

Step 1: Generate a new React project

First, make sure you have Node.js installed. Then open your terminal and run:

npx create-react-app cra-runtime-env-demo
cd cra-runtime-env-demo

This generates a default CRA project in the cra-runtime-env-demo directory.

Next, create a file called .env in the project root with some example environment variables:

REACT_APP_API_BASE_URL=https://api.example.com
REACT_APP_GA_TRACKING_ID=UA-12345678-1

We prefix the variable names with REACTAPP so CRA will include them in the build.

Step 2: Write a bash script to inject environment variables

To convert our .env file to a JavaScript file that can be imported in the browser, create a new file called env.sh in the project root:

#!/bin/bash

rm -rf ./env-config.js touch ./env-config.js

echo "window.env = {" >> ./env-config.js

while read -r line || [[ -n "$line" ]]; do

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

value=$(printf ‘%s\n‘ "${!varname}")

[[ -z $value ]] && value=${varvalue}

echo " $varname: \"$value\"," >> ./env-config.js done < .env

echo "}" >> ./env-config.js

This script does the following:

  1. Removes any existing env-config.js file and creates a new one
  2. Writes JS code to assign a config object to window.env
  3. Reads each line of the .env file and splits on the = character
  4. Checks if the variable is set in the environment, otherwise falls back to .env
  5. Appends each key-value pair to the config JS object

To include the generated config in your app, add this line to the of public/index.html:

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

Now your React code can access the environment variables under window.env. For example:

console.log(window._env_.REACT_APP_API_BASE_URL)

Be sure to add /public/env-config.js to your .gitignore so you don‘t commit it.

Step 3: Configure NGINX

To serve our React app using Nginx, create the following files and directories:

mkdir -p nginx/conf.d
touch nginx/conf.d/default.conf

Add the following config to nginx/conf.d/default.conf:

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; } }

This tells Nginx to serve files from /usr/share/nginx/html and fallback to index.html for client-side routing. The expires -1 header prevents the browser from caching so it always gets the latest config.

We can also add compression by creating nginx/conf.d/gzip.conf:

gzip on;
gzip_http_version  1.0;
gzip_comp_level    5; # 1-9
gzip_min_length    256;
gzip_proxied       any;
gzip_vary          on;

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;

This enables gzip compression for common web MIME types.

Step 4: Create a multi-stage Dockerfile

With our Nginx config in place, we can now write a Dockerfile that builds the React app and injects the environment config:

# Build app
FROM node:alpine as builder
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile
COPY . .
RUN yarn build

FROM nginx:stable-alpine COPY --from=builder /app/build /usr/share/nginx/html COPY --from=builder /app/env.sh /usr/share/nginx/html COPY --from=builder /app/.env /usr/share/nginx/html COPY nginx/conf.d /etc/nginx/conf.d RUN apk add --no-cache bash RUN chmod +x /usr/share/nginx/html/env.sh CMD ["/bin/bash", "-c", "/usr/share/nginx/html/env.sh && nginx -g \"daemon off;\""]

This multi-stage Dockerfile uses node:alpine to build the React app, then copies the static files to an nginx:stable-alpine image. It also copies over our env.sh script and .env file, and sets up the Nginx config.

The CMD directive runs our bash script to generate the config and starts the Nginx server.

Step 5: Use docker-compose to build and run

We‘re now ready to build our Docker image and run the container. While you could use docker build and docker run directly, it‘s often more convenient to use docker-compose.

Create a docker-compose.yml file in the project root:

version: ‘3‘
services:
  web:
    build: 
      context: .
      dockerfile: Dockerfile
    image: cra-runtime-env
    env_file: .env  
    environment:
      - PORT=8080
    ports:
      - 80:80

This defines a web service that builds our Dockerfile and starts the container with the environment variables from the .env file. It also exposes port 80.

To build the image and start the container, run:

docker-compose up --build

Once it‘s done, you should be able to access the app at http://localhost.

Step 6: Deploy to multiple environments

With our Docker setup complete, we can now easily deploy to multiple environments just by changing the environment variables.

For example, to deploy to a staging environment, you could create a docker-compose.staging.yml file:

version: ‘3‘
services:
  web:
    image: cra-runtime-env
    env_file: .env.staging
    environment:  
      - PORT=8080
    ports:
      - 80:80

This uses the same image but mounts a different .env.staging file with staging-specific variables.

Then to deploy:

docker-compose -f docker-compose.staging.yml up

You can create compose files for as many environments as you need.

Conclusion

In this guide, we covered how to implement runtime environment variables in a Create React App project using Docker and Nginx. The key steps were:

  1. Generating the React app
  2. Writing a bash script to inject variables
  3. Configuring Nginx to serve the app
  4. Building a Docker image with multi-stage builds
  5. Running the container with docker-compose
  6. Extending to multiple environments

While this approach takes some initial setup, it makes your React app much more flexible and portable. By injecting environment-specific config at runtime, you can build once and deploy anywhere without changing your code.

I hope you found this guide useful! Feel free to adapt the code for your own projects. The full example code is available on GitHub at:

https://github.com/yourusername/cra-runtime-env-demo

If you have any questions or suggestions, let me know in the comments. Happy coding!

Similar Posts