Skip to content

Reading Config Vars in Docker

Often you will need to read in config values in a react app, for example when connecting to a backend API, or when using config values to change various UI elements or control enabled features.

When using a full stack framework such as Next.JS or Gatsby, you can use the process.env object to read in environment variables as these applications are both server and client side rendered.

If we are using a client side only framework we will not have the luxury of using process.env. In this case we need to be able to load in our own configuration values into the app running in a Docker container. To this end we can create a config.json file, and serve this file in the Docker container mounted as a volume.

For this demo we'll be using a simple Vite react frontend, with an ExpressJS backend. We'll be using Docker to containerize our application.

Creating a Backend

Setup

First get started by creating a backend directory, and inside it a package.json with the following content:

json
{
  "name": "backend"
}

Installing Dependencies

bash
cd backend
npm install typescript express cors
npm install -D @types/express @types/cors @types/node ts-node

Creating the backend

In our case the backend will be a simple ExpressJS server that returns a list of users, so we can make this all one file:

typescript
import express from "express";
import cors from "cors";

const USERS = [
    { id: 1, name: 'John Doe', email: '[email protected]' },
    { id: 2, name: 'Jane Doe', email: '[email protected]' },
    { id: 3, name: 'John Smith', email: '[email protected]' }
]

const app = express();
app.use(cors());

app.get('/', (_, res) => {
    res.send('Hello World!');
});

app.get('/users', (_, res) => {
    res.json(USERS);
});

app.listen(3000, () => {
    console.log('Server is running on port 3000');
});

Setting Up Typescript

Create a tsconfig.json file in the backend directory with the following content:

json
{
  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs",
    "esModuleInterop": true,
    "strict": true,
    "skipLibCheck": true,
    "rootDir": "./src",
    "outDir": "./dist",
  }
}

Add Scripts

Add the following scripts to the package.json file:

json
{
    "scripts": {
        "dev": "ts-node src/index.ts",
        "build": "tsc",
        "start": "node dist/index.js"
    },
}

Creating a Frontend

Setup

Create a new Vite React TS app using the command npm create vite@latest frontend -- --template react-ts, ensuring you are in the parent directory of the backend directory.

Ignore the Local Config File

Update the .gitignore file to ignore the config.json file we will be creating later:

# ...
public/config.json

Create Config File

Create a config.js file in the public directory with the following content:

js
const config = {
    apiUrl: 'http://localhost:3000'
}

window.config = config;

Next update the index.html file to include the config.json file:

html
<!-- ... -->
    <script src="/config.js"></script>
</head>
 <!-- ... -->

Create a Config Util

Create a config.ts file in the src directory with the following content:

typescript
export interface Config {
    apiUrl: string;
}

export const config = (window as any).config as Config;

Update App

Update the app to call the backend API:

tsx
import { useEffect, useState } from "react"
import { config } from "./config"

type User = {
  id: number, 
  name: string
  email: string
}

function App() {
  const [users, setUsers] = useState([] as User[])

  useEffect(() => {
    const fetchUsers = async () => {
      const response = await fetch(`${config.apiUrl}/users`)
      const data = await response.json()
      setUsers(data)
    }
  
    fetchUsers();
  }, [])

  return (
    <>
      <h1>Users</h1>
      <br />
      <ul>
        {users.map((user) => (
          <li key={user.id}>
            {user.name} - ({user.email})
          </li>
        ))}
      </ul>
    </>
  )
}

export default App

Running the Application

At this stage you will be able to run the application locally by running the backend and frontend separately. The backend will expose the user list, and the frontend will display the list of users.

Dockerise

Finally the last step is dockerising the application. Create the following Dockerfiles, docker-compose and config files in the root of the project:

FROM node:alpine

WORKDIR /app

COPY package.json .
COPY package-lock.json .

RUN npm install

COPY . .

RUN npm run build

CMD ["npm", "start"]
FROM node:alpine as build

WORKDIR /app

COPY package.json .
COPY package-lock.json .

RUN npm install

COPY . .

RUN npm run build

FROM nginx:alpine 

COPY --from=build /app/dist /usr/share/nginx/html

ENTRYPOINT ["nginx","-g","daemon off;"]
yml
services:
  api:
    build:
      context: './backend'
      dockerfile: '../Dockerfile.backend'
    ports:
      - "8081:3000"
    
  web:
    build:
      context: './frontend'
      dockerfile: '../Dockerfile.frontend'
    ports:
      - "8080:80"
    volumes:
      - './config-production.js:/usr/share/nginx/html/config.js'
js
const config = {
    apiUrl: 'http://localhost:8081'
}

window.config = config;

If you then run docker-compose up you should see the application running in a Docker container and when navigating to http://localhost:8080.

Conclusion

In this guide we have seen how to read config values in a React app running in a Docker container. We have created a config.js file that we use to store our config values, and then use a volume to mount this file into the Docker container. This allows us to read the config values in our React app.

As a follow up this config.js file would be created per environment, or even created as part of the CI/CD pipeline, so that the correct values are used for each environment. Please note however as this is a client side app all values will be visible to the end user, so do not store any sensitive information in the config.js file.