Docker 部署 json4u 项目问题总结与解决方案

写在前面

最近在小红书上看到一个名为json4u的开源项目,感觉挺有意思的。考虑到日常工作涉及大量JSON数据处理,并且出于对数据安全和效率提升的考虑,本地化部署显然比在线工具更加可靠和高效。这个项目看起来非常实用,尤其是对于需要频繁处理JSON数据的场景,应该能带来不少便利。相比在线工具,本地部署不仅降低了数据泄露的风险,还能提高工作效率,所以准备部署一把尝试一下。

项目简介

json4u 是一个开源、全功能的 JSON 可视化与处理工具。它能将 JSON 数据以直观的树状图或思维导图形式展示,并支持格式化、转义、压缩等操作。虽然我曾使用 pnpm 成功部署过该项目,但在 Docker 部署过程中遇到了一些问题,特别是关于构建产物与环境变量加载的问题。本文总结了这些问题和解决方案,希望这篇总结对你未来的部署工作有所帮助。


1. 依赖安装问题:绕过 Corepack

问题描述

最初使用 Corepack 调用 pnpm 安装依赖时,出现签名验证错误,导致构建失败。

原Dockerfile:

# build stage
FROM node:20-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 pnpm-lock.yaml* ./
RUN corepack enable pnpm && pnpm i --frozen-lockfile

# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

ARG APP_URL=http://localhost.json4u.cn:3000
ARG FREE_QUOTA=99
ARG SENTRY_AUTH_TOKEN=

ENV SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN
ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_ENV=production
ENV NEXT_PUBLIC_APP_URL=$APP_URL
ENV NEXT_PUBLIC_FREE_QUOTA="{\"graphModeView\":$FREE_QUOTA,\"tableModeView\":$FREE_QUOTA,\"textComparison\":$FREE_QUOTA,\"jqExecutions\":$FREE_QUOTA}"

RUN corepack enable pnpm && pnpm run build

# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app

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

# server.js is created by next build from the standalone output
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]

解决方案

改用 npm 全局安装 pnpm,再使用 pnpm 安装依赖。修改后的 Dockerfile 部分代码如下:

RUN npm install -g pnpm && pnpm install --frozen-lockfile

这种方式成功避免了 Corepack 签名验证的问题,保证了依赖能够正确安装。


2. Runner 阶段缺少 pnpm 和 package.json

问题描述

在生产镜像运行时,先后出现了“Cannot find module ‘/app/pnpm’”和“ERR_PNPM_NO_IMPORTER_MANIFEST_FOUND”错误,表明 runner 阶段缺少 pnpm 命令和 package.json 文件。

解决方案

在 runner 阶段显式全局安装 pnpm,并复制 package.json 至容器内:

FROM base AS runner
WORKDIR /app
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs

# 复制依赖、构建产物、public 目录以及 package.json
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./

# 在 runner 阶段全局安装 pnpm
RUN npm install -g pnpm

USER nextjs

EXPOSE 8888
ENV PORT=8888
ENV HOSTNAME="0.0.0.0"
CMD ["pnpm", "start"]

这样确保了启动时 pnpm 能够找到 package.json 和启动脚本,避免报错。


3. Next.js 构建输出问题:Standalone 模式 vs. 非 Standalone 模式

尝试方案 A:启用 Standalone 模式

原计划在 Next.js 构建中使用 Standalone 模式,通过在 next.config.cjs 中配置:

module.exports = {
  output: 'standalone',
  // 其他配置…
}

以生成精简的构建产物,然后在 runner 阶段只复制 .next/standalone.next/static 文件夹。但在实际尝试后,发现构建后 .next/standalone 并未生成,导致复制时报错:“/app/.next/standalone: not found”。经过排查,发现可能与 Next.js 版本、配置或项目结构有关,Standalone 模式方案最终无法奏效。

采用方案 B:非 Standalone 模式

最终选择复制整个 .next 目录,通过 Next.js 默认的启动命令启动应用。这种方式虽然生成的镜像体积较大,但更稳定,能确保构建产物完整。

修改后的 builder 和 runner 阶段代码如下:

# Builder 阶段:构建产物
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm install -g pnpm
RUN pnpm run build

# Runner 阶段:生产环境镜像(非 Standalone 模式)
FROM base AS runner
WORKDIR /app
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./
RUN npm install -g pnpm
USER nextjs
EXPOSE 8888
ENV PORT=8888
ENV HOSTNAME="0.0.0.0"
CMD ["pnpm", "start"]

