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?
答案 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一样:
runtime
就像您很少更改的基本图像。 polyfills
是您需要的那些没有包含在所需基本映像中的东西。 main
是您的实际应用,每个发行版都有很大的变化。 您可以使用前端应用程序执行与使用Docker应用程序相同的操作!
如何?
只需将发臭的/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,我觉得这并不简单。
基本上,我采用了类似的方法,并提供了以下解决方案:
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
Dockerfile
中标记为变量:
ARG AUTH_SERVER_URL
ARG GAME_SERVER_URL
ARG GUI_SERVER_URL
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