DEV Community

Eamonn Walsh
Eamonn Walsh

Posted on

React Application: Build Once, Deploy Anywhere Solution

In this blog post, we will explore a solution for the build once deploy many issue faced by projects working with web application libraries such as React.

In React environment-specific configuration is baked in at build time, meaning you most likely need to create individual build artifacts for dev, test, uat, live etc. This can turn into a significant time sink if the build process is even remotely lengthy.

The goal is to decouple environment-specific configuration from the build process, allowing the same build artifact to be deployed to multiple environments with different configurations.

Prerequisites

  • Basic knowledge of React and Vite
  • Familiarity with Docker and Kubernetes concepts

Steps

1. Placeholder-based Configuration

To make the application deployable to different environments, we need to remove all hard-coded references to environment variables and replace them with placeholders. During the build process, Vite will generate a deployable asset containing these placeholders instead of the actual environment variable values.

2. Move Environment Variables to Kubernetes

Store your environment-specific configuration in Kubernetes, preferably using a ConfigMap. This allows you to set the configuration on a per-environment basis.

3. Create a Shared Environment File

Create a single environment file, e.g., .env.all, that will be used for all builds. Set your variables to align with the placeholder names. For example:

VITE_VARIABLE_1=%VITE_VARIABLE_1%
VITE_VARIABLE_2=%VITE_VARIABLE_2%
VITE_VARIABLE_3=%VITE_VARIABLE_3%
Enter fullscreen mode Exit fullscreen mode

4. Create a Configuration Replacement Script

In your React application codebase, create a JavaScript script that can be executed inside your Docker container. This script will replace the placeholders with the actual environment variable values at deploy time, pulling the values from the ConfigMap in Kubernetes. It's recommended to create this script in a folder outside the /src directory, such as /static, to avoid it being included in the JavaScript asset during build time.

5. Copy the Replacement Script

Use Vite's static copy plugin to move the replacement script into the build output folder (by default, dist). Add the following configuration to your Vite project:

import { react } from '@vitejs/plugin-react';
import viteStaticCopy from 'vite-plugin-static-copy';

export default {
  // Other Vite configuration options...
  plugins: [
    react(),
    viteStaticCopy({
      targets: [
        {
          src: './static/replacePlaceHolders.js',
          dest: './static/replacePlaceHolders.js',
        },
      ],
    }),
  ],
};
Enter fullscreen mode Exit fullscreen mode

6. Run the Build Command

Run your build command as usual, but make sure to point to your single .env.all file using the mode flag. For example:

tsc -p tsconfig.build.json && vite build --mode all
Enter fullscreen mode Exit fullscreen mode

7. Verify the Build Output

Verify that the built asset contains the variable placeholders instead of the actual values. Also, ensure that the replacePlaceHolders.js script exists in the dist folder.

8. Update the Dockerfile

Update your Dockerfile to include Node.js as part of the image so that it can run the replacePlaceHolders.js script during the image preparation phase.

The majority of Docker images used for deploying React applications do not typically necessitate the presence of Node. However, we specifically require Node to be running in order to access and retrieve environment-specific configuration set in your ConfigMap via the process.env object in Node.

Add the following command to the Dockerfile:

CMD ["node", "./replacePlaceHolders.js"]
Enter fullscreen mode Exit fullscreen mode

This command will execute the JavaScript script in Node.js, providing access to the variables set in the ConfigMap. You can replace placeholders in files using the following code snippet:

const fs = require('fs');
const filePath = '<path-to-file>';

let fileContent = fs.readFileSync(filePath, 'utf-8');
        let fileContent = await fs.readFile(<path-to-file>, 'utf-8');

        fileContent = fileContent.replace('%VITE_VARIABLE_1%', process.env.VITE_VARIABLE_1);
Enter fullscreen mode Exit fullscreen mode

Conclusion

There are a few other approaches to do this but they all need to add what I would consider as configuration logic to the app itself and don't run at deploy time. There is no perfect solution

Top comments (0)