Docker Container from Image无法识别React Application中的env变量,即使它们可以在Docker Container中进行echo

vjhs03f7  于 2023-08-03  发布在  Docker
关注(0)|答案(3)|浏览(107)

我正在经历一些奇怪的事情,我试图在我的React应用程序中实现CI/CD,以将代码作为Docker Hub中的图像推送,但是当我尝试使用env变量拉取并运行它时,即使env变量带有前缀REACT_APP,它们在前端也是未定义的,但由于某种原因,它们存储在容器中,因为我做了echo,它会打印出来。另一方面,如果我只是在本地构建dockerimage,而不是通过拉取镜像(运行dockerfile),则env变量会正确存储。
驳接文件:

FROM node:16-alpine as builder
# Set the working directory to /app inside the container
WORKDIR /app
# Copy app files
COPY . .
# Install dependencies (npm ci makes sure the exact versions in the lockfile gets installed)
RUN npm ci 
# Build the app
RUN npm run build

# Bundle static assets with nginx
FROM nginx:1.21.0-alpine as production
ENV NODE_ENV production
# Copy built assets from `builder` image
COPY --from=builder /app/build /usr/share/nginx/html
# Add your nginx.conf
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Expose port
EXPOSE 80
# Start nginx
CMD ["nginx", "-g", "daemon off;"]

字符串
conf(在根目录下作为文件不在nginx文件夹中)

server {
  listen 80;

  location / {
    root /usr/share/nginx/html/;
    include /etc/nginx/mime.types;
    try_files $uri $uri/ /index.html;
  }
}


.github/workflow/docker-image.yml

name: Push Backend as Docker Image

on:
  push:
    branches: ["master"]
  pull_request:
    branches: ["master"]

jobs:
  push_to_registry:
    name: Push Docker image to Docker Hub
    runs-on: ubuntu-latest
    steps:
      - name: Check out the repo
        uses: actions/checkout@v3

      - name: Log in to Docker Hub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Build and push Docker image
        uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
        with:
          context: .
          push: true
          tags: dummy-frontend/test:latest


.env

REACT_APP_URL=url
REACT_APP_API_URL=url
REACT_APP_VEHICLE_IMAGE_URL=url
REACT_APP_NAME=app_name


代码示例:

{process.env.REACT_APP_NAME === null || process.env.REACT_APP_NAME === undefined
        ? "undefined"
        : process.env.REACT_APP_NAME}


当我不是通过docker pull而是从docker文件在本地运行应用程序时,这些是命令(它显示了react应用程序名称):

docker build . -t dockerized-react

docker run -p 3000:80 --env-file .env -d dockerized-react


当我通过docker pull在本地运行应用程序时,这些是命令(env变量是undefined的,但我可以通过容器回显env变量):

docker run -p 3000:80 --env-file .env -d dummy-frontend/test:latest


在这里,它是undefined。
我不明白问题出在哪里。

h5qlskok

h5qlskok1#

当您构建React应用程序时,it inlines the value of environment variables into the static JavaScript bundle that it produces。这种情况发生在 * 构建 * 时,而不是运行时。
这就是为什么环境变量在Docker容器中是未定义的,因为它们在构建过程中不存在(由GitHub Action完成的过程)。
当您在本地机器上构建Docker映像时,您有.env file present,因此环境变量被正确地内联到静态包中。
但是,当您使用GitHub Actions构建Docker映像时,环境变量并不存在,因为您的Dockerfile显示您没有将它们复制到Docker映像中。因此,它们在生成时不可用,也不能内联到包中。
可以考虑在运行时构建React应用程序。
但是......,这将显著增加容器的启动时间,因为每次启动容器时都需要构建React应用程序。
我更喜欢在GitHub Actions上构建Docker映像时包含您的环境变量。
您可以使用GitHub Actions秘密来存储您的环境变量,然后使用build-args输入将它们传递到工作流的docker/build-push-action步骤。

