Baeldung Pro – Ops – NPI EA (cat = Baeldung on Ops)
announcement - icon

Learn through the super-clean Baeldung Pro experience:

>> Membership and Baeldung Pro.

No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.

Partner – Orkes – NPI EA (cat=Kubernetes)
announcement - icon

Modern software architecture is often broken. Slow delivery leads to missed opportunities, innovation is stalled due to architectural complexities, and engineering resources are exceedingly expensive.

Orkes is the leading workflow orchestration platform built to enable teams to transform the way they develop, connect, and deploy applications, microservices, AI agents, and more.

With Orkes Conductor managed through Orkes Cloud, developers can focus on building mission critical applications without worrying about infrastructure maintenance to meet goals and, simply put, taking new products live faster and reducing total cost of ownership.

Try a 14-Day Free Trial of Orkes Conductor today.

1. Overview

Modern web applications often require the flexibility to adapt to different environments without altering the code. For many Vue.js developers, modifying settings like API keys or base URLs at runtime is crucial, as environment variables are typically baked into the build by default, limiting flexibility.

This tutorial explores passing environment variables to Vue applications at runtime. We’ll walk through a practical approach to dynamically injecting these variables and configuring the app to read them effectively.

2. Prerequisites

Before proceeding, we need to have a few key components in place:

With these in place, let’s get started.

3. Understanding Runtime Environment Variables in Vue.js

Vue.js, like many frontend frameworks, handles environment variables at build time. When an application is built, values from .env files or process.env are embedded directly into the application’s JavaScript bundle. This means that the values cannot be changed without rebuilding the app.

This behavior becomes a limitation when configurations need to change dynamically. For example, an API base URL or an API key might vary between environments (development, staging, production). If these variables are baked into the app during the build, each change requires a rebuild and redeployment, which can be inefficient and time-consuming.

Additionally, this approach lacks flexibility for multi-environment deployments. Imagine deploying the same app across multiple servers, each requiring different settings. The inability to adjust variables at runtime adds unnecessary complexity to the deployment process.

4. Building the Runtime Environment Setup

To enable dynamic configuration in a Vue.js app, we need a way to inject environment variables at runtime rather than embedding them during the build process. We can achieve this through a setup that generates, serves, and integrates runtime variables when the application starts.

This approach involves three main files — config.jsentrypoint.sh, and nginx.conf. Before we look at those in detail, let’s set up the starter project, which contains the foundational files and configurations needed for this implementation.

4.1. Cloning the Starter Project

First, let’s clone the starter project’s repository. It contains a basic Vue.js project — a currency converter app:

$ git clone https://github.com/Baeldung/ops-tutorials.git

Next, let’s navigate to the project directory:

$ cd ops-tutorials/docker-modules/passing-env-vars-to-vue-app-at-runtime/vue-env-vars-starter

Now, let’s install the dependencies using:

$ npm install

or

$ yarn install

We can now verify the local setup by running npm run dev and navigating to http://localhost:5173 in the browser to ensure everything is working.

Starter project

4.2. Creating the config.js File

The config.js file serves as the source of runtime variables. Instead of embedding configurations during the build process, this file is dynamically generated at runtime to reflect the current environment.

Let’s create the config.js file in the public folder of our project:

$ touch public/config.js

Next, we need to open the file in a text editor and add an empty object as its default content:

window.config = {};

This ensures the app can load successfully, even before the runtime variables are injected.

Next, let’s reference the config.js file in the index.html. Open the index.html and insert the following script before the closing body tag:

<script src="/config.js"></script>

This ensures the config.js file is loaded alongside the main application bundle whenever the app is accessed.

4.3. Creating and Configuring the entrypoint.sh File

The entrypoint.sh script is designed to dynamically inject environment variables into the config.js file at runtime. This flexibility allows the app to adapt to varying environments — like development, staging, and production — without rebuilding.

First, let’s create the entrypoint.sh file in the root of our project:

$ touch entrypoint.sh

Next, let’s open the entrypoint.sh and add some content:

#!/bin/sh

# Path to the runtime config.js file
CONFIG_FILE=/usr/share/nginx/html/config.js

# Replace placeholders in config.js with environment variables
echo "Generating runtime configuration in $CONFIG_FILE"
cat <<EOF > $CONFIG_FILE
window.config = {
    VUE_APP_API_URL: "${VUE_APP_API_URL:-undefined}"
};
EOF

# Start Nginx
nginx -g "daemon off;"

This script performs several critical functions. First, it defines the path to the config.js file, which is located in the directory where Nginx serves the app. The CONFIG_FILE variable specifies this path to ensure the script knows exactly where to inject the environment variables.

Next, the script uses the cat command to dynamically write the runtime variable values into the config.js file. The placeholder ${VUE_APP_API_URL:-undefined} fetches the value of the VUE_APP_API_URL environment variable, if set, or defaults to undefined if the variable is not set. This ensures that the config.js file always has a value, even in cases where the variable hasn’t been defined.

