How to pass environment variables to a frontend web application?
Asked Answered
M

6

35

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?

Marasmus answered 3/2, 2018 at 9:22 Comment(1)
This was cross-posted to the Docker forum on Reddit.Mccowan
E
29

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.

Expound answered 18/3, 2018 at 16:0 Comment(4)
Great job @daniel-calderaBlackshear
Great answer Daniel thank you. Interchangeably another solution of entrypoint shell script could be : ``` shell #!/bin/bash output="$(printenv REST_API_URL)" sed -i 's|REST_API_URL_REPLACE|'$output'|g' /usr/share/nginx/html/main*bundle.js exec "$@"```Vibrator
What about caching? If you change an environment variable you may like the client to reload the bundle file. Usually, you add the hash of the file to the filename (for example main.ab4c6c83e4fa9c.js). Since, you change the file by inserting the environment variables, you should update the hash in the filename afterwards.Soffit
You also could use envsubst to do this instead of sed. gnu.org/software/gettext/manual/html_node/…Monoclinous
M
12

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!

Macropterous answered 5/12, 2018 at 3:39 Comment(2)
Thanks, this is really nice! Essentially piggybacking on the fact that index.html cannot be immutable either way (can't have a hash in the filename). No exceptions in caching config :)Criseldacrisey
I really like this approach in theory and have referenced the principles outlined in your site often. However, I'm struggling with the mechanism to achieve burning the environment variables into index.html in a Docker image. I can do this at build time using Webpack, but need a way to modify index.html dynamically inside the container with the environment variables that are passed to Docker via, e.g. , docker-compose.yml.Cursed
D
5

I had a similar problem for a static HTML file and here is what I wanted to solve:

  • the number of env vars can scale
  • the env vars can be set at launch time and not at build time
  • not worrying about maintaining the format of variable replacement so that it won't clash with other text

I tried other answers but it seems like they didn't fit the above. So this is what I ended up with using envsubst

Dockerfile

FROM nginx:alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY . /usr/share/nginx/html
EXPOSE 80

# awkwardly replace env variables
COPY ./replaceEnvVars.sh /
RUN chmod +x replaceEnvVars.sh
ENTRYPOINT ["./replaceEnvVars.sh"]
CMD ["nginx", "-g", "daemon off;"]

replaceEnvVars.sh

#!/bin/sh

envsubst < /usr/share/nginx/html/index.tmpl.html > /usr/share/nginx/html/index.html && nginx -g 'daemon off;' || cat /usr/share/nginx/html/index.html

index.tmpl.html

<html>
 ...
 <script>
 gtag('config', '${GA_CODE}');
 </script>
 ...
 <a href="${BASE_URL}/login" class="btn btn-primary btn-lg btn-block">Login</a>
</html>

docker-compose.yml

version: '3'
services:
  landing:
    build: .
    ...
    environment:
      - BASE_URL=https://dev.example.com
      - GA_CODE=UA-12345678-9
    ...
Dallas answered 15/8, 2019 at 2:30 Comment(0)
B
3

I was struggling with the same problem but also needed to pass the configuration values from docker-compose level down to Angular, which I didn't find that straightforward.

Basically, I took a similar approach and came with the following solution:

  1. I passed the desired values from docker-compose.yml to Dockerfile using the compose ARGS. So in docker-compose.yml I have:

magicsword.core.web: build: args: - AUTH_SERVER_URL=http://${EXTERNAL_DNS_NAME_OR_IP}:55888/ - GAME_SERVER_URL=http://${EXTERNAL_DNS_NAME_OR_IP}:55889/ - GUI_SERVER_URL=http://${EXTERNAL_DNS_NAME_OR_IP}:55890/ # =self

  1. These now have to be marked in the Dockerfile as variables:

ARG AUTH_SERVER_URL ARG GAME_SERVER_URL ARG GUI_SERVER_URL

  1. Since, during the build process these become normal environment variables, the last step is to do the actual substitution in the target file, e.g. using some magical one-liner. I did as follows (just a pet project so doesn't need to be optimal):

RUN apt-get update && apt-get install -y gettext RUN envsubst < ./src/environments/environment.ts > ./src/environments/environment.ts.tmp && mv ./src/environments/environment.ts.tmp ./src/environments/environment.ts

The environment.ts before the substitution, for reference:

export const environment = { production: true, GAME_SERVER_URL: "$GAME_SERVER_URL", GUI_SERVER_URL: "$GUI_SERVER_URL", AUTH_SERVER_URL: "$AUTH_SERVER_URL" };

Voila. Hope this helps someone :)

Bice answered 10/9, 2018 at 20:10 Comment(1)
The variable substitution only occurs at image build time since you're using RUN, correct? How would you accomplish this at run time using a single image to support multiple environment variable sets?Cursed
F
2

My solution: at run time use docker volumes to mount a specific js config file as the env.js.

I have a docker compose file for dev and prod.

I have dev.env.js and prod.env.js.

My html file references env.js.

In docker-compose.yml I volume mount either env file as env.js.

E.g. my dev compose:

web:
    image: nginx
    ports:
      - 80:80
    volumes:
      - ../frontend:/usr/share/nginx/html
      - ../frontend/dev.env.js:/usr/share/nginx/html/env.js

And my prod compose:

web:
    image: nginx
    ports:
      - 80:80
    volumes:
      - ../frontend:/usr/share/nginx/html
      - ../frontend/prod.env.js:/usr/share/nginx/html/env.js
Farrow answered 2/12, 2018 at 13:46 Comment(1)
Thanks, It's useful :)Raki
A
0

I just encountered this problem with Docker and the use of the DotEnv webpack plugin. I wanted to use the same image for multiple environments so I had to delay the webpacking process until the image was ran with environment specific ENVs.

I did this by creating start_app.sh and have it run in the entrypoint:

start_app.sh:

#!/bin/bash
npm run build
npm start

exec "$@"

end of dockerfile:

# Use ENV on docker run
RUN chmod +x start-app.sh
ENTRYPOINT ["./start-app.sh"]

docker command:

docker run --env REST_API_URL=testURL  -p 9900:9900 app:0.0.0
Antiknock answered 24/3, 2023 at 16:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.