【问题标题】:Dynamic deployment Angular. One docker image, multiple deployment动态部署 Angular。一个 docker 镜像,多个部署
【发布时间】:2019-02-01 11:12:00
【问题描述】:

我需要找到一种动态部署 Angular 应用程序的方法,以便同一个 docker 映像允许我进行多个部署(开发、登台、生产),其中访问 URL 被修改。

我正在使用 Angular-cli 6.4.1 在 Angular 7 中制作应用程序。 对于部署,我正在创建一个 docker 多级容器,在其中构建映像并使用 nginx 对其进行配置。

问题在于,对于部署,我们使用私有注册表,在该注册表中我们使用处理重定向的代理,因此我的应用程序将按照以下方案进行部署:{SERVER_HOST} : {PORT} / {SERVER_LOCATION}

当我尝试访问该 URL 时,仅加载 index.html,因为其余资源与基本路径“/”相关联。

Angular 在构建中提供了一个参数(--base-href),允许修改整个应用程序的路径,但是它对我没有帮助,因为我需要相同的 Docker 映像来允许我执行不同的部署,以便{SERVER_LOCATION} 参数并不总是相同的。

我也尝试在运行时读取环境变量来修改基本标记的 href 属性,但是很难执行位于索引旁边未加载的文件中的代码。

作为一种解决方法,我决定在索引中创建一个函数,该函数执行一个 ajax 请求,该请求收集配置参数以加载其余资源,但我不喜欢它,因为它破坏了 Angular 的操作。

<-- This is working fine, but is not dynamic: -->
npm run build -- --prod --base-href https://myHost.net:8080/path/app/ --configuration=$configuration


<-- And this is working but is not Angular friendly -->

/** assets/data/appConfig.json */
{
    "SERVER_HOST": "https://myHost.net:8080",
    "SERVER_PATH": "/path/app/"
}

/** index.html */
<script>
  (function() {
    if (window.onerror) {
      loadConfig();
    }

    function loadConfig() {
      var xhttp = new XMLHttpRequest();
      var url = 'assets/data/appConfig.json';
      xhttp.onreadystatechange = function() {
        if (this.readyState == 4 && this.status == 200) {
          var data = JSON.parse(this.response);
          window.appBaseHref = data.SERVER_PATH;
          document.getElementById("base").href = data.SERVER_PATH;
        }
      };
      xhttp.open('GET', url);
      xhttp.send();
    }
  })()
</script>

有人告诉我有一个跳转代理的选项,但我找不到方法,我不知道如何配置它。我认为也许可以在 nginx 配置文件中进行某种调整,以便应用程序从提供的 URL 中“读取”,并且应用程序的基本 href 可以始终保持为“/”。

目前我的 nginx.conf 文件是:

server {
    listen 80;
    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
        try_files $uri $uri/ /index.html =404;
    }

    location /documentation {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
        try_files $uri $uri/ /documentation/index.html =404;
    }
}

位置路径“/”是我的 Angular 应用程序,位置路径“/documentation”是项目文档的保留路径。

欢迎任何形式的帮助。

这几天我一直在尝试几件事,但事实是我对如何继续感到很困惑,因为似乎没有任何效果。

我已经将 Nginx 配置为添加 env $uri+$basepath 属性,但我意识到关系正好相反。我需要我的 index.html 来向代理路由而不是路径“/”发出请求,因为因此,请求甚至不会进入日志(代理不会重定向我,因为它没有我的前缀应用程序)

我给你举个例子: 我的应用托管在:myHost:8080/myapp/front 当我输入这个 url(myHost:8080/myapp/front) 时,应用程序会加载索引,但相关的资源(样式、运行时、polyfills、脚本和 main)没有加载,因为它们将请求发送到:myHost:8080/ {resourcePath} 而不是 myHost:8080/myapp/front/{resourcePath}

此时我不知道该去哪里。我将能够得到我想要放置标志 --base-href /myapp/front 的结果,但我希望这条路线是动态的,并且取决于我的 VIRTUAL_HOST 系统的环境变量,当然我不知道在构建应用程序时

接下来我粘贴我的 dockerfile 和 nginx 配置文件。

Dockerfile(注释行是没有按预期方式工作的提案)。

### STAGE 0: Based on Node.js, to build and compile Angular ###
FROM node:alpine as node

# Create app directory
WORKDIR /app

# Copy the dependencies to install once and let Docker use the cache for the next builds
COPY package*.json /app/

# Install all dependencies
RUN npm install

# Copy all the project into the image
COPY ./ /app/

# Argument to build the image according to the environment
ARG configuration=production

# Compiles our project
RUN npm run build -- --prod --configuration=$configuration

### STAGE 1: Based on Nginx, to have only the compiled app, ready for production with Nginx ###
FROM nginx:1.13.3-alpine