确保 package.json 中有如下启动脚本:

"scripts": {
  "start": "next start",
  "build": "next build",
  // ...
}

4. 环境变量加载问题

问题描述

启动时应用校验环境变量失败,日志中显示:

  • LEMONSQUEEZY_STORE_ID 被期望为数字,但解析为 NaN。
  • LEMONSQUEEZY_WEBHOOK_SECRETLEMONSQUEEZY_API_KEYSUPABASE_KEY 等必填变量未提供有效值。
  • 同时,LEMONSQUEEZY_SUBSCRIPTION_VARIANT_MAP 出现 “Invalid JSON” 错误(常见原因是外层引号问题)。

解决方案

使用 docker run 命令的 --env-file 参数加载环境变量文件,而不是依赖 Dockerfile 自动加载。假设你的 .env 文件内容如下(注意去掉数值型变量的引号,并保证 JSON 格式合法):

NEXT_PUBLIC_APP_URL="http://localhost.json4u.com:8888"
NEXT_PUBLIC_FREE_QUOTA='{"graphModeView":30,"tableModeView":30,"textComparison":30,"jqExecutions":30}'
NEXT_PUBLIC_SUPABASE_URL="https://kfuwzghygbtmonplcuou.supabase.co"
NEXT_PUBLIC_SUPABASE_ANON_KEY="your_supabase_anon_key"
NEXT_PUBLIC_HCAPTCHA_SITE_KEY="c87e1d8c-e81c-4dbc-a540-60246482751a"

SUPABASE_PROJECT_ID="kfuwzghygbtmonplcuou"
SUPABASE_KEY=your_supabase_key
LEMONSQUEEZY_STORE_ID=1
LEMONSQUEEZY_SUBSCRIPTION_VARIANT_MAP={"monthly":1,"yearly":2}
LEMONSQUEEZY_WEBHOOK_SECRET=your_webhook_secret
LEMONSQUEEZY_API_KEY=your_api_key
SENTRY_ORG="loggerhead"
SENTRY_PROJECT="json4u"
SENTRY_DSN="https://d60bd8847a6d8afc72e3de0d9288fa4c@o4506325094236160.ingest.us.sentry.io/4506325157085184"
SENTRY_AUTH_TOKEN=your_sentry_auth_token

然后通过如下命令启动容器:

docker run -d -p 8888:8888 --env-file .env json4u:latest

这样 Docker 会自动将 .env 文件中的所有变量注入到容器中,确保应用在启动时能正确读取并验证这些环境变量。


5. 完整部署命令

构建镜像

在项目根目录下运行:

docker build -t json4u:latest .

运行容器并加载环境变量

确保 .env 文件在项目根目录,执行:

docker run -d -p 8888:8888 --env-file .env json4u:latest

总结

在 Docker 部署 json4u 项目的过程中,我遇到的主要问题和解决方案如下:

  1. 依赖安装问题:通过 npm 全局安装 pnpm,避免了 Corepack 的签名验证问题。
  2. Runner 阶段环境缺失:在生产阶段重新安装 pnpm,并复制 package.json,确保启动时能读取配置。
  3. Standalone 模式问题:尝试 Standalone 模式(方案 A)未能生成预期产物,最终采用复制整个 .next 目录的方案(方案 B),使用 Next.js 默认的启动命令。
  4. 环境变量加载问题:Dockerfile 不会自动加载主机上的 .env 文件,所以必须在 docker run 时使用 --env-file 参数来加载环境变量,并确保变量格式正确(如数值变量不加引号,JSON 格式保持合法)。

最终的部署指令如下:

构建镜像: docker build -t json4u:latest .

使用环境变量文件启动容器: docker run -d -p 8888:8888 --env-file .env json4u:latest

修改后的Dockerfile:

# build stage
FROM node:20-alpine AS base

# Install dependencies only when needed
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json pnpm-lock.yaml* ./
RUN npm install -g pnpm && pnpm install --frozen-lockfile

# Builder 阶段:复制依赖和代码,并执行构建
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm install -g pnpm
RUN pnpm run build

# Runner 阶段:生产环境镜像(非 standalone 模式)
FROM base AS runner
WORKDIR /app
RUN npm install -g pnpm
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

# 复制 node_modules、.next 和 public 目录
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./

USER nextjs

EXPOSE 8888
ENV PORT=8888
ENV HOSTNAME="0.0.0.0"
# 通过 package.json 中的 start 脚本启动服务
CMD ["pnpm", "start"]