Finally, the script starts the Nginx server using the nginx -g “daemon off;” command. This keeps Nginx running in the foreground, allowing it to serve our app along with the dynamically generated config.js file.

4.4. Configuring nginx.conf

The nginx.conf file is another critical part of this setup. It configures Nginx to serve our Vue.js app along with the dynamically generated config.js file. Additionally, it ensures that client-side routing works correctly by forwarding unrecognized routes to the index.html file.

Let’s create the nginx.conf file in the root of our project:

$ touch nginx.conf

Next, let’s add the configuration:

worker_processes 1;

events {
    worker_connections 1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    keepalive_timeout 65;

    server {
        listen 80;

        server_name localhost;

        # Serve the main app
        location / {
            root /usr/share/nginx/html;
            index index.html;
            try_files $uri /index.html;
        }

        # Serve runtime environment variables as config.js
        location /config.js {
            root /usr/share/nginx/html;
            default_type application/javascript;
        }
    }
}

This configuration starts with basic directives to optimize Nginx for performance. The worker_processes and worker_connections directives configure the number of worker processes and connections, respectively, ensuring efficient handling of requests.

The server block configures the behavior for serving files. The first location block ensures that all requests are routed through the index.html file, which is essential for Vue.js client-side routing. It uses the try_files $uri /index.html; directive to forward any unmatched requests to index.html.

The second location block serves the config.js file, which contains runtime environment variables. This block ensures the file is served as a JavaScript file by setting the default_type to application/javascript. This lets our app load the file during runtime and use the environment variables it contains without requiring a rebuild.

In the next section, we’ll manually test our environment variable injection.

5. Manual Environment Variable Injection

Before we move on to containerizing our app, let’s test the runtime environment variable injection in a local setup.

5.1. Preparing the Environment

First, let’s set up the .env file in the project root directory with the environment variables:

$ touch .env

Next, we need to add our environment variable to the file:

VUE_APP_API_URL=https://v6.exchangerate-api.com/v6/API_KEY/latest/USD

This variable will be dynamically injected into our app during runtime.

5.2. Running the entrypoint.sh Script

Now, let’s use the entrypoint.sh script to generate the config.js file with the environment variable.

Before running the script, we need to temporarily update the CONFIG_FILE path in the entrypoint.sh file to point to ./public/config.js.

This adjustment is necessary because, when testing locally, the default path (/usr/share/nginx/html/config.js) is relevant only within the Docker container and does not exist in our local setup.

Let’s open the entrypoint.sh file and update the CONFIG_FILE variable:

CONFIG_FILE=./public/config.js

After making this change, let’s ensure the entrypoint.sh file has the correct permissions to execute:

$ chmod +x entrypoint.sh

Next, we need to export the environment variable to make it available:

$ export VUE_APP_API_URL=https://v6.exchangerate-api.com/v6/API_KEY/latest/USD

With the variable set, let’s run the script:

$ ./entrypoint.sh

The script dynamically generates the config.js file in the public folder. Once it completes, we can open the public/config.js file to verify its content. It should look like:

window.config = {
    VUE_APP_API_URL: "https://v6.exchangerate-api.com/v6/API_KEY/latest/USD"
};

With this, we can confirm that our script works perfectly. Now, let’s revert the CONFIG_FILE path to its original value (/usr/share/nginx/html/config.js) before moving on to containerizing the app.

6. Containerizing the App

With the runtime environment setup complete, let’s containerize our Vue.js app.

6.1. Creating the Dockerfile

The Dockerfile defines the instructions to build and package our Vue.js app into a container. We’ll use a multi-stage build process to separate the app’s build and runtime environments, minimizing the final image size.

First, let’s create the Dockerfile:

$ touch Dockerfile

Next, let’s add the code to it:

# Build Stage
FROM node:18-alpine as build-stage

# Install Python and make for build compatibility
RUN apk add --no-cache python3 make g++

# Set working directory
WORKDIR /app

# Copy package.json and package-lock.json
COPY package*.json ./

# Install dependencies
RUN npm install

# Copy application files
COPY . .

# Build the Vue.js app
RUN npm run build

# Production Stage
FROM nginx:stable-alpine as production-stage

# Set working directory in Nginx container
WORKDIR /usr/share/nginx/html

# Copy built files from the build stage
COPY --from=build-stage /app/dist /usr/share/nginx/html

# Copy custom Nginx configuration
COPY nginx.conf /etc/nginx/nginx.conf

# Add runtime configuration script
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

EXPOSE 80

CMD ["/entrypoint.sh"]

The multi-stage approach ensures that the final container image only includes the production-ready files, optimizing for size and performance.

6.2. Configuring docker-compose.yml

The docker-compose.yml file simplifies the process of managing and running the containerized app. It also provides a convenient way to pass environment variables into the container, making our runtime configuration seamless.

First, let’s create the docker-compose.yml file in the root directory of our Vue.js app:

$ touch docker-compose.yml

