202 lines
6.9 KiB
Docker
202 lines
6.9 KiB
Docker
# ============================================
|
||
# 阶段 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 不会失败,但会警告
|
||
# 如果 npm ci 失败(通常是 package-lock.json 问题),构建会停止
|
||
RUN --mount=type=cache,target=/root/.npm \
|
||
npm ci --prefer-offline --no-audit
|
||
|
||
# 清理构建依赖和 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=""
|
||
|
||
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
|
||
|
||
# 从 deps 阶段复制 node_modules(包含所有依赖)
|
||
COPY --from=deps /app/node_modules ./node_modules
|
||
|
||
# 复制应用源代码(放在后面,因为源代码变化更频繁)
|
||
# 使用 .dockerignore 排除不需要的文件
|
||
COPY . .
|
||
|
||
# 生成 Prisma Client(需要完整的 node_modules)
|
||
# 注意:binaryTargets 已指定 linux-musl-openssl-3.0.x,即使检测失败也会使用正确的二进制
|
||
RUN npx prisma generate
|
||
|
||
# 验证关键依赖是否已安装
|
||
RUN if [ ! -d "node_modules/.prisma" ]; then \
|
||
echo "错误: Prisma Client 未生成" >&2 && exit 1; \
|
||
fi
|
||
|
||
# 构建 Next.js 应用(standalone 模式)
|
||
# 允许构建时出现预渲染错误(这些页面会在运行时动态渲染)
|
||
RUN npm run build || (echo "构建完成,但部分页面预渲染失败(这是预期的,这些页面将在运行时动态渲染)" && exit 0)
|
||
|
||
# 验证构建产物
|
||
RUN if [ ! -f ".next/standalone/server.js" ]; then \
|
||
echo "错误: standalone 输出未生成,请检查 next.config.js 中的 output: 'standalone'" >&2 && exit 1; \
|
||
fi
|
||
|
||
# 为可选模块创建占位符(确保 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
|
||
|
||
# 创建非 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. Prisma schema 和迁移文件(运行时可能需要)
|
||
COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma
|
||
|
||
# 3. lib 目录(包含 configLoader 等运行时模块)
|
||
COPY --from=builder --chown=nextjs:nodejs /app/lib ./lib
|
||
|
||
# 4. 补充关键模块(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
|
||
|
||
# 复制 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
|
||
|
||
# 创建上传目录并设置权限
|
||
RUN mkdir -p /app/uploads && \
|
||
chown -R nextjs:nodejs /app/uploads && \
|
||
chmod -R 755 /app/uploads
|
||
|
||
# 切换到非 root 用户
|
||
USER nextjs
|
||
|
||
EXPOSE 3000
|
||
|
||
# 使用 standalone 输出的 server.js
|
||
CMD ["node", "server.js"]
|
||
|