# ============================================ # 阶段 1: 基础镜像 # ============================================ FROM node:18-alpine AS base # 使用阿里云镜像源加速(所有阶段统一配置) RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories # ============================================ # 阶段 2: 依赖安装 (deps) # ============================================ FROM base AS deps # 安装构建时依赖(使用虚拟包,便于后续清理) # 注意:cairo/pango 主要用于 canvas(可选依赖),sharp 不需要这些 # 如果 canvas 安装失败,应用仍可使用 ag-psd 的 fallback 方法 RUN apk add --no-cache --virtual .build-deps \ libc6-compat \ openssl3 \ openssl-dev \ # Canvas 相关依赖(可选,用于 ag-psd 的 canvas 支持) cairo \ cairo-dev \ pango \ pango-dev \ jpeg-dev \ giflib-dev \ librsvg \ pixman-dev \ # 编译工具(用于编译原生模块) python3 \ make \ g++ \ pkgconfig WORKDIR /app # 复制依赖文件(放在前面以利用 Docker 缓存) COPY package.json package-lock.json* ./ # 配置 npm(仅在构建阶段,使用国内镜像源加速) # 注意:这些配置不会进入最终镜像 # sharp 的二进制文件通过环境变量配置,而不是 npm config RUN npm config set registry https://registry.npmmirror.com/ # 安装所有依赖(包括 devDependencies,构建时需要) # 使用缓存挂载加速 npm install(需要 BuildKit: DOCKER_BUILDKIT=1) # 注意:canvas 是可选依赖,如果安装失败,npm ci 不会失败,但会警告 # 使用 --legacy-peer-deps 解决 canvas 版本冲突问题 RUN --mount=type=cache,target=/root/.npm \ npm ci --prefer-offline --no-audit --legacy-peer-deps # 清理构建依赖和 npm 配置(保留运行时所需的库) RUN apk del .build-deps && \ # 清理 npm 配置(不在运行镜像中保留) npm config delete registry && \ # 只保留运行时库 apk add --no-cache \ libc6-compat \ ca-certificates \ openssl3 # ============================================ # 阶段 3: 构建应用 (builder) # ============================================ FROM base AS builder ARG DATABASE_URL="" ARG NEXTAUTH_URL="" ARG APP_PUBLIC_URL="" ARG PRISMA_ENGINE_CACHE_FILE_NAME="libquery_engine-linux-musl-openssl-3.0.x.so.node" ARG PRISMA_SCHEMA_ENGINE_CACHE_FILE_NAME="schema-engine-linux-musl-openssl-3.0.x" ENV PRISMA_ENGINE_CACHE_FILE_NAME=${PRISMA_ENGINE_CACHE_FILE_NAME} ENV PRISMA_SCHEMA_ENGINE_CACHE_FILE_NAME=${PRISMA_SCHEMA_ENGINE_CACHE_FILE_NAME} ENV DATABASE_URL=${DATABASE_URL} ENV NEXTAUTH_URL=${NEXTAUTH_URL} ENV APP_PUBLIC_URL=${APP_PUBLIC_URL} ENV NEXT_TELEMETRY_DISABLED=1 WORKDIR /app # 安装 OpenSSL 3(让 Prisma 能够检测到,消除警告) RUN apk add --no-cache openssl3 # 设置构建环境变量 ENV NODE_ENV=production # Prisma 官方偶尔在下载校验或引擎文件时返回 5xx,这里允许缺失校验文件 ENV PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING=1 # 从 deps 阶段复制 node_modules(包含所有依赖) COPY --from=deps /app/node_modules ./node_modules # 复制应用源代码(放在后面,因为源代码变化更频繁) # 使用 .dockerignore 排除不需要的文件 COPY . . # 配置 Prisma 引擎缓存路径(可离线使用) ENV PRISMA_ENGINE_CACHE_PATH=/app/prisma/engine-cache/${PRISMA_ENGINE_CACHE_FILE_NAME} ENV PRISMA_SCHEMA_ENGINE_CACHE_PATH=/app/prisma/engine-cache/${PRISMA_SCHEMA_ENGINE_CACHE_FILE_NAME} # 将缓存的引擎文件复制到 Prisma CLI 缓存目录,避免构建时重新下载 RUN set -ex && \ if [ -f "${PRISMA_ENGINE_CACHE_PATH}" ]; then \ echo "检测到本地 Prisma 查询引擎缓存,路径:${PRISMA_ENGINE_CACHE_PATH}" && \ PRISMA_ENGINE_VERSION=$(node -p "require('@prisma/engines-version/package.json').prisma.enginesVersion") && \ TARGET_DIR="/root/.cache/prisma/master/${PRISMA_ENGINE_VERSION}/linux-musl-openssl-3.0.x" && \ mkdir -p "${TARGET_DIR}" && \ cp "${PRISMA_ENGINE_CACHE_PATH}" "${TARGET_DIR}/libquery_engine.so.node" && \ gzip -c "${PRISMA_ENGINE_CACHE_PATH}" > "${TARGET_DIR}/libquery_engine.so.node.gz" && \ sha256sum "${TARGET_DIR}/libquery_engine.so.node.gz" | awk '{print $1}' > "${TARGET_DIR}/libquery_engine.so.node.gz.sha256"; \ else \ echo "未找到本地 Prisma 查询引擎缓存(${PRISMA_ENGINE_CACHE_PATH}),将尝试在线下载"; \ fi && \ if [ -f "${PRISMA_SCHEMA_ENGINE_CACHE_PATH}" ]; then \ echo "检测到本地 Prisma schema 引擎缓存,路径:${PRISMA_SCHEMA_ENGINE_CACHE_PATH}" && \ PRISMA_ENGINE_VERSION=$(node -p "require('@prisma/engines-version/package.json').prisma.enginesVersion") && \ TARGET_DIR="/root/.cache/prisma/master/${PRISMA_ENGINE_VERSION}/linux-musl-openssl-3.0.x" && \ mkdir -p "${TARGET_DIR}" && \ cp "${PRISMA_SCHEMA_ENGINE_CACHE_PATH}" "${TARGET_DIR}/schema-engine" && \ chmod +x "${TARGET_DIR}/schema-engine" && \ gzip -c "${PRISMA_SCHEMA_ENGINE_CACHE_PATH}" > "${TARGET_DIR}/schema-engine.gz" && \ sha256sum "${TARGET_DIR}/schema-engine.gz" | awk '{print $1}' > "${TARGET_DIR}/schema-engine.gz.sha256"; \ else \ echo "未找到本地 Prisma schema 引擎缓存(${PRISMA_SCHEMA_ENGINE_CACHE_PATH}),将尝试在线下载"; \ fi # 生成 Prisma Client(需要完整的 node_modules) # 默认使用官方源,如遇 5xx 再自动切换到 npm 镜像源重试 RUN set -ex && \ if [ -f "${PRISMA_ENGINE_CACHE_PATH}" ]; then \ export PRISMA_QUERY_ENGINE_LIBRARY="${PRISMA_ENGINE_CACHE_PATH}" && \ export PRISMA_CLIENT_ENGINE_TYPE=library && \ export PRISMA_CLI_QUERY_ENGINE_TYPE=library && \ echo "使用本地 Prisma 查询引擎缓存优先生成 Client"; \ else \ echo "本地 Prisma 引擎缓存不可用,将使用在线下载"; \ unset PRISMA_QUERY_ENGINE_LIBRARY PRISMA_CLIENT_ENGINE_TYPE PRISMA_CLI_QUERY_ENGINE_TYPE; \ fi && \ if [ -f "${PRISMA_SCHEMA_ENGINE_CACHE_PATH}" ]; then \ export PRISMA_SCHEMA_ENGINE_BINARY="${PRISMA_SCHEMA_ENGINE_CACHE_PATH}" && \ echo "使用本地 Prisma schema 引擎缓存"; \ else \ echo "本地 Prisma schema 引擎缓存不可用,将使用在线下载"; \ unset PRISMA_SCHEMA_ENGINE_BINARY; \ fi && \ if npx prisma generate; then \ exit 0; \ fi && \ echo "Prisma 官方源下载失败,尝试使用镜像源重试..." && \ PRISMA_ENGINES_MIRROR="https://registry.npmmirror.com/-/binary/prisma/" npx prisma generate # 验证关键依赖是否已安装 RUN if [ ! -d "node_modules/.prisma" ]; then \ echo "错误: Prisma Client 未生成" >&2 && exit 1; \ fi # 构建 Next.js 应用(standalone 模式) # 商业交付镜像必须完成服务端混淆;混淆失败不能继续出包。 RUN set -ex && \ if [ -f "${PRISMA_ENGINE_CACHE_PATH}" ]; then \ export PRISMA_QUERY_ENGINE_LIBRARY="${PRISMA_ENGINE_CACHE_PATH}" && \ export PRISMA_CLIENT_ENGINE_TYPE=library && \ export PRISMA_CLI_QUERY_ENGINE_TYPE=library; \ else \ unset PRISMA_QUERY_ENGINE_LIBRARY PRISMA_CLIENT_ENGINE_TYPE PRISMA_CLI_QUERY_ENGINE_TYPE; \ fi && \ if [ -f "${PRISMA_SCHEMA_ENGINE_CACHE_PATH}" ]; then \ export PRISMA_SCHEMA_ENGINE_BINARY="${PRISMA_SCHEMA_ENGINE_CACHE_PATH}"; \ else \ unset PRISMA_SCHEMA_ENGINE_BINARY; \ fi && \ npm run build:prod # 验证构建产物(允许 ESLint 警告,但不允许致命错误) RUN if [ ! -f ".next/standalone/server.js" ]; then \ echo "错误: standalone 输出未生成,请检查 next.config.js 中的 output: 'standalone'" >&2 && \ echo "提示: 如果构建过程中有 React Hooks 或其他编译错误,请先修复后再构建" >&2 && \ exit 1; \ fi # 写入版本文件(用于许可证系统版本检查) ARG APP_VERSION="1.0.0" RUN echo "${APP_VERSION}" > VERSION.txt # 为可选模块创建占位符(确保 COPY 不会失败) # 如果 canvas 未安装,创建一个空占位符目录 RUN if [ ! -d "node_modules/canvas" ]; then \ echo "Canvas 未安装,创建占位符目录" && \ mkdir -p node_modules/canvas && \ echo '{"name":"canvas-placeholder"}' > node_modules/canvas/package.json; \ fi # ============================================ # 阶段 4: 生产运行 (runner) # ============================================ FROM base AS runner WORKDIR /app # 设置生产环境变量 ENV NODE_ENV=production ENV NEXT_TELEMETRY_DISABLED=1 ENV PORT=3000 ENV HOSTNAME="0.0.0.0" # 只安装运行时必需的库(最小化镜像大小) # sharp 只需要基本的系统库,不需要 cairo/pango # 但如果 canvas 被安装(可选依赖),则需要这些库 RUN apk add --no-cache \ libc6-compat \ ca-certificates \ openssl3 \ # Canvas 运行时库(如果 canvas 被安装则需要,否则可忽略) # 注意:如果 canvas 未安装,这些库不会造成问题,只是占用一点空间 cairo \ pango \ jpeg \ giflib \ librsvg \ pixman \ # 字体支持(canvas 渲染中文时需要) fontconfig \ ttf-dejavu \ # FFmpeg(视频转码功能,可选) # 如果不需要视频转码功能,可以注释掉以下两行以减小镜像大小(约节省 30-45MB) ffmpeg \ ffmpeg-libs # 创建非 root 用户 RUN addgroup --system --gid 1001 nodejs && \ adduser --system --uid 1001 nextjs # 从 builder 阶段复制构建产物 # standalone 输出包含所有必需的依赖和 server.js COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static # 复制必要的额外文件 # 1. public 目录(静态资源) COPY --from=builder --chown=nextjs:nodejs /app/public ./public # 2. 额外静态资源(如默认 Logo) COPY --from=builder --chown=nextjs:nodejs /app/logo ./public/logo # 3. Prisma schema 和迁移文件(运行时可能需要) COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma # 4. 版本文件(许可证系统版本检查) COPY --from=builder --chown=nextjs:nodejs /app/VERSION.txt ./VERSION.txt # 4. lib 目录(包含 configLoader 等运行时模块) COPY --from=builder --chown=nextjs:nodejs /app/lib ./lib # 5. 补充关键模块(standalone 输出可能未完整包含) # 复制 Prisma Client(必需) COPY --from=builder --chown=nextjs:nodejs /app/node_modules/.prisma ./node_modules/.prisma # 复制 Prisma 包,确保运行时存在 @prisma/client 及其生成器 COPY --from=builder --chown=nextjs:nodejs /app/node_modules/@prisma ./node_modules/@prisma # 复制 Prisma CLI(用于运行时执行 migrate deploy) COPY --from=builder --chown=nextjs:nodejs /app/node_modules/prisma ./node_modules/prisma # 复制 npm 可执行文件,确保 npx prisma 可用 COPY --from=builder --chown=nextjs:nodejs /app/node_modules/.bin ./node_modules/.bin # 复制 Canvas 模块(可选依赖) # 如果 canvas 未安装,builder 阶段已创建占位符,所以这个 COPY 不会失败 # 在运行时,如果 canvas 是占位符,应用会使用 ag-psd 的 fallback 方法 COPY --from=builder --chown=nextjs:nodejs /app/node_modules/canvas ./node_modules/canvas # 复制 ag-psd 模块 COPY --from=builder --chown=nextjs:nodejs /app/node_modules/ag-psd ./node_modules/ag-psd # 清理占位符(如果 canvas 是占位符,删除它) RUN if [ -f "./node_modules/canvas/package.json" ] && grep -q "canvas-placeholder" ./node_modules/canvas/package.json 2>/dev/null; then \ echo "Canvas 未安装,清理占位符,使用 ag-psd fallback" && \ rm -rf ./node_modules/canvas; \ fi # 创建上传目录和数据目录并设置权限 # - uploads: 用户上传的文件 # - data: 许可证实例 ID 等持久化数据 RUN mkdir -p /app/uploads /app/data && \ chown -R nextjs:nodejs /app/uploads /app/data && \ chmod -R 755 /app/uploads /app/data # 复制启动脚本 COPY --from=builder --chown=nextjs:nodejs /app/scripts/docker-entrypoint.sh ./docker-entrypoint.sh RUN chmod +x ./docker-entrypoint.sh # 切换到非 root 用户 USER nextjs EXPOSE 3000 # 使用启动脚本(自动执行数据库迁移和初始化) CMD ["./docker-entrypoint.sh"]