How to pass environment variables to a frontend web application?

时间:2018-02-03 09:22:53

标签: docker environment-variables dockerfile 12factor

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?

5 个答案:

答案 0 :(得分:17)

我解决这个问题的方法如下:

1.使用唯一且可识别的字符串设置enviroment.prod.ts中的值:

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

2.创建一个entryPoint.sh,每次你完成容器的docker运行时都会执行这个entryPoint。

#!/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 "$@"

如您所见,此入口点获取'REST_API_URL_REPLACE'参数并在main * bundle.js文件中替换它(在本例中)以获取var的值。

3.在CMD之前的dockerfile中添加entrypoint.sh(它需要执行权限):

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.使用env或使用docker-compose(斜杠必须转义)来恢复图像:

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

可能存在一个更好的解决方案,不需要在缩小文件中使用常规表达式,但这样可以正常工作。

答案 1 :(得分:4)

将环境变量放入index.html

相信我,我知道你来自哪里!将特定于环境的变量烘烤到Angular应用程序的构建阶段中,与我所学到的有关可移植性和关注点分离的一切情况背道而驰。

但是等等!仔细看一下常见的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>

这就是所有配置!

就像您用来维护Docker应用程序的docker-compose.yml一样:

  • 版本化的不可变资产
  • 环境变量
  • 应用程序绑定
  • 环境元数据
  • 即使不同的捆绑包也感觉像是docker image sorta的图层,不是吗?
    • runtime就像您很少更改的基本图像。
    • polyfills是您需要的那些没有包含在所需基本映像中的东西。
    • main是您的实际应用,每个发行版都有很大的变化。

您可以使用前端应用程序执行与使用Docker应用程序相同的操作!

  1. 构建,版本化和发布不可变资产(js包/ Docker映像)
  2. 将部署清单发布到登台(index.html / docker-compose.yml)
  3. 分期测试
  4. 将部署清单发布到生产中。引用您刚测试的相同资产!即刻!原子地!

如何?

只需将发臭的/src/environments/environment.prod.ts指向window对象。

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

,然后使用环境变量它们在哪里!

将脚本添加到index.html中
<script>
  env = { api: 'https://api.myapp.com' }
</script>

我对这种方法感到非常强烈,因此我专门创建了一个网站:https://immutablewebapps.org。我想您会发现还有很多其他好处!

~~~

现在,我已经使用两个AWS S3存储桶成功完成了此操作:一个用于版本化的静态资产,一个仅用于index.html(这使路由变得非常简单:为每个路径提供index.html)。我还没有像您建议的那样运行容器。如果要使用容器,则希望在构建和发布新资产以及发布新的index.html之间进行明确区分。也许我会从带有容器环境变量的模板中即时渲染index.html

如果您选择这种方法,我很想知道结果如何!

答案 2 :(得分:2)

对于静态HTML文件,我也遇到类似的问题,这是我要解决的问题:

  • 环境变量的数量可以扩展
  • 可以在启动时设置环境变量,而不能在构建时设置
  • 不用担心保持变量替换的格式,以免与其他文本冲突

我尝试了其他答案,但似乎它们不符合以上要求。这就是我最终使用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
    ...

答案 3 :(得分:1)

我在同样的问题上苦苦挣扎,但还需要将docker-compose级别的配置值传递给Angular,我觉得这并不简单。

基本上,我采用了类似的方法,并提供了以下解决方案:

  1. 我使用compose ARGS将所需的值从docker-compose.yml传递到Dockerfile。 所以在docker-compose.yml中,我有:

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. 这些现在必须在Dockerfile中标记为变量:

ARG AUTH_SERVER_URL ARG GAME_SERVER_URL ARG GUI_SERVER_URL

  1. 由于在构建过程中它们成为正常的环境变量,因此最后一步是在目标文件中进行实际替换,例如使用一些神奇的单线。我做了如下(只是一个宠物项目,所以不需要是最优的):

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

替换前的environment.ts,以供参考:

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

Voila。希望这对某人有帮助:)

答案 4 :(得分:1)

我的解决方案:在运行时使用docker卷将特定的js配置文件挂载为env.js。

我有一个用于dev和prod的docker compose文件。

我有dev.env.js和prod.env.js。

我的html文件引用了env.js。

在docker-compose.yml中,我将两个env文件都作为env.js挂载。

例如我的开发人员撰写:

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

我的产品组成:

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