解决Nextjs应用镜像拿不到环境变量的问题

1,323 阅读3分钟

问题:

使用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 是固定的,如下图所示

image.png

无论我在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 应用镜像的环境变量传递问题。