## Remove default nginx website
RUN rm -rf /usr/share/nginx/html/*


## From 'builder' stage copy over the artifacts in dist folder to default nginx public folder
COPY --from=node /app/dist/dsa-frontend /usr/share/nginx/html

# Add directive
# COPY nginx-custom.conf.template /etc/nginx/conf.d/default.conf.template

COPY nginx-custom.conf /etc/nginx/conf.d/default.conf

# CMD /bin/bash -c "envsubst '\$VIRTUAL_SERVICE_LOCATION' < nginx-custom.conf > /etc/nginx/conf.d/default.conf"

CMD ["nginx", "-g", "daemon off;"]

nginx-custom.conf

server {
    listen 80;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
        try_files $uri $uri/ /myapp/front$uri /myapp/front$uri/;
        # try_files $uri $uri/ $uri$VIRTUAL_SERVICE_LOCATION $uri$VIRTUAL_SERVICE_LOCATION/ /index.html;
    }

    location /documentation {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
        try_files $uri $uri/ /documentation/index.html =404;
    }
}

但这些似乎都不起作用。

最后,我们决定从代理中删除开发并打开一个新端口来部署此应用程序,因此根路径现在是“/”。

现在一切正常。

【问题讨论】:

  • 你不能在 nginx conf 中处理它吗?请简化问题。
  • 我刚刚编辑了这个问题。谢谢你的建议。
  • 正如你所指出的。您可以通过环境变量动态提供应用程序的上下文根,并将其设置为 nginx env $uri+$basepath,其中 $basepath 将根据部署进行填充。
  • 另外别忘了,部署镜像时可以传递 nginx 环境变量。
  • 使用 confd 模板配合 nginx 配置。 confd

标签: angular docker nginx deployment


【解决方案1】:

我想我设法获得了全面的解决方案(尽管可能相当脆弱 - 随意推荐改进!)完全相同的问题的解决方案:为多个环境重用一个 Angular/NGINX Docker 映像(因此多个基本路线)。

另外我想解决访问外部 API 的问题,所以应用程序请求的所有内容都将通过容器中的 NGINX,所以你不必考虑CORS。在我的示例中,我有两件事:

  • 图像服务器端点
  • Socket.IO (WebSocket) 端点

客户端路由按预期工作,ROOT_PATH 对应于/

我仍然做了尝试(但我会):

  • HTTPS(可能不会因此发生太大变化)

最终结果如下所示:


在本地构建和运行容器(一切就绪后)

ROOT_PATH 是您要查找的参数。

由于NGINX proxy_pass,需要DNS_IP,您可能想要substitute Google DNS with something else

docker build . -t fancy-web-app && \
docker run -it --rm \
    --name fancy-web-app-configured \
    --publish target=80,published=80 \
    --env ROOT_PATH='staging' \
    --env WEBSOCKET_PATH='socket.io' \
    --env IMAGE_PATH='image' \
    --env API_URL='http://api.server.example.com/api/v1/' \
    --env DNS_IP='8.8.8.8 8.8.4.4' \
    fancy-web-app

为了清楚起见,我将列出您需要的所有内容:

NGNIX

  1. nginx.conf

    在容器启动期间修改,否则无法使用,因为它引用了环境变量。

Docker

  1. docker-entrypoint.sh magic 发生的地方。 * * *

  2. Dockerfile

    Multistage,将docker-entrypoint.sh 作为运行时阶段的默认entrypoint

角度

  1. index.html

    默认开发配置,在容器启动时修改,变成“运行时配置”。

  2. app.conf.ts

    配置允许您抽象出全局变量中的配置,而不会将这种丑陋进一步带入您的服务

  3. app.module.ts

    Angular 依赖注入允许您在任何服务中引用配置

  4. some-angular.service.ts

    这是您使用注入配置的地方。

角度加分

  1. proxy.conf.json

    用于在没有容器的本地主机上工作(相当于 nginx.conf 用于开发,将被 WebPack/ng serve 使用)

  2. angular.json

    在这里你可以指定一次代理配置,默认情况下到处都有。


如果有兴趣,我可以设置 GitHub 存储库,其中包含所有需要的部分(请评论/评分,以便我看到有需要)。


NGINX

1. nginx.conf

server {
  listen 80;

  # Serve static files (HTML, CSS, JS)
  location /${ROOT_PATH} {
    # Using `alias` insead of `root` directive, so ${ROOT_PATH}
    # will be discarded and static file will be fetched straight
    # from the specified folder
    alias /usr/share/nginx/html;
    try_files $uri $uri/ /index.html =404;
  }

  # Re-route calls to external APIs
  location  ~ ^/${ROOT_PATH}/(${WEBSOCKET_PATH}|${IMAGE_PATH})/(.*) {
    resolver ${DNS_IP};
    proxy_pass ${API_URL}/$1/$2$is_args$args;
    proxy_http_version 1.1;
    proxy_cache_bypass $http_upgrade;
    proxy_set_header Host $host;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection 'upgrade';
  }

}

Docker

2。 docker-entrypoint.sh

#!/usr/bin/env sh
set -eu

# Inject environment variables into NGINX configuration
# List all variables to be substituted to avoid clashing with
# NGINX own variables: https://serverfault.com/questions/577370
envsubst \
    '${API_URL} \
    ${ROOT_PATH} \
    ${WEBSOCKET_PATH} \
    ${IMAGE_PATH} \
    ${DNS_IP}' \
    < /etc/nginx/conf.d/default.conf.template \
    > /etc/nginx/conf.d/default.conf
cat /etc/nginx/conf.d/default.conf

# Set correct HTML base tag, so static resources are fetched
# from the right path instead of the root path.
# NOTE: Trailing and leading slashes in base href are important!
# Using `~` separator to avoid problems with forward slashes
sed --in-place \
  's~<base href="/">~<base href="/'$ROOT_PATH'/">~' \
  /usr/share/nginx/html/index.html

# Set WebSocket API endpoint
# Using `~` separator to avoid problems with forward slashes
sed --in-place \
  "s~webSocketPath.*,~webSocketPath: \`/$ROOT_PATH/$WEBSOCKET_PATH\`,~" \
  /usr/share/nginx/html/index.html

# Set image API endpoint
# Using `~` separator to avoid problems with forward slashes
sed --in-place \
  's~imageBaseUrl.*~imageBaseUrl: `${window.location}'$IMAGE_PATH'`~' \
  /usr/share/nginx/html/index.html

cat /usr/share/nginx/html/index.html

exec "$@"

3. Dockerfile

# Produce static files
FROM node:10.15.3-alpine
WORKDIR /app
COPY ./package.json ./package.json
COPY ./package-lock.json ./package-lock.json
RUN npm set progress=false && \
    npm install --silent
COPY . /app
RUN npm run ng build -- --prod --output-path=dist

# Use NGINX to serve static files and re-route requests
FROM nginx:1.15.10-alpine
RUN rm -rf /usr/share/nginx/html/*
COPY --from=0 /app/dist/ /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf.template
COPY docker-entrypoint.sh /
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD [ "nginx", "-g", "daemon off;" ]
EXPOSE 80

角度

4. index.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Fancy web app</title>
  <base href="/">
  <script>
    // These default values make sense when you `npm start`
    // They will be substituted during container startup
    // Using global variable on the window object here.
    // Is there a better way?
    window['app-config'] = {
      webSocketUrl: `${window.location.host}`,
      webSocketPath: `/socket.io`,
      imageBaseUrl: `${window.location}image`
    };
    console.log(window['app-config']);
  </script>

  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
  <pdd-root></pdd-root>
</body>
</html>

5. app.conf.ts

import { Injectable } from '@angular/core';

@Injectable()
export class Configuration {
  webSocketUrl: string;
  webSocketPath: string;
  imageBaseUrl: string;
}

// Reading configuration out, so we have it Angular world 
export const AppConfiguration: Configuration = window['app-config'];

6. app.module.ts(剪掉不感兴趣的细节)

import <--snip-->

@NgModule({
  declarations: [
    <--snip-->
  ],
  imports: [
    <--snip-->
  ],
  providers: [
    <---snip-->
    SomeAngularService,
    { provide: Configuration, useValue: AppConfiguration },
    <---snip--->
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

7. some-angular.service.ts(剪掉不感兴趣的细节)

<--snip-->
export class BackendService {

  constructor(private configuration: Configuration) {

    const client = io(
      // Do not append route to the hostname
      // otherwise Socket.IO will treat it as `namespace`
      this.configuration.webSocketUrl,
      {
        // Do not auto-connect to exclude racing
        // between setup and connection
        autoConnect: false,
        // You have to specify route here
        path: configuration.webSocketPath
      });
      <--snip-->

角度加分

8. proxy.conf.json

{
  "/socket.io/*": {
    "target": "http://localhost:3000/socket.io/",
    "ws": true,
    "secure": false,
    "logLevel": "debug",
    "pathRewrite": { "^/socket.io" : "" }
  },
  "/image/*": {
    "target": "http://localhost:3000/image/",
    "secure": false,
    "logLevel": "debug",
    "pathRewrite": { "^/image" : "" }
  }
}

9. angular.json -> 见下面截图前的最后一行

<--snip-->
        "serve": {
          "builder": "@angular-devkit/build-angular:dev-server",
          "options": {
            "browserTarget": "fancy-web-app:build",
            "proxyConfig": "proxy.conf.json"
<--snip-->

【讨论】:

    猜你喜欢
    • 2020-08-19
    • 1970-01-01
    • 2022-01-21
    • 1970-01-01
    • 1970-01-01
    • 2020-09-09
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多