54

I am using docker to build my react application and deploy it in nginx.

I have set an environment variable in docker-compose.yml

version: '2'
services:
  nginx:
    container_name: ui
    environment:
      - HOST_IP_ADDRESS= xxx.xxx.xx.xx
    build:
      context: nginx/
    ports:
      - "80:80"

After the docker container is created I can see hi when I echo the variable inside the container.

However, when I am trying to read it in react using process.env.HOST_IP_ADDRESS it is logging undefined.

I read in a blogpost somewhere that the env variables can be only accessed in production environment. Since, I am building the app and deploying it in nginx, I should be able to access it, but for some reason I am not able to read it.

Am I doing something fundamentally wrong here. If so, please let me know a solution. I am not a react expert, I am just managing someone else's code.

UPDATE:

The Dockerfile looks as follows:

FROM node:8 as ui-builder

WORKDIR /home/ui

COPY helloworld .

RUN npm install

RUN npm run build

FROM nginx
COPY --from=ui-builder /home/ui/build /usr/share/nginx/html
CMD ["nginx", "-g", "daemon off;"]

The React Component snippet is as follows:

import React, { Component } from 'react';

class HelloWorld extends Component {
  render() {
    console.log(process.env.HOST_IP_ADDRESS);
    return (
      <div className="helloContainer">
        <h1>Hello, world!</h1>
      </div>
    );
  }
}

