Environment variables can only be used in Vue.js applications at build time and the variables are hardcoded in javascript files during build. When you build docker image for the Vue.js application, you use the same image for test and production environments. But the Vue.js application config values can be different in test and production environments. For example: api url configuration value. Since the variables are different in those environments you must pass the configuration values for test and production environments at docker run:

docker run -d -p 8080:80 -e VARIABLE1=var1value -e VARIABLE2=var2value my-image

But the Vue.js configuration variables are hardcoded to javascript files in the docker image after build. We must find a way to substitute the values in javascript files when a container is started. I’ll show you how to do that.

Vue.js application

The Vue.js application uses Vite env variables. We have to variables: VITE_VARIABLE1 and VITE_VARIABLE2.

We are using Vite.js build tool and şn Vite, environmental variables need to be prefixed with VITE_ as in VITE_*variable-name.

.env Files

In .env.development file we can hardcode the variables:

VITE_VARIABLE1=var1valuedev
VITE_VARIABLE2=var2valuedev

In .env.production file we give variable names in __...__ format.

VITE_VARIABLE1=__VARIABLE1__
VITE_VARIABLE2=__VARIABLE2__

config.ts

We create a separate config.ts file and read all all variables with import.meta.env.

export interface Config {
    variable1: string
    variable2: string
}

export const config: Config = {
    variable1: import.meta.env.VITE_VARIABLE1,
    variable2: import.meta.env.VITE_VARIABLE2
}

Use config variables in application

We import the config from ‘../config’ and use in vue template:

<script setup lang="ts">
import { config } from '../config'
</script>

<template>
  <div class="card">  
    <p>var1 = {{ config.variable1 }} </p>
    <p>var2 = {{ config.variable2 }} </p>
  </div>
</template>

vite.config.ts

In vite.config.ts we create a separate output bundle for config.js using manualChunks. Because we will only substitute the variables in config.js file. We are doing this because we don’t want to accidentally change the values that might have the same text as our variable names in other javascript files.

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  build: {
    rollupOptions: {
      output: {
        // Define the main output bundle (e.g., for your application)
        entryFileNames: '[name]-[hash].js',
        // Define the directory where the main bundle should be output
        dir: 'dist',
      },
      // Create a separate output bundle for your specific file
      manualChunks(id) {
        if (id.endsWith('src/config.ts')) {
          return 'config' // This will create a separate bundle for config-[hash].js
        }
      }
    }
  }
})

Using manualChunks we create a separate bundle for src/config.ts. After run vite build, a separate bundle for config-[hash].js file will be created. For example: config-4f3cd5bb.js.

Substitution of variables after vite build

__VARIABLE1__ and __VARIABLE2__ comes from .env.production file.

config-4f3cd5bb.js file:

const _={variable1:"__VARIABLE1__",variable2:"__VARIABLE2__"};export{_ as c};

After we serve the application with npm run dev. The page looks like this: Dev Page

DockerFile

In docker file we run entrypoint.sh as ENTRYPOINT. In entrypoint.sh we substitute variables and run nginx.

We are substituting variables in ENTRYPOINT in order to read the environment variables passed at docker run.

If we have used RUN /entrypoint.sh instead of ENTRYPOINT ["/entrypoint.sh"] we wouldn’t be able to environment variables at docker run.

Because RUN is used during image build time to execute commands and create image layers, while ENTRYPOINT is used to define the container’s main process or command at runtime.

# Build the Vue app
FROM node:18-alpine as build-stage
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY ./ .
RUN npm run build

# Copy the built app in an NGINX container
FROM nginx as production-stage
RUN mkdir /app
COPY --from=build-stage /app/dist /app
COPY nginx.conf /etc/nginx/nginx.conf

# Copy entrypoint.sh
COPY ./entrypoint.sh /entrypoint.sh
# Make entrypoint.sh executable
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

entrypoint.sh file

In entrypoint.sh we substitute variables and run nginx.

In keys array we store our variables.

We iterate the keys and get environment variable values with value=$(eval echo "\$$key").

And then replace the value using sed command. sed command replaces __VARIABLE1__ and __VARIABLE2__ values with the environment variables in docker run: docker run -d -p 8080:80 -e VARIABLE1=var1valueprod -e VARIABLE2=var2valueprod my-image.

#!/bin/bash

ROOT_DIR=/app

keys=("VARIABLE1" "VARIABLE2")

# Replace env vars in files served by NGINX
for file in $ROOT_DIR/assets/config-*.js*;
do
  echo "Processing $file ...";
  for key in "${keys[@]}"
  do
    # Get environment variable
    value=$(eval echo "\$$key")
    echo "replace $key by $value"

    # replace __[variable_name]__ value with environment variable
    sed -i 's|__'"$key"'__|'"$value"'|g' $file
  done
done

nginx -g 'daemon off;'

Build docker image and run docker container

Build image and run docker image:

docker build . -t vue-environment-variables-docker
docker run -d -p 8080:80 -e VARIABLE1=var1valueprod -e VARIABLE2=var2valueprod vue-environment-variables-docker

The final config-4f3cd5bb.js file:

const _={variable1:"var1valueprod",variable2:"var2valueprod"};export{_ as c};

http://localhost:8080/ page looks like this: Prod Page

Thanks for reading.

Happy coding!