- name: Build and push Docker image
  uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
  with:
      context: .
      push: true
      tags: dummy-frontend/test:latest
      build-args: |
        REACT_APP_URL=${{ secrets.REACT_APP_URL }}
        REACT_APP_API_URL=${{ secrets.REACT_APP_API_URL }}
        REACT_APP_VEHICLE_IMAGE_URL=${{ secrets.REACT_APP_VEHICLE_IMAGE_URL }}
        REACT_APP_NAME=${{ secrets.REACT_APP_NAME }}

字符串
这样,您的环境变量将在构建时可用,并且可以内联到静态React包中。
同样,用你的GitHub密码的实际名称替换secrets语法中的占位符。
但是,这将使用内嵌到静态包中的环境变量构建Docker映像,因此同一Docker映像不能用于不同的环境变量。
最后,不要忘记在Dockerfile中为每个构建参数添加ARG指令:

# ...
ARG REACT_APP_URL
ARG REACT_APP_API_URL
ARG REACT_APP_VEHICLE_IMAGE_URL
ARG REACT_APP_NAME
# ...


这些指令将放在npm run build命令 * 之前 *。
但事实上,我想在Docker容器上运行这些,我是说ENV属性,因为在GitHub上我不认为是非常安全的。运行容器时是否有机会编辑这些内容?
如果您希望在运行容器时使用不同的环境变量,则需要对应用程序的服务方式进行一些更改。
由于环境变量在React中是在构建时内联的,因此您将无法在运行时以传统方式更改它们。另请参阅React environment variables: A developer’s guideLee Brandt
然而,有一个解决方法。
一种常见的方法是在容器启动时运行一个服务器端脚本,该脚本将生成一个包含环境变量的JavaScript文件。然后,该文件可以包含在HTML中,从而允许JavaScript代码访问环境变量。
在公用文件夹中添加一个脚本(我们将其命名为env-config.js):

window._env_ = {
  REACT_APP_URL: "default",
  REACT_APP_API_URL: "default",
  REACT_APP_VEHICLE_IMAGE_URL: "default",
  REACT_APP_NAME: "default",
}


public/index.html中包括脚本:

<!DOCTYPE html>
<html lang="en">
  <head>
    <!-- ... -->
  </head>
  <body>
    <script src="%PUBLIC_URL%/env-config.js"></script>
    <!-- ... -->
  </body>
</html>


更新JavaScript代码以使用window._env_而不是process.env

{
  window._env_.REACT_APP_NAME === null || window._env_.REACT_APP_NAME === undefined
    ? "undefined"
    : window._env_.REACT_APP_NAME
}


然后在Dockerfile中添加一个在运行时生成env-config.js的脚本:
下面是使用shell脚本scripts/env.sh的示例:

#!/bin/bash
# Stop script on errors
set -e

# Recreate config file
rm -rf ./public/env-config.js
touch ./public/env-config.js

# Add assignment 
echo "window._env_ = {" >> ./public/env-config.js

# Read each line in .env file
# Each line represents key=value pairs
while IFS= read -r line || [ -n "$line" ];
do
  if printf '%s\n' "$line" | grep -q -e '=';
  then
    varname=$(printf '%s\n' "$line" | sed -e 's/=.*//')
    varvalue=$(printf '%s\n' "$line" | sed -e 's/^[^=]*=//')
    echo "  $varname: \"$varvalue\"," >> ./public/env-config.js
  fi
done < .env

echo "}" >> ./public/env-config.js


然后,您可以在容器启动时调用此脚本。Dockerfile可能如下所示:

# ...
# Add your nginx.conf
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Copy env.sh
COPY scripts/env.sh /usr/share/nginx/html/env.sh
# Make the script executable
RUN chmod +x /usr/share/nginx/html/env.sh
# Expose port
EXPOSE 80
# Start nginx
CMD [ "/bin/sh", "-c", "/usr/share/nginx/html/env.sh && nginx -g \"daemon off;\"" ]


现在,当您运行容器时,可以传入环境变量,应用程序将获取这些变量:

docker run -p 3000:80 --env-file .env -d dummy-frontend/test:latest