export default HelloWorld;
ganapathy
  • 1,407
  • 3
  • 13
  • 20
  • 3
    react on frontend - env on backend – xadm Aug 30 '18 at 18:31
  • @xadm So, the env that I am trying to set is IP Address of the host docker machine. This Ip address will be used by the front end to access backend apis. – ganapathy Aug 30 '18 at 18:33
  • @xadm The above code is just a test snippet that I was trying out before I actually touched the real code base. – ganapathy Aug 30 '18 at 18:40
  • compile in react app or use some backend tech (php) to pass info into app (html containing js) and make app parameter aware – xadm Aug 30 '18 at 18:42
  • What @xadm is saying is that nginx simply serves the JavaScript code to the browser, and the browser (which has no idea any of this Docker stuff is happening) sees `process.env.HOST_IP_ADDRESS` and isn't really sure what to do with it. – David Maze Aug 30 '18 at 23:24
  • @DavidMaze Thanks, it makes a lot of sense. Is there any way to pass the env variable `HOST_IP_ADDRESS` inside the react app when I am building the react project using `RUN npm run build` in Dockerfile – ganapathy Aug 30 '18 at 23:57
  • That's very dependent on your JavaScript build chain; [Webpack's DefinePlugin is one way to do it](https://webpack.js.org/plugins/define-plugin/). – David Maze Aug 31 '18 at 00:47
  • @DavidMaze Thanks for your suggestion. I see some files in the code base which uses webpack. I will try this out and let you know – ganapathy Aug 31 '18 at 17:09

9 Answers9

69

I would like to thank everyone who posted answers and comments.The problem that I was facing was solved using a combination of these answers and some help from other resources.

As suggested by @DavidMaze (in the comments), I started looking into webpack config present in my code. I found out that the webpack was reading all the environment variables declared inside the container.

So I started experimenting with my Dockerfile and docker-compose.yml as I realized that REACT_APP_HOST_IP_ADDRESS was not being passed as an environment variable when the react was building the code.

The first thing I changed was the Dockerfile. I statically declared the IP inside dockerfile for testing
ENV REACT_APP_HOST_IP_ADDRESS localhost. By doing this I was able to see the value localhost inside the env variables which were read by webpack.

Now I tried passing the ENV Variable from docker-compose to dockerfile as suggested by @Alex in his answer, but it didn't work.

So I referred to https://github.com/docker/compose/issues/5600 and changed the docker-compose.yml and Dockerfile as follows

docker-compose.yml

version: '2'
services:
  nginx:
    container_name: ui
    build:
      context: nginx/
      args:
        REACT_APP_HOST_IP_ADDRESS: ${IP_ADDRESS}
    ports:
      - "80:80"

where IP_ADDRESS is exported as an env variable.

Dockerfile

FROM node:8 as ui-builder

WORKDIR /home/ui

COPY helloworld .

RUN npm install

ARG REACT_APP_HOST_IP_ADDRESS

ENV REACT_APP_HOST_IP_ADDRESS $REACT_APP_HOST_IP_ADDRESS

RUN npm run build

FROM nginx
COPY --from=ui-builder /home/ui/build /usr/share/nginx/html
CMD ["nginx", "-g", "daemon off;"]

React Component

import React, { Component } from 'react';

class HelloWorld extends Component {
  render() {
    console.log(process.env.REACT_APP_HOST_IP_ADDRESS);
    return (
      <div className="helloContainer">
        <h1>Hello, world!</h1>
      </div>
    );
  }
}

export default HelloWorld;

This configuration makes available the variables passed via ARG in docker-compose to Dockerfile during the image build process and hence the variables can be in turn declared as env variables which React can use during build process provided the webpack reads the env variables.

The webpack will be able to read the env variables using DefinePlugin https://webpack.js.org/plugins/define-plugin/.

Make sure you prefix your variables with REACT_APP_ (as seen here), otherwise it won't be picked up by React.

i01573
  • 75
  • 2
  • 11
ganapathy
  • 1,407
  • 3
  • 13
  • 20
  • 4
    So where is the actual value for `REACT_APP_HOST_IP_ADDRESS` arg saved? – Ghasem Feb 22 '20 at 08:40
  • 3
    For me it worked without the `ENV REACT_APP_HOST_IP_ADDRESS $REACT_APP_HOST_IP_ADDRESS` line in the `Dockerfile`. – Sanlok Lee Jul 28 '20 at 01:52
  • This answer solved my problem, but it is slightly incomplete. For those who are having difficulty, just look at this complementary answer for the time of execution of the "docker build" command. https://stackoverflow.com/questions/42297387/docker-build-with-build-arg-with-multiple-arguments – Uder Moreira Sep 30 '21 at 21:14
  • 1
    This is exactly what I was facing from 40 hours. Thanks a lot! – Aditya Gaddhyan Jan 25 '23 at 10:30
7

You should check next moments

I. You env variables have prefix REACT_APP_

II. In docker file you have ARG and ENV commands like

ARG REACT_APP_DEBUG
ENV REACT_APP_DEBUG=$REACT_APP_DEBUG

III. you pass your arg as build arg in docker-compose.yml it looks like

services:
  my-app:
    build:
      args:
        REACT_APP_DEBUG=True

or in docker build it looks like

docker build -t my_app:dev --build-arg REACT_APP_DEBUG=True .
Ryabchenko Alexander
  • 10,057
  • 7
  • 56
  • 88
4

Env variables should start with REACT_APP_ otherwise NODE_ENV variables are a bit confused and your environment variable will not work:

environment:
  - REACT_APP_DEBUG=TRUE

Otherwise, docker-compose.yml is not valid and you will see an error message:

services.client.environment contains an invalid type, it should be an object, or an array

Here is a working sample:

docker-compose.yml

version: "3.3"

services:
  client:
    container_name: client
    environment:
      - REACT_APP_DEBUG=TRUE
    build:
      dockerfile: Dockerfile
      context: ./web/client

Dockerfile

FROM node:6.0.0

# Set env variable
ARG REACT_APP_DEBUG
ENV REACT_APP_DEBUG=$REACT_APP_DEBUG

# that will be empty
RUN echo "DEBUG": $REACT_APP_DEBUG

Run:

->docker-compose run  client node
->process.env.REACT_APP_DEBUG 
'TRUE'
Rubén Fanjul Estrada
  • 1,296
  • 19
  • 31
Alex
  • 2,074
  • 1
  • 15
  • 14
  • Hey, I tried your suggestion, react is still not able to access the environment variable. I have updated the question with few other snippets that I am using to test this scenario. Please let me know if you see any other problems :) – ganapathy Aug 30 '18 at 21:32
  • Added one more line in Docker file to set env variable. – Alex Aug 30 '18 at 22:11
  • Will not work because you need to prefix the variable with REACT_APP_ – Rubén Fanjul Estrada Jul 09 '19 at 09:42
2

Here is my solution using ENV in my Dockerfile, DefinePlugin in the webpack.config.js and process.env in my javascript codes:

First set you environment variable and its value in your Dockerfile :

...
RUN npm install
ENV MY_ENV_VAR my_env_value
...

Then using DefinePlugin plugin, add it to process.env in webpack.config.js:

const webpack = require('webpack');
...
  plugins: [
    new webpack.DefinePlugin({
      'process.env.MY_ENV_VAR': JSON.stringify(env.MY_ENV_VAR),
    }),
  ],
...

And finally use the env variable in your code:

const host = process.env.MY_ENV_VAR || 'a_default_value_in_case_no_env_is_found';
Ghasem
  • 14,455
  • 21
  • 138
  • 171
  • I am trying your solution @Ghasem, shouldn't we use JSON.stringify(process.env.MY_ENV_VAR) above ? – Nikhil Jun 16 '23 at 12:06
1

I checked how it's done in API Platform, config just defines consts based on env ('.env' file):

export const API_HOST = process.env.REACT_APP_API_ENTRYPOINT;
export const API_PATH = '/';

Importing this you have one value (API_HOST) while process.env.HOST_IP_ADDRESS refers to deep object structure unavailable at runtime.

xadm
  • 8,219
  • 3
  • 14
  • 25
  • That's one of the reasons why I want to read the env variable during build process of react and compile the env variable. – ganapathy Aug 31 '18 at 17:11
  • So according to you all I need to do is create a `.env` file in react app and read the `process.env.HOST_IP_ADDRESS` into a variable and import the file wherever I need to use it. Right? If so, this sounds simple. I will try this out first. – ganapathy Aug 31 '18 at 17:39
0

I use Github CI to set secrets per env. First in GH action file I use run: docker build ... --build-arg REACT_APP_APIURL=${{ secrets.REACT_APP_APIURL }} .

Then I use them in Dockerfile in my repo to build there final image with the React app like:

ARG REACT_APP_APIURL
RUN test -n "$REACT_APP_APIURL" || (echo "REACT_APP_APIURL not set in GH/your environment" && false)
...
RUN npm run build

This value is used in the npm run build automatically (used in my react typescript codebase as process.env.REACT_APP_APIURL). I chose to check for this value and let the app fail immediately on load if something is wrong with my Docker image or configuration somewhere.

export const config = {
  apiUrl: setApiUrlFromEnv(),
};
  
function setApiUrlFromEnv() {
  if (process.env.REACT_APP_APIURL) {
    return process.env.REACT_APP_APIURL;
  } else {
    // if something goes wrong in setup, do not start app and show err directly in web console
    throw new Error(
      `ENV not configured properly (REACT_APP_APIURL) to use desired ENV variables (${process.env.REACT_APP_APIURL})`
    );
  }
}
dkocich
  • 221
  • 4
  • 7
0

Step 1: Add args for env to docker-compose file

We use args instead of environment field because environment fields are not available on build stage

services:
    ...
    web_app:
        build:
          context: .
          dockerfile: Dockerfile
          args:
            - MY_ENV=HELLO_WORLD

Step 1_alternative: If image is built by cloudbuild instead of docker-compose, we should add args to cloudbuild.yml file

steps:
- name: ...
    args:
    ...
    - --build-arg
  - MY_ENV=HELLO_WORLD

Step: 2: Add ARGS and ENVS to dockerfile

We use ARG command to get variables from docker-compose args

We use ENV to set env for the build

ARG MY_ENV

ENV MY_ENV=$MY_ENV

RUN echo "$MY_ENV"

Step 3: Update webpack config

  • Use webpack.ProvidePlugin({ process: "process/browser" }) to enable process in web app
  • Use webpack.DefinePlugin to define env variables available in web app
  • Add process library to dev dependency by npm i -S process
plugins: [
    new webpack.ProvidePlugin({
      process: "process/browser"
    }),
    new webpack.DefinePlugin({ "process.env": JSON.stringify(process.env) })
  ]
SchemeSonic
  • 376
  • 5
  • 12
0

Technically, we can't use environment variables in browser context, that's why we usually use DefinePlugin or EnvironmentPlugin in webpack based projects like CRA and Vue-CLI to statically replace process.env.* with environment variables.

But this way forces us to rebuild the whole application multiple times (e.g., development. staging and production).

To fix this, I want to share you a set of plugins: import-meta-env, with these plugins, you only need to define and pass the env you want to use and the plugins do the rest for you.

During production, you can use this plugin to statically replace import.meta.env.* with some expression (we use import.meta because process.env is a Node specific object), and on the startup of container, you can run a special script to inject your environment variables which may passed from docker run, stored in your Google Cloud Run, etc.

I have also created an example for Docker.

Hope this helps you and people who needs it.

Ernest
  • 174
  • 7
0

Accessing container environment at startup time with typescript / react / docker

Here a solution that works with .env files that can be included via env_file: myapp.env in docker-compose or directly as .env.

