code-snippets/docs/react/reading-env-vars-docker.md
Liam Pietralla 8ad5845efc
Some checks failed
Build, Test & Publish / Build and Publish Container Image (push) Has been cancelled
Build, Test & Publish / Deploy to Infrastructure (push) Has been cancelled
Build, Test & Publish / Build (push) Has been cancelled
initial commit
2024-09-05 13:54:08 +10:00

308 lines
6.5 KiB
Markdown

# 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:
::: code-group
```json [backend/package.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:
::: code-group
```typescript [backend/src/index.ts]
import express from "express";
import cors from "cors";
const USERS = [
{ id: 1, name: 'John Doe', email: 'john.doe@gmail.com' },
{ id: 2, name: 'Jane Doe', email: 'jane.doe@gmail.com' },
{ id: 3, name: 'John Smith', email: 'john.smith@gmail.com' }
]
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:
::: code-group
```json [backend/tsconfig.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:
::: code-group
```json [backend/package.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:
::: code-group
``` [frontend/.gitignore]
# ...
public/config.json
```
:::
### Create Config File
Create a `config.js` file in the `public` directory with the following content:
::: code-group
```js [frontend/public/config.js]
const config = {
apiUrl: 'http://localhost:3000'
}
window.config = config;
```
:::
Next update the `index.html` file to include the `config.json` file:
::: code-group
```html [frontend/public/index.html]
<!-- ... -->
<script src="/config.js"></script>
</head>
<!-- ... -->
```
:::
### Create a Config Util
Create a `config.ts` file in the `src` directory with the following content:
::: code-group
```typescript [frontend/src/config.ts]
export interface Config {
apiUrl: string;
}
export const config = (window as any).config as Config;
```
:::
### Update App
Update the app to call the backend API:
::: code-group
```tsx [frontend/src/App.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:
::: code-group
``` [Dockerfile.backend]
FROM node:alpine
WORKDIR /app
COPY package.json .
COPY package-lock.json .
RUN npm install
COPY . .
RUN npm run build
CMD ["npm", "start"]
```
``` [Dockerfile.frontend]
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 [compose.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 [config-production.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.