这意味着您的环境变量不会被烘焙到Docker映像中,这意味着同一个映像可以在不同的环境中使用。而且,由于环境变量是在运行时插入HTML的,因此只需使用不同的变量重新启动Docker容器就可以更改它们。
但是,它会在HTML的源代码中公开环境变量,因此不应将其用于敏感变量。如果您需要使用敏感变量,请考虑在服务器端调用一个可以返回您的变量的安全终结点。
您还可以使用Joel Lord中的“使环境变量可在前端容器中访问”作为替代方法。

kr98yfug

kr98yfug2#

您遇到的问题是由于React应用程序中环境变量的性质以及Docker的工作方式。
当使用create-react-app构建React应用程序时,它有一个构建步骤,其中它采用以REACT_APP_开头的环境变量并将其直接内联到构建中。这意味着这些值是在生成时设置的,而不是在运行时设置的。因此,环境变量需要在执行npm run build命令的环境中可用。
这意味着当你在本地构建Docker镜像并使用--env-file .env运行它时,环境变量在构建时被正确读取并内联到JavaScript包中。这就是为什么您的应用程序按预期工作的原因。
但是,当Docker镜像在GitHub Actions上构建时,.env文件不存在,因此环境变量不会在构建中内联。稍后,当您拉取映像并使用--env-file .env运行它时,它不会产生预期的效果,因为这些环境变量是在运行时读取的,这已经太晚了。在没有这些环境变量的情况下,构建已经发生。
要解决这个问题,您需要在构建时在GitHub Actions工作流中构建映像时使环境变量可用。
您可以在GitHub Actions工作流中添加一个步骤,以便在构建步骤之前创建.env文件:

name: Push Backend as Docker Image

on:
  push:
    branches: ["master"]
  pull_request:
    branches: ["master"]

jobs:
  push_to_registry:
    name: Push Docker image to Docker Hub
    runs-on: ubuntu-latest
    steps:
      - name: Check out the repo
        uses: actions/checkout@v3

      - name: Set up environment variables
        run: |
          echo "REACT_APP_URL=${{ secrets.REACT_APP_URL }}" >> .env
          echo "REACT_APP_API_URL=${{ secrets.REACT_APP_API_URL }}" >> .env
          echo "REACT_APP_VEHICLE_IMAGE_URL=${{ secrets.REACT_APP_VEHICLE_IMAGE_URL }}" >> .env
          echo "REACT_APP_NAME=${{ secrets.REACT_APP_NAME }}" >> .env

      - name: Log in to Docker Hub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Build and push Docker image
        uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
        with:
          context: .
          push: true
          tags: dummy-frontend/test:latest

字符串
您需要在GitHub存储库中将REACT_APP_URLREACT_APP_API_URLREACT_APP_VEHICLE_IMAGE_URLREACT_APP_NAME设置为secret。这允许您保持这些值的安全性,并防止它们在存储库或GitHub Actions日志中暴露。

pgvzfuti

pgvzfuti3#

其他答案已经详细说明了为什么会有问题。然而,我想提出一个替代解决方案。
您可以在运行时将变量写入JS文件并将其导入React应用程序中。这样,您就不需要在构建时知道它应该具有什么值。在我看来,构建时技术违背了容器精神。
所以一个简单的实现看起来像这样。
1.为容器创建一个入口点脚本。在该入口点中,您可以选择env变量并将其写入一个最小化的JS文件。

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

cat <<EOF >/usr/share/nginx/html/runtime/vars.js
export default {
  url: "${REACT_APP_URL:-some-default-url}",
  apiUrl: "${REACT_APP_API_URL:-other-default-url}",
};
EOF

字符串
使用nginx时,可以将此脚本添加到entrypoint.d

COPY --chmod=0755 entrypoint.sh /docker-entrypoint.d/


1.现在,您可以向JavaScript应用添加一些代码来导入文件,并在应用中使用它。
比如说

import injects from './runtime/vars.js';

function Welcome(props) {
  return <a href="{props.url}">Example</a>;
}

const root = ReactDOM.createRoot(document.getElementById('root'));
const element = <Welcome url=injects.url />;
root.render(element);

注意我不是一个react开发人员,所以这段代码只是给予一个大致的概念。您可能更清楚如何以及在何处传递变量。

相关问题