Basic idea is following this approach https://blog.codecentric.de/react-application-container-environment-aware-kubernetes-deployment

Basic idea

Provide a config.js file as static hosted resource under public at container startup. Use the entrypoint of the docker container to generate the config.js. Link to the config.js within index.html to make it available in the app.

Full working example

Step by step instruction. Git repo here

  1. Create example app
npx create-react-app read-env-example --template typescript

  1. Navigate to fresh app
cd read-env-example
  1. Create Dockerfile
mkdir -p docker/build

docker/build/Dockerfile

# build environment
FROM node:19-alpine3.15 as builder
WORKDIR /app
ENV PATH /app/node_modules/.bin:$PATH

COPY package.json ./
COPY package-lock.json ./
RUN npm ci
RUN npm install react-scripts@5.0.1 -g
COPY . ./
RUN PUBLIC_URL="." npm run build

# production environment
FROM nginx:stable-alpine
COPY --from=builder /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

COPY docker/build/docker-entrypoint.sh /
RUN chmod +x docker-entrypoint.sh

ENTRYPOINT ["/docker-entrypoint.sh"]

  1. Create docker-entrypoint.sh

This script will be executed at container start. It generates the config.js file containing all environment variables starting with 'MYAPP' under window.extended.

docker/build/docker-entrypoint.sh

#!/bin/sh -eu


function generateConfigJs(){
    echo "/*<![CDATA[*/";
    echo "window.extended = window.extended || {};";
    for i in `env | grep '^MYAPP'`
    do
        key=$(echo "$i" | cut -d"=" -f1);
        val=$(echo "$i" | cut -d"=" -f2);
        echo "window.extended.${key}='${val}' ;";
    done
    echo "/*]]>*/";
}
generateConfigJs > /usr/share/nginx/html/config.js

nginx -g "daemon off;"
  1. Create docker-compose.yml
mkdir docker/run

docker/run/docker-compose.yml

version: "3.2"
services:

  read-env-example:
    image: read-env-example:0.1.0
    ports:
      - 80:80
    env_file:
      - myapp.env
    
  1. Create runtime config for your app

docker/run/myapp.env

MYAPP_API_ENDPOINT='http://elasticsearch:9200'
  1. Create config.js <-- this is where .env will be injected.

public/config.js

/*<![CDATA[*/
window.extended = window.extended || {};
window.extended.MYAPP_API_ENDPOINT='http://localhost:9200';
/*]]>*/

Note: This file will be completely overwritten by the docker-entrypoint.sh. For development purposes you can set it to any value that is appropriate, e.g. when used together with npm start.

  1. Include config.js in index.html

public/index.html

   <head>
     ...
    <script type="text/javascript" src="%PUBLIC_URL%/config.js" ></script>
     ...
   </head>
   <body>
  1. Make use of your environment variable

src/index.tsx

...
declare global {
    interface Window { extended: any; }
}

root.render(
  <React.StrictMode>
    <App {...{MYAPP_API_ENDPOINT:window.extended.MYAPP_API_ENDPOINT}}/>
  </React.StrictMode>
);
...

src/App.tsx

...
type Config={
    MYAPP_API_ENDPOINT:string
}
function App(props : Config) {
  return (
    <div className="App">
      <header className="App-header">
        <div>
          You have configured {props.MYAPP_API_ENDPOINT}
        </div>
      </header>
    </div>
  );
}
...

src/App.test.tsx

test('renders learn react link', () => {
  render(<App {...{MYAPP_API_ENDPOINT:"teststring"}}/>);
  const linkElement = screen.getByText(/You have configured teststring/i);
  expect(linkElement).toBeInTheDocument();
});
  1. Build and test
npm install
npm test
  1. Create docker image
docker build -f docker/build/Dockerfile -t read-env-example:0.1.0 .
  1. Run container
docker-compose -f ./docker/run/docker-compose.yml up
  1. Navigate to your app

Open http://localhost in your browser. You will see the content of MYAPP_API_ENDPOINT like you provided in your docker/run/myapp.env.

  1. Further usage

You can provide additional variables starting with MYAPP. The docker-entrypoint.sh script will search for all variables starting with MYAPP and make them available through the windows object.

jschnasse
  • 8,526
  • 6
  • 32
  • 72
  • A slightly different approach uses a volume mount to mount in the `config.js` directly. Like described [here](https://stackoverflow.com/a/53439439/1485527). – jschnasse Dec 07 '22 at 14:18