问题:
使用nextjs官方给的nextjs-with-docker的 docker 模板构建出来的 nextjs 应用镜像拿不到docker run时,实时给的环境变量-e。
背景:
由于项目里要用到一些动态的环境变量,例如 obs 的地址,用于前端拼接静态资源的路径,例如图片资源(由于某些原因要用 obs 的 ip 地址来访问,否则经 nginx 代理固定的obs服务名的话,就不存在问题)。
在应用启动的时候,才能知道 obs 地址,这些都无法在docker build的时候知道,因为部署在不同的环境,obs 的地址会改,只有在kubesphere运行应用节点的时候才能确定这些环境变量。
问题在于,使用nextjs官方推荐的 nextjs-with-docker的 docker 模板,构建好的应用镜像,应用里上下文获取到的process.env的变量 key 是固定的,如下图所示
无论我在docker run -e的时候传了多少环境变量,应用的process.env都获取不到我传进去的环境变量值。
包括查询了nextjs 官方文档,尝试过serverRuntimeConfig配置、.env.production配置文件等方法都解决不了这个问题,这些都是在docker build的时候“冻结”环境变量process.env 的,于是我开始寻求别的解决方法。
解决方法
既然nextjs自己的 build 过程中已经把process.env“冻结”,那么只能脱离 nextjs 的 build 工作流另外去做这个读写环境变量的工作了。在别的地方去执行一个命令,将实时的环境变量写进文件系统,在 nextjs 应用跑起来后再去读这个环境变量文件。
官方给出的Dockerfile:
FROM node:18-alpine AS base
# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
# ENV NEXT_TELEMETRY_DISABLED 1
RUN yarn build
# If using npm comment out above and use below instead
# RUN npm run build
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
# Uncomment the following line in case you want to disable telemetry during runtime.
# ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000
# set hostname to localhost
ENV HOSTNAME "0.0.0.0"
CMD ["node", "server.js"]
经过调试,可以通过在镜像构建的过程中加一个环节,在node server.js之前执行一个node脚本
init.js
const fs = require('fs')
try {
fs.writeFileSync('/app/env.json', JSON.stringify(process.env), 'utf8')
} catch (err) {
console.log(err)
process.exit(1)
}
然后在上述的Dockerfile中加上
// line 51
RUN echo "" > ./env.json
RUN chown nextjs:nodejs ./env.json
// 省略...
CMD node ./init.js && node server.js
这样的话在docker run之后,传进去的环境变量-e就会写进容器的文件系统中,位于/app/env.json,之后可以通过 nextjs 的 runtime 能力去读这个文件了,例如可以通过 nextjs 的接口Route handler来读取这个文件,通过接口将前端需要的桶地址给到前端,例如:
// /src/app/env/route.js
import { NextResponse } from "next/server";
import fs from 'fs'
import { cookies } from 'next/headers'
export async function GET(request) {
const cookieStore = cookies()
const token = cookieStore.get('token')
token
let info
try {
info = JSON.parse(fs.readFileSync('/app/env.json', 'utf8'))
} catch (error) {
info = {}
}
return NextResponse.json({ ...info });
}
也可以通过nextjs的静态文件代理能力将文件暴露出去。至此,解决了 nextjs 应用镜像的环境变量传递问题。