最近boss给了我一个需求, 要把前端项目分离出来, 现在要让整个前端独立运行. 现在发现容器不够灵活, apihost不一样, 要为staigng和 production环境分别打一个包, 避免环境不一致的问题. 经过我的摸索, 终于找到了个可行的解决方案, 能够一次打包, 在多个环境所运行
众所周知, 在构建 Next.js 应用程序时,对于不同的环境来管理环境变量通常很难,尤其是使用 Docker 部署。一般情况下,是通过 process.env.VARIABLE_NAME 访问环境变量,这中模式对服务器端代码很友好。但是,由于浏览器无法访问服务器的运行时变量,我们只能使用在客户端打包构建过程中嵌入的环境变量。仅仅是因为环境不同而要多次打包, 这不禁令人抓狂😫
在客户端代码中访问 process.env 时,Next.js 通过将这些变量的值直接嵌入到生成的 JavaScript 中来编译它们。例如,以下代码:
const apiURL = process.env.NEXT_PUBLIC_API_URL;
它在构建时将被翻译成: const apiURL = "https://api.example.com";
https://api.example.com
是构建时 NEXT_PUBLIC_API_URL 环境变量的值, 这种行为使得在不重新构建应用程序的情况下无法更改环境变量,同一个镜像无法在不同环境中使用, 失去了灵活性.
那么, 有什么简单又高效的办法使得我们仅需构建一次, 从而能够在多个环境中使用呢? 🤔
有的兄弟, 有的, 接下来, 我将详细阐述如何解决这个问题: 使用自定义 Docker 入口点在运行时动态替换环境变量
准备一个next.js 应用
假设有个next 应用, 我们还需要两个环境变量, 环境名称和 API_URL, 这些值我们要在client 端中使用, 并且, 开发环境/预发布环境/生成, 他们的值均有所不同, 样式如下:
- 开发环境
- env: dev
- apiUrl: dev-api.example.com
- 预发布环境
- env: staging
- apiUrl: staging-api.example.com
- 生产环境
- env: production
- apiUrl: api.example.com
在 next.config.ts 中定义这些变量
为了使这些变量可供客户端使用,我们将在next.config.ts
中使用publicRuntimeConfig
定义。这使我们能够在运行时动态访问特定于环境的值(理论上是这样的):
js
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
publicRuntimeConfig: {
env: process.env.NEXT_PUBLIC_ENV,
apiURL: process.env.NEXT_PUBLIC_API_URL,
},
};
export default nextConfig;
在客户端使用环境变量
一但我们在 next.config.ts
中完成环境变量定义后, 我们可以在客户端代码的任何地方中来访问它们. 在这个实例中, 我将创建一个简单的page来验证该功能, 该page 使用 publicRuntimeConfig
来显示当前的环境以及apiUrl
js
import getConfig from "next/config";
export default function Home() {
const { publicRuntimeConfig } = getConfig();
return (
<div>
<p>你好, 我当前运行在<b>{publicRuntimeConfig.env}</b> 环境</p>
<p>当前apiUrl是<b>{publicRuntimeConfig.apiURL}</b></p>
</div>
);
}
现在我们可以在启动时传入这些参数, 并运行这个项目:
sh
NEXT_PUBLIC_ENV=dev NEXT_PUBLIC_API_URL=https://dev-api.example.com yarn dev
在服务启动后, 你将在页面看到以下内容:
这时, 环境变量确实准确的反应在了本地的开发环境中😃, 但是, 使用这个方式之所以有效, 是因为环境变量是在build之后, 在run之前完成注入的. 如果我们把这个应用打包成docker, 也会存在相关的问题😔, 必须为每个环境单独打包一个镜像
build之后, 环境变量确实会被替换, 这是next本身所决定的, 那么如果我们想通过在运行时来指定环境变量, 那么我们该怎么做呢🤔?
答案就是: build完成替换后, 我们再替换回来
秘籍------使用 Docker Entrypoint 在运行时完成变量替换
假设你已经有一个Dockerfile(虽然你可能没有...), 但是关于创建Dockerfile的内容不在本次博客的范围中, 我们跳过这一部分, 我们的重点放在如何使用 自定义的 Entrypoint
在运行时完成环境变量替换
想法非常简单, 在构建时, 我们不会传入实际的变量, 而是使用占位符
. 然后, 当容器启动后, 再通过脚本把这些占位符替换为你所需要的环境变量
准备环境
如上所述, 我们要使用占位符(环境变量中间值)来简化替换过程, 这些占位符将在运行时被替换为实际的环境变量. next.js 使用 .env* 文件来获取这些环境变量. 你可以使用.env.docker
, .env.production.
, 以及其他env文件. 在本次教程中, 我将使用.env.production
来演示
.env.production
NEXT_PUBLIC_ENV=_NEXT_PUBLIC_ENV_
NEXT_PUBLIC_API_URL=_NEXT_PUBLIC_API_URL_
遵循中间值的简单模式, _{env_var_name}_
,因为这种方法可以更容易地在客户端包中搜索和替换占位符
在运行Dockerfile之前, 你可以复制这个文件到docker中, 从而能够让next.js 在构建后使用这些环境变量, 可以将以下代码复制到你的dockerfile中
Dockerfile
...
COPY .env.production .env.production
RUN yarn build
...
创建Docker 入口点
现在到了整个文章最核心的部分------创建一个Dokcer 入口点脚本
, 脚本将在容器运行前, 搜索客户端中的占位符, 并替换为实际的环境变量, 这是相关脚本:
sh
#!/usr/bin/env sh
set -e
# 获取环境变量并筛选以NEXT_PUBLIC_开头的环境变量
printenv | grep NEXT_PUBLIC_ | while read -r ENV_LINE ; do
# 通过找到的行, 将 key 和 value 分隔开
ENV_KEY=$(echo $ENV_LINE | cut -d "=" -f1)
ENV_VALUE=$(echo $ENV_LINE | cut -d "=" -f2)
# 找到所有的占位符, 并用当前的环境变量值替换它
find .next -type f -exec sed -i "s|_${ENV_KEY}_|${ENV_VALUE}|g" {} \;
done
# 执行应用程序
exec "$@"
这个脚本确保了.next.production
文件(构建的客户端包)中的占位符(如_NEXT_PUBLIC_ENV_
和_NEXT_PUBLIC_API_URL_
)被传递给容器的实际环境变量替换. 最后,它执行传递给容器的主命令( exec "$@"
).
现在只需要把这个脚本复制到容器中, 以下是更新后的Dockerfile
Dockerfile
COPY ./docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
# 把入口点设置到新加的脚本上
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
CMD ["node", "server.js"]
测试Docker容器
现在我们尝试在运行容器时, 传入不同的环境变量值来校验它是否生效
先从staing 开始
bash
docker run --rm -e NEXT_PUBLIC_ENV=staging -e NEXT_PUBLIC_API_URL=https://staging-api.example.com -p 3000:3000 next-app
替换成 production
bash
docker run --rm -e NEXT_PUBLIC_ENV=prod -e NEXT_PUBLIC_API_URL=https://api.example.com -p 3000:3000 next-app
总结
在本篇文章中,我们探讨了在 Dockerized Next.js 应用程序中动态管理环境变量的实用方法. 通过使用中间占位符并在运行时用自定义 Docker 入口点替换它们,克服了环境变量在构建时固定的限制. 此方法可确保单个 Docker 映像可以部署到多个环境中,而无需为每个环境重建镜像
虽然这个解决方案是使用 Next.js 应用进行演示的,但它并不仅限于 Next.js. 同样的方法可以应用于其他前端框架和库,例如 React、NuxtJS 甚至普通的 JavaScript 应用. 如果您的应用程序在容器化设置中需要动态环境特定值,此策略可提供灵活、高效且可重复使用的解决方案
最后, 希望能够帮助到你🙇