# 多阶段构建:前后端一体化部署(支持混合编译) # 构建参数:控制是否在 Docker 内编译 # - BUILD_IN_DOCKER=true (默认): Docker 内部编译(本地开发) # - BUILD_IN_DOCKER=false: 使用外部产物(GitHub Actions) ARG BUILD_IN_DOCKER=true # ==================== 阶段1:构建前端 ==================== FROM node:18-alpine AS frontend-build ARG BUILD_IN_DOCKER WORKDIR /app/frontend # 定义构建参数(版本号信息) ARG VERSION=dev ARG GIT_TAG= ARG GITHUB_REPO_URL=https://github.com/WrBug/PolyHermes # 设置环境变量(用于 Vite 构建时注入) ENV VERSION=${VERSION} ENV GIT_TAG=${GIT_TAG} ENV GITHUB_REPO_URL=${GITHUB_REPO_URL} # 复制前端文件(先复制 package.json 以利用 Docker 缓存) COPY frontend/package*.json ./ # 条件:仅在 Docker 内部编译时安装依赖 RUN if [ "$BUILD_IN_DOCKER" = "true" ]; then \ npm ci; \ fi # 复制所有前端源文件 COPY frontend/ ./ # 条件:仅在 Docker 内部编译时执行构建 # 如果 BUILD_IN_DOCKER=false,需要确保构建上下文中存在 frontend/dist # 注意:COPY frontend/ ./ 已经复制了整个 frontend 目录(包括 dist,如果存在) RUN if [ "$BUILD_IN_DOCKER" = "true" ]; then \ echo "🔨 Docker 内部编译前端..."; \ npm run build; \ else \ echo "⏭️ 使用外部产物..."; \ if [ ! -d "dist" ] || [ -z "$(ls -A dist 2>/dev/null)" ]; then \ echo "❌ 错误:BUILD_IN_DOCKER=false 但找不到外部产物 frontend/dist"; \ echo " 请先执行: cd frontend && npm install && npm run build"; \ exit 1; \ else \ echo "✅ 找到外部构建的前端产物"; \ fi; \ fi # ==================== 阶段2:构建后端 ==================== FROM gradle:8.5-jdk17 AS backend-build ARG BUILD_IN_DOCKER WORKDIR /app/backend # 复制 Gradle 配置文件 COPY backend/build.gradle.kts backend/settings.gradle.kts ./ COPY backend/gradle ./gradle # 条件:仅在 Docker 内部编译时下载依赖 RUN if [ "$BUILD_IN_DOCKER" = "true" ]; then \ gradle dependencies --no-daemon || true; \ fi # 复制源代码 COPY backend/src ./src # 尝试复制外部构建的 JAR(如果存在) # 注意:COPY 指令如果源不存在会失败 # GitHub Actions 使用 BUILD_IN_DOCKER=false,会先构建产物,所以 backend/build 应该存在 # 本地开发使用 BUILD_IN_DOCKER=true,会在 Docker 内编译,所以 backend/build 可能不存在 # 解决方案:先复制整个 backend 目录(包括 build,如果存在),然后只使用需要的部分 # 使用 .dockerignore 确保不会复制不需要的文件(如 .gradle、out、bin 等) COPY backend/build ./build-external # 处理外部构建的 JAR(如果存在) RUN if [ -d "build-external/libs" ] && [ -n "$(ls -A build-external/libs/*.jar 2>/dev/null)" ]; then \ echo "📦 找到外部构建的后端产物,复制到 build/libs..."; \ mkdir -p build/libs; \ cp build-external/libs/*.jar build/libs/; \ rm -rf build-external; \ else \ echo "⏭️ 未找到外部构建的 JAR,将在 Docker 内编译"; \ rm -rf build-external; \ mkdir -p build/libs; \ fi # 条件:仅在 Docker 内部编译时执行构建(会覆盖外部产物) RUN if [ "$BUILD_IN_DOCKER" = "true" ]; then \ echo "🔨 Docker 内部编译后端..."; \ gradle bootJar --no-daemon; \ else \ echo "⏭️ 使用外部产物"; \ if [ -z "$(ls -A build/libs/*.jar 2>/dev/null)" ]; then \ echo "❌ 错误:BUILD_IN_DOCKER=false 但找不到外部产物 backend/build/libs/*.jar"; \ echo " 请先执行: cd backend && ./gradlew bootJar"; \ exit 1; \ else \ echo "✅ 使用外部构建的后端产物"; \ fi; \ fi # ==================== 阶段3:运行环境 ==================== FROM eclipse-temurin:17-jre-jammy WORKDIR /app # 安装 Nginx、Python 和必要的工具 RUN apt-get update && \ apt-get install -y nginx curl tzdata jq python3 python3-flask python3-requests && \ rm -rf /var/lib/apt/lists/* && \ rm -rf /etc/nginx/sites-enabled/default # 从构建阶段复制文件 # 当 BUILD_IN_DOCKER=false 时,构建阶段已经复制了外部产物 COPY --from=frontend-build /app/frontend/dist /usr/share/nginx/html COPY --from=backend-build /app/backend/build/libs/*.jar app.jar # 复制 Nginx 配置 COPY docker/nginx.conf /etc/nginx/nginx.conf # 创建更新服务相关目录和脚本 RUN mkdir -p /app/updates /app/backups /var/log/polyhermes COPY docker/update-service.py /app/update-service.py COPY docker/start.sh /app/start.sh RUN chmod +x /app/start.sh # 记录初始版本(从构建参数) ARG VERSION=dev ARG GIT_TAG=dev RUN echo "{\"version\":\"${VERSION}\",\"tag\":\"${GIT_TAG}\",\"buildTime\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}" > /app/version.json # 创建非 root 用户 RUN useradd -m -u 1000 appuser # 设置目录权限 RUN mkdir -p /var/log/nginx /var/lib/nginx /var/cache/nginx /var/run && \ chown -R appuser:appuser /app && \ chown -R root:root /usr/share/nginx/html /var/log/nginx /var/lib/nginx /var/cache/nginx /etc/nginx /var/run # 暴露端口 EXPOSE 80 # 健康检查 HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \ CMD curl -f http://localhost/api/system/health || exit 1 # 启动服务 ENTRYPOINT ["/app/start.sh"]