Next, let’s define our app service and configure environment variable handling:

version: "3.8"

services:
    vue-app:
        build:
            context: .
            dockerfile: Dockerfile
        ports:
            - "8080:80"
        env_file:
            - .env

This configuration builds the Docker image, maps port 8080 on the host to port 80 in the container, and loads environment variables from the .env file.

In this configuration, we specify the build context and the path to our Dockerfile. The ports section maps port 8080 on the host machine to port 80 inside the container, enabling us to access the app via http://localhost:8080.

The env_file directive ensures that the environment variables defined in our .env file are passed to the container at runtime.

With this setup, we’re ready to build and run our container.

7. Testing Runtime Variable Injection

With our docker-compose.yml file configured, let’s build and run our containerized app. This process will package our app into a Docker container and launch it with the runtime configurations we’ve set up.

First, let’s create a .env file in our project’s root directory:

$ touch .env

Then, we need to add a single line of code:

VUE_APP_API_URL=https://v6.exchangerate-api.com/v6/API_KEY/latest/USD

Be sure to replace API_KEY with the API key obtained from Exchange API.

Now, let’s build the Docker image:

$ docker-compose up --build

To verify that our runtime environment variables are working correctly, let’s check the /config.js endpoint. Let’s open its URL in the browser:

http://localhost:8080/config.js

This should display the dynamically injected environment variable, such as:

window.config = {
    VUE_APP_API_URL=https://v6.exchangerate-api.com/v6/API_KEY/latest/USD
};

If we need to update an environment variable, we can modify the .env file. For example, let’s change the VUE_APP_API_URL value in the .env file:

VUE_APP_API_URL=https://new-api.example.com

After updating the .env file, we restart the container to apply the changes:

$ docker-compose down
$ docker-compose up

Reloading the /config.js endpoint in the browser will confirm the updated value, reflecting the new configuration in real-time.

This step completes the process of containerizing and running our Vue.js app with dynamic runtime environment variables. In the next section, we’ll explore how to adapt this setup for production environments.

8. Adapting the Setup for Production

Now that we’ve successfully containerized and run our Vue.js app with runtime environment variables, let’s see how to adapt this setup for production environments. Typically, in production, our app will likely run on a platform like Kubernetes, AWS ECS, or Docker Swarm. Therefore, it’s essential to ensure that the runtime configuration process remains efficient and secure.

8.1. Managing Environment Variables in Production

There are two primary ways to manage environment variables securely and efficiently.

First, we can continue using a .env file in production. However, sensitive data like API keys should be managed with greater security. For this purpose, tools like Docker Secrets can encrypt and safely store these variables, making them inaccessible to unauthorized users.

Secondly, for Kubernetes deployments, ConfigMaps or Secrets are the standard methods to manage environment variables. ConfigMaps handle non-sensitive data like URLs, while Secrets are designed for sensitive data like API keys. For example:

apiVersion: v1
kind: ConfigMap
metadata:
  name: vue-config
data:
  VUE_APP_API_URL: "https://api.production.example.com"

These can then be mounted as environment variables in the pod.

8.2. Ensuring Seamless Updates

In production, there are a couple of ways we can update environment variables without disrupting the application, depending on our setup.

For Kubernetes, we can perform a rolling update to gradually replace pods with the updated environment variables.

Meanwhile, for Docker Compose or Swarm setups, we can use a blue-green deployment strategy or restart services with minimal impact.

8.3. Handling Caching for config.js

Aggressive caching policies can cause the app to load outdated versions of the config.js file in production. To address this issue, we can implement a cache-busting mechanism.

For instance, in the index.html file, append a timestamp-based query parameter when referencing config.js:

<script src="./config.js?v=<%= Date.now() %>"></script>

Also, we can modify the Nginx configuration to disable caching for config.js:

location /config.js {
    default_type application/javascript;
    add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
    return 200 "window.config = { VUE_APP_API_URL: '${VUE_APP_API_URL}' };";
}

8.4. Ensuring Security

We need to prioritize protecting sensitive data and preventing unauthorized access in production. Let’s consider a few key practices to help us with this:

  • Use tools like AWS Secrets Manager, HashiCorp Vault, or Kubernetes Secrets for sensitive data
  • If config.js contains sensitive data, restrict access by IP or authentication
  • Always serve the app and its configurations over HTTPS to prevent man-in-the-middle attacks

With these in place, our setup is ready for production, ensuring that the Vue.js app can dynamically adapt to different environments while maintaining reliability and security.

9. Conclusion

In this article, we demonstrated how to pass environment variables to a Vue.js app at runtime, effectively addressing the limitations of build-time configurations. Specifically, we implemented a practical setup that allowed the app to dynamically inject and access environment variables without rebuilding. Furthermore, we containerized the app with Docker and explored how to adapt the setup for production.

As a result, this approach ensures the app remains flexible and easy to manage, even in complex deployment environments. The complete code for this tutorial is available over on GitHub.