# ============================================================================= # 追光AI 项目 Dockerfile (多阶段构建) # - stage 1 deps : 安装全部依赖 (含 dev) # - stage 2 builder : 跑 next build + prisma generate # - stage 3 runner : 仅拷贝运行所需,最小化镜像 # - 一个镜像两种身份:APP 启动 nextjs / CRON 启动 supercronic(docker-compose 决定) # ============================================================================= # ---- 阶段 1: 安装依赖 ---- FROM node:20-alpine AS deps WORKDIR /app # 装 python + make + g++ 给 mysql2 / bcryptjs 等原生模块编译用 # 先切换到 aliyun apk 镜像(默认源在中国大陆极慢,1m+ vs 0.1s) RUN sed -i 's|dl-cdn.alpinelinux.org|mirrors.aliyun.com|g' /etc/apk/repositories \ && apk add --no-cache python3 make g++ openssl # 先 copy lock file 缓存层 COPY package.json package-lock.json ./ COPY prisma ./prisma # 用 ci 安装保证版本一致(同时切到 npmmirror 加速) RUN npm config set registry https://registry.npmmirror.com \ && npm ci --no-audit --no-fund --include=dev # ---- 阶段 2: 构建 ---- FROM node:20-alpine AS builder WORKDIR /app RUN sed -i 's|dl-cdn.alpinelinux.org|mirrors.aliyun.com|g' /etc/apk/repositories \ && apk add --no-cache python3 make g++ openssl ENV NEXT_TELEMETRY_DISABLED=1 ENV NODE_ENV=production ENV npm_config_registry=https://registry.npmmirror.com COPY --from=deps /app/node_modules ./node_modules COPY . . # 跑 next build 生成 .next/ # DATABASE_URL 在 build 时需要(next 预渲染静态页面时连接数据库) # 用 .env.example 当 placeholder 生成 prisma client # 同时注入 OPENAI_API_KEY / DEEPSEEK_API_KEY 占位值,避免 openai SDK v6+ 构造时抛错 # (build 期不会真正调用 AI,仅为通过模块求值) ARG DATABASE_URL RUN echo "=== ARG raw ===" && echo "${DATABASE_URL}" && echo "=== env dump ===" && env | grep DATABASE && echo "=== .env file ===" && cat .env | grep DATABASE 2>/dev/null && echo "=== .env.example ===" && cat .env.example | grep DATABASE 2>/dev/null && if [ ! -f .env ]; then cp .env.example .env 2>/dev/null || echo "DATABASE_URL=placeholder" > .env; fi \ && export DATABASE_URL="${DATABASE_URL:-placeholder}" \ && echo "=== FINAL DATABASE_URL ===" && echo "$DATABASE_URL" \ && export DEEPSEEK_API_KEY="${DEEPSEEK_API_KEY:-sk-build-placeholder}" \ && export OPENAI_API_KEY="${OPENAI_API_KEY:-sk-build-placeholder}" \ && npx prisma generate \ && DATABASE_URL="$DATABASE_URL" DEEPSEEK_API_KEY="$DEEPSEEK_API_KEY" OPENAI_API_KEY="$OPENAI_API_KEY" npm run build # ---- 阶段 3: 运行 ---- FROM node:20-alpine AS runner WORKDIR /app # runtime 基础包 RUN apk add --no-cache openssl tini curl bash # supercronic 用于 cron 容器调度(架构:alpine musl 兼容) # 直接 COPY 本地预下载的二进制(避免 Docker build 阶段从 GitHub 下载超时) # 下载源: https://github.com/aptible/supercronic/releases/tag/v0.2.33 # SHA1: 71b0d58cc53f6bd72cf2f293e09e294b79c666d8 ARG SUPERCRONIC=supercronic-linux-amd64 COPY --chown=root:root scripts/bin/${SUPERCRONIC} /usr/local/bin/${SUPERCRONIC} RUN chmod +x /usr/local/bin/${SUPERCRONIC} ENV NODE_ENV=production ENV NEXT_TELEMETRY_DISABLED=1 ENV PORT=8301 ENV HOSTNAME=0.0.0.0 # 非 root 运行 RUN addgroup -g 1001 -S nodejs && adduser -S nextjs -u 1001 # 拷贝必要文件 COPY --from=builder /app/public ./public COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next COPY --from=builder /app/node_modules ./node_modules # .prisma 目录交给 nextjs 用户(entrypoint 启动时会重新 prisma generate 同步 schema) RUN chown -R nextjs:nodejs /app/node_modules/.prisma COPY --from=builder /app/package.json ./package.json COPY --from=builder /app/prisma ./prisma COPY --from=builder /app/prisma.config.ts ./prisma.config.ts COPY --from=builder /app/scripts ./scripts # seed data files for volume auto-fill COPY --from=builder --chown=nextjs:nodejs /app/data ./data-seed # 配置/类型文件(必须存在;如果不存在 COPY 会失败但构建继续) COPY --from=builder /app/next.config.mjs ./next.config.mjs COPY --from=builder /app/postcss.config.mjs ./postcss.config.mjs COPY --from=builder /app/tsconfig.json ./tsconfig.json COPY --from=builder /app/components.json ./components.json # entrypoint 脚本 COPY --chown=root:root scripts/entrypoint-app.sh /usr/local/bin/entrypoint-app.sh COPY --chown=root:root scripts/entrypoint-cron.sh /usr/local/bin/entrypoint-cron.sh RUN chmod +x /usr/local/bin/entrypoint-app.sh /usr/local/bin/entrypoint-cron.sh # 数据 / 日志目录交给 nextjs RUN mkdir -p /app/data /app/public/bot-avatars /app/logs && chown -R nextjs:nodejs /app/data /app/public /app/logs # 健康检查 (用 curl 探活) HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ CMD curl -fs http://localhost:8301/api/health || exit 1 # 默认是 app 身份,cron 容器在 compose 里 override command USER nextjs EXPOSE 8301 ENTRYPOINT ["/sbin/tini", "--"] CMD ["/usr/local/bin/entrypoint-app.sh"]