flnxi-Wu/Dockerfile
2026-05-25 16:10:18 +08:00

300 lines
12 KiB
Docker
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ============================================
# 阶段 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 || (echo "构建完成,但部分页面预渲染失败(这是预期的,这些页面将在运行时动态渲染)" && true)
# 验证构建产物(允许 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"]