Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to pass environment variables to a frontend web application?

I am trying to containerize a frontend web application and I am having troubles to figure out how to pass environment variables. The application is a Angular application, so it is 100% client-side.

In a typical backend service, passing environment variables is easy, as everything is running on the same host, so the environment variables can be easily picked by the backend service. However, in a frontend application, this is different: the application is running in the browser of the client.

I want to configure my application via environment variables, as this makes deployment much easier. All configuration can be done in docker-compose.yml and there is no need to maintain several images, one for every possible environment. There is just one single immutable image. This follows the 12-factor application philosophy, as can be found on https://12factor.net/config.

I am building my application image as following:

FROM node:alpine as builder COPY package.json ./ RUN npm i && mkdir /app && cp -R ./node_modules ./app WORKDIR /app COPY . . RUN $(npm bin)/ng build  FROM nginx:alpine COPY nginx/default.conf /etc/nginx/conf.d/ RUN rm -rf /usr/share/nginx/html/* COPY --from=builder /app/dist /usr/share/nginx/html CMD ["nginx", "-g", "daemon off;"] 

In app/config.ts, I have:

export const config = {     REST_API_URL: 'http://default-url-to-my-backend-rest-api' }; 

Ideally, I want to do something like this in my docker-compose.yml:

backend:   image: ... frontend:   image: my-frontend-app   environment:     - REST_API_URL=http://backend:8080/api 

So I believe I should alter this app/config.ts to replace REST_API_URL with the environment variable. As I prefer an immutable Docker image (so I do not want to do this replace during the build), I am quite puzzled how to progress here. I believe I should support to alter the app/config.ts at runtime before the nginx proxy is started. However, the fact that this file is minified and webpack-bundled, makes this more diffucult.

Any ideas how to tackle this?

like image 430
dockernodejs Avatar asked Feb 03 '18 09:02

dockernodejs


People also ask

How do you pass an environment variable?

Environment variables can be used to pass configuration to an application when it is run. This is done by adding the definition of the environment variable to the deployment configuration for the application. To add a new environment variable use the oc set env command.

Can JavaScript access environment variables?

All values assigned to environment variables are represented as strings when they are accessed in JavaScript code. That means a variable assigned as MY_VARIABLE=true will have the value of true be the string 'true' in JavaScript.

Can a browser access environment variables?

By default environment variables are only available in the Node.js environment, meaning they won't be exposed to the browser. This loads process.env.NEXT_PUBLIC_ANALYTICS_ID into the Node.js environment automatically, allowing you to use it anywhere in your code.


2 Answers

The way that I resolved this is as follows:

1.Set the value in the enviroment.prod.ts with a unique and identificable String:

export const environment = {   production: true,   REST_API_URL: 'REST_API_URL_REPLACE', }; 

2.Create a entryPoint.sh, this entryPoint will be executed every time that you done a docker run of the container.

#!/bin/bash set -xe : "${REST_API_URL_REPLACE?Need an api url}"  sed -i "s/REST_API_URL_REPLACE/$REST_API_URL_REPLACE/g" /usr/share/nginx/html/main*bundle.js  exec "$@" 

As you can see, this entrypoint get the 'REST_API_URL_REPLACE' argument and replace it (in this case) in the main*bundle.js file for the value of the var.

3.Add the entrypoint.sh in the dockerfile before the CMD (it need execution permissions):

FROM node:alpine as builder COPY package.json ./ RUN npm i && mkdir /app && cp -R ./node_modules ./app WORKDIR /app COPY . . RUN $(npm bin)/ng build --prod  FROM nginx:alpine COPY nginx/default.conf /etc/nginx/conf.d/ RUN rm -rf /usr/share/nginx/html/* COPY --from=builder /app/dist /usr/share/nginx/html  # Copy the EntryPoint COPY ./entryPoint.sh / RUN chmod +x entryPoint.sh  ENTRYPOINT ["/entryPoint.sh"] CMD ["nginx", "-g", "daemon off;"] 

4.Lauch the image with the env or use docker-compose (the slash must be escaped):

docker run -e REST_API_URL_REPLACE='http:\/\/backend:8080\/api'-p 80:80 image:tag 

Probably exists a better solution that not need to use a regular expresion in the minified file, but this works fine.

like image 133
Daniel Caldera Avatar answered Sep 17 '22 16:09

Daniel Caldera


Put your environment variables in the index.html!!

Trust me, I know where you are coming from! Baking environment-specific variables into the build phase of my Angular app goes against everything I have learned about portability and separation of concerns.

But wait! Take a close look at a common Angular index.html:

<!doctype html> <html lang="en"> <head>   <meta charset="utf-8">   <title>mysite</title>   <base href="/">   <meta name="viewport" content="width=device-width, initial-scale=1">   <link rel="icon" type="image/x-icon" href="favicon.ico">   <link rel="stylesheet" href="https://assets.mysite.com/styles.3ff695c00d717f2d2a11.css">   <script>   env = {     api: 'https://api.mysite.com/'   }   </script> </head> <body>   <app-root></app-root>   <script type="text/javascript" src="https://assets.mysite.com/runtime.ec2944dd8b20ec099bf3.js"></script>   <script type="text/javascript" src="https://assets.mysite.com/polyfills.20ab2d163684112c2aba.js"></script>   <script type="text/javascript" src="https://assets.mysite.com/main.b55345327c564b0c305e.js"></script> </body> </html> 

This is all configuration!!!

It is just like the docker-compose.yml that you are using to maintain your Docker apps:

  • versioned immutable assets
  • environment variables
  • application binding
  • environment meta-data
  • even the different bundles feel like layers of an docker image sorta, don't they?
    • runtime is like your base image that you rarely change.
    • polyfills are those things you need that didn't come included in the base image that you need.
    • main is your actual app that pretty much changes every release.

You can do the same thing with your frontend app that you do with your Docker app!

  1. Build, version, and publish immutable assets (js bundles / Docker image)
  2. Publish a deployment manifest to staging (index.html / docker-compose.yml)
  3. Test in staging
  4. Publish a deployment manifest to production.. referencing the same assets you just tested! Instantly! Atomically!

How??

Just point the stinking /src/environments/environment.prod.ts at the window object.

export const environment = (window as any).env; // or be a rebel and just use window.env directly in your components 

and add a script to your index.html with the environment variable WHERE THEY BELONG!:

<script>   env = { api: 'https://api.myapp.com' } </script> 

I feel so strongly about this approach I created a website dedicated to it: https://immutablewebapps.org. I think you will find there are a lot of other benefits!

~~~

Now, I have done this successfully using two AWS S3 Buckets: one for the versioned static assets and one for just the index.html (it makes routing super simple: serve index.html for every path). I haven't done it running containers like you are proposing. If I were to use containers, I would want to make a clean separation between the building and publishing new assets, and releasing of a new index.html. Maybe I would render index.html on-the-fly from a template with the container's environment variables.

If you choose this approach, I'd love to know how it turns out!

like image 41
Gene C Avatar answered Sep 20 '22 16:09

Gene C