1. 这不是“又一个CI/CD教程”而是Java工程师每天都在用的部署流水线你有没有过这样的经历改完一行代码手动打包、上传服务器、重启服务等了三分钟发现配置文件少了个分号或者团队里新人配Maven环境配到怀疑人生IDEA报错“java: you arent using a compiler supported by lombok”结果查了一下午才发现JDK版本和Lombok插件不匹配又或者上线前临时打个tag却忘了更新pom.xml里的版本号导致生产环境跑着一个连Git日志都找不到对应commit的jar包。这些不是小问题是每天在消耗Java工程师真实的时间、注意力和交付信心。这篇内容讲的就是怎么用GitHub Actions把这套重复、易错、依赖人肉记忆的操作变成一次git push就自动完成的确定性流程——它不是概念演示不是Hello World级别的玩具而是我过去三年在5个中型Java项目Spring Boot微服务、Quarkus后台、Gradle多模块工程、遗留Struts2系统容器化迁移、以及一个对接银行核心的批处理调度平台里反复打磨、踩坑、重构后沉淀下来的可直接抄作业的生产级CI/CD流水线。核心关键词就五个GitHub Actions、Java、CI/CD、部署、Maven——没有Docker、没有K8s、不碰云厂商控制台只聚焦在Java项目最原始也最关键的三个动作编译 → 测试 → 部署。它能跑在任何一台有Java环境的Linux服务器上甚至你本地Mac或Windows WSL里也能完整验证。如果你正在被mvn clean package之后那一连串手动操作折磨或者正为新同事入职三天还配不好Maven仓库而头疼那接下来的内容就是为你写的。2. 整体设计思路为什么是GitHub Actions Maven原生链路而不是Docker或云平台2.1 拒绝“为自动化而自动化”的陷阱很多教程一上来就堆砌Dockerfile、Kubernetes YAML、Helm Chart看起来很酷但对绝大多数Java项目来说这是典型的“杀鸡用牛刀”。我见过太多团队花两周时间写完一套K8s部署脚本结果发现线上服务器压根没装Docker运维说“容器化要走年度预算审批流程”最后还是回到scp systemctl restart的老路。GitHub Actions的价值从来不在它多炫技而在于它天然与代码仓库耦合、零额外基础设施依赖、权限模型清晰、调试路径极短。你改一行workflow YAMLgit push后立刻能看到执行日志失败时点开就能看到哪一行mvn test报了OutOfMemoryError而不是在K8s Pod日志里翻十分钟找OOMKilled事件。所以整个方案的设计锚点非常明确所有构建产物必须是标准Maven输出jar/war所有部署动作必须是纯SSH命令或SCP传输所有环境变量必须通过GitHub Secrets注入绝不引入任何需要额外维护的中间层。这意味着构建阶段完全复用本地开发环境你本地mvn clean compile能过Actions里就一定能过部署目标可以是任意Linux服务器只要能SSH登录、有Java运行时、有足够磁盘空间无需预装Docker、K3s、Ansible等任何工具调试成本趋近于零本地模拟mvn verify再对比Actions日志差异点一目了然提示这个设计不是技术保守而是对Java生态现实的尊重。Maven本身就是Java世界的“操作系统”它的生命周期管理依赖解析、插件执行、profile激活已经足够成熟。强行绕过它去搞容器化等于在Linux上写个Python脚本再去调用Shell来执行ls——不是不行但增加了不必要的抽象层和故障点。2.2 为什么坚持用Maven而非Gradle以及如何兼容Gradle项目热搜词里反复出现maven安装与配置、maven配置阿里云仓库、idea配置maven这说明什么说明Maven仍是Java企业级项目的事实标准。不是Gradle不好而是Maven的XML声明式配置、稳定的插件生态、以及与IDEA/IntelliJ的深度集成在大型团队协作中容错率更高。比如dependencyManagement统一版本、pluginManagement锁定插件版本、profiles按环境切换配置这些能力在Gradle里需要写Groovy或Kotlin DSL对新手更不友好。但现实是你手头可能真有个Gradle项目。别担心方案完全兼容——只需要两处微调Workflow文件里替换Maven命令把mvn clean verify换成./gradlew clean build --no-daemon加--no-daemon避免Gradle守护进程在CI环境中残留部署阶段定位构建产物Maven默认是target/*.jarGradle是build/libs/*.jar改下scp命令的源路径即可注意Gradle项目务必在根目录提交gradlew和gradlew.bat并确保gradle/wrapper/gradle-wrapper.jar已提交到仓库。我见过太多团队因为.gitignore误删wrapper导致CI直接失败这种低级错误比任何技术难题都更消耗团队士气。2.3 “部署”到底指什么我们定义清楚边界热搜词里“部署”和“CI/CD”高频共现但很多人混淆了概念。CI持续集成解决的是“代码合并是否安全”CD持续部署解决的是“代码能否自动到达生产环境”。而本方案聚焦的“部署”特指将经过测试验证的可执行jar包安全、可追溯、可回滚地发布到目标服务器并完成服务启停。它不包含不包含数据库迁移Flyway/Liquibase应作为应用启动时的内建逻辑而非CI步骤不包含Nginx反向代理配置更新那是基础设施即代码IaC的范畴应独立于应用CI不包含蓝绿发布/金丝雀发布中小项目用简单滚动重启健康检查已足够真正的部署价值在于原子性和可追溯性。所谓原子性是指一次部署要么全部成功jar包上传旧进程停止新进程启动健康检查通过要么全部失败保留旧版本继续提供服务所谓可追溯性是指每个生产环境的jar包都能通过其文件名中的Git commit hash或tag精准定位到代码仓库的某一行变更。这才是工程师敢半夜接告警电话的底气。3. 核心细节解析从workflow.yaml到服务器上的systemd服务3.1 GitHub Actions工作流文件.github/workflows/ci-cd.yml逐行拆解这是整个方案的“心脏”必须理解每一行背后的意图。以下是一个精简但生产可用的版本我将逐段解释name: Java CI/CD Pipeline on: push: branches: [main, develop] tags: [v*.*.*] pull_request: branches: [main, develop]name只是显示名称不影响执行on.push.branches定义触发分支main是生产发布分支develop是预发分支每次推送到这两个分支就触发构建on.push.tags是关键v*.*.*匹配语义化版本tag如v1.2.3这是发布正式版本的唯一入口。绝不允许直接向main分支push代码后自动上线必须打tag才触发部署——这是防止误操作的铁律pull_request仅触发CI编译测试不触发部署避免PR未合并就污染生产环境env: JAVA_VERSION: 17 MAVEN_VERSION: 3.9.6 MAVEN_OPTS: -Xmx2g -XX:MaxMetaspaceSize512mJAVA_VERSION指定Actions运行器使用的JDK版本。必须与项目pom.xml中java.version一致否则maven-compiler-plugin会报错MAVEN_VERSION指定Maven版本。建议固定版本如3.9.6避免因Actions默认Maven升级导致插件兼容性问题MAVEN_OPTS是重点-Xmx2g防止mvn clean package时OOM尤其多模块项目-XX:MaxMetaspaceSize512m限制元空间避免类加载过多导致GC风暴。这个参数是我在线上项目实测调整出来的比盲目设-Xmx4g更稳妥jobs: build-and-test: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 with: fetch-depth: 0runs-on: ubuntu-latest选择Ubuntu运行器稳定且社区支持好actions/checkoutv4拉取代码fetch-depth: 0是关键默认只拉最新一次commit但我们需要获取Git tag和历史commit信息用于生成版本号、计算diff必须全量拉取- name: Set up JDK ${{ env.JAVA_VERSION }} uses: actions/setup-javav4 with: java-version: ${{ env.JAVA_VERSION }} distribution: temurinsetup-javav4安装Temurin JDKOpenJDK官方构建版比Adoptium更可靠。distribution: temurin明确指定避免某些地区镜像源返回非标准JDK- name: Cache Maven dependencies uses: actions/cachev4 with: path: ~/.m2/repository key: ${{ runner.os }}-maven-${{ hashFiles(**/pom.xml) }} restore-keys: | ${{ runner.os }}-maven-缓存Maven本地仓库~/.m2/repositorykey基于pom.xml内容哈希只要依赖没变下次构建直接复用缓存节省3-5分钟。restore-keys提供模糊匹配当pom.xml变更时能降级恢复最近一次缓存- name: Build with Maven run: mvn -B clean verify -DskipTestsfalse -Pprod env: MAVEN_OPTS: ${{ env.MAVEN_OPTS }}mvn -B启用批处理模式禁用交互提示clean verify是黄金组合clean清除旧产物verify执行所有验证阶段包括test、integration-test、verify比单纯package更严格-DskipTestsfalse显式关闭跳过测试有些项目pom.xml里默认设了skipTeststrue必须覆盖-Pprod激活prodprofile用于加载生产环境配置如数据库连接池大小、Redis密码等- name: Upload artifact uses: actions/upload-artifactv4 with: name: java-app-jar path: target/*.jar将构建产物target/*.jar上传为Artifact供后续部署Job下载。注意Artifact有效期默认90天足够应对绝大多数回滚场景deploy-to-prod: needs: build-and-test if: startsWith(github.event.ref, refs/tags/v) runs-on: ubuntu-latest steps: - name: Download artifact uses: actions/download-artifactv4 with: name: java-app-jar path: ./targetneeds: build-and-test确保部署Job只在构建成功后执行if: startsWith(github.event.ref, refs/tags/v)是安全阀只有打tag如git tag v1.2.3 git push origin v1.2.3才触发部署push到main分支不会部署download-artifact从上一个Job下载jar包到本地./target目录- name: Deploy to Production Server uses: appleboy/scp-actionmaster with: host: ${{ secrets.HOST_PROD }} username: ${{ secrets.USERNAME_PROD }} key: ${{ secrets.PRIVATE_KEY_PROD }} source: target/*.jar target: /opt/myapp/releases/使用scp-action通过SSH密钥上传jar包。这里secrets.*是GitHub仓库Settings → Secrets and variables → Actions里预设的密钥source和target路径需严格匹配/opt/myapp/releases/是约定的发布目录所有jar包都放这里不覆盖旧版本- name: Remote deploy script uses: appleboy/ssh-actionmaster with: host: ${{ secrets.HOST_PROD }} username: ${{ secrets.USERNAME_PROD }} key: ${{ secrets.PRIVATE_KEY_PROD }} script: | cd /opt/myapp # 1. 停止当前服务 sudo systemctl stop myapp || true # 2. 创建软链接指向新jar包 JAR_NAME$(ls -t releases/*.jar | head -n1) ln -sf $JAR_NAME current.jar # 3. 启动服务 sudo systemctl start myapp # 4. 等待健康检查 timeout 60 bash -c while [[ $(curl -s -o /dev/null -w %{http_code} http://localhost:8080/actuator/health) ! 200 ]]; do sleep 5; donessh-action执行远程服务器上的部署脚本关键技巧用软链接current.jar解耦部署路径与服务路径。systemd服务文件里ExecStart指向/opt/myapp/current.jar每次部署只需更新软链接无需修改服务配置健康检查用curl轮询/actuator/health端点超时60秒。这是Spring Boot Actuator的标准端点若你的项目没集成Actuator需替换成自己的健康检查URL如/healthz3.2 服务器端systemd服务文件/etc/systemd/system/myapp.service详解这是让Java应用真正“服务化”的关键不是简单nohup java -jar xxx.jar 。以下是生产环境验证过的配置[Unit] DescriptionMy Java Application Afternetwork.target [Service] Typesimple Usermyapp Groupmyapp WorkingDirectory/opt/myapp ExecStart/usr/bin/java -Xms512m -Xmx2g -XX:UseG1GC -Dspring.profiles.activeprod -jar /opt/myapp/current.jar Restartalways RestartSec10 SuccessExitStatus143 EnvironmentJAVA_HOME/usr/lib/jvm/temurin-17-jdk-amd64 EnvironmentSPRING_CONFIG_LOCATIONfile:/opt/myapp/config/application-prod.yml [Install] WantedBymulti-user.targetTypesimple适用于前台运行的Java进程java -jar会阻塞终端User/Group必须创建专用用户sudo useradd -r -s /bin/false myapp禁止用root运行应用这是安全基线ExecStart参数详解-Xms512m -Xmx2g初始堆512MB最大堆2GB根据服务器内存调整建议设为总内存的50%-75%-XX:UseG1GCG1垃圾收集器适合大堆内存和低延迟要求-Dspring.profiles.activeprod激活生产Profile-jar /opt/myapp/current.jar永远指向软链接实现无缝切换Restartalways进程意外退出时自动重启RestartSec10重启间隔10秒避免频繁崩溃打满日志SuccessExitStatus143捕获SIGTERM15信号的优雅退出码让Spring Boot能执行PreDestroy钩子Environment设置JAVA_HOME和外部配置文件路径SPRING_CONFIG_LOCATION指向/opt/myapp/config/与代码内嵌配置分离实操心得第一次部署后务必执行sudo systemctl daemon-reload sudo systemctl enable myapp sudo systemctl start myapp。enable确保开机自启start立即启动。然后用sudo journalctl -u myapp -f实时查看日志这是排查启动失败的第一现场。3.3 GitHub Secrets安全配置与最佳实践所有敏感信息必须通过Secrets注入绝不能硬编码在YAML里。以下是必需的Secrets清单Secret Name用途说明安全建议HOST_PROD生产服务器IP或域名使用内网IP如10.0.1.100避免暴露公网USERNAME_PRODSSH登录用户名如myapp-deploy该用户应无sudo权限仅能执行systemctl相关命令PRIVATE_KEY_PRODSSH私钥内容需PEM格式以-----BEGIN OPENSSH PRIVATE KEY-----开头私钥必须用ssh-keygen -t ed25519 -C cimyapp生成禁用RSA密钥APP_DB_PASSWORD数据库密码若application-prod.yml中引用密码应含大小写字母数字符号长度≥12位注意PRIVATE_KEY_PROD的值是私钥文件的全部内容包括首尾的-----BEGIN...和-----END...行。复制时务必选中整块文本漏掉任何一行都会导致SSH认证失败。我曾因编辑器自动删除末尾换行符而调试两小时教训深刻。4. 实操过程从零开始搭建每一步都有截图级指引4.1 准备工作服务器环境初始化5分钟在目标服务器假设Ubuntu 22.04上执行以下命令这是所有Java部署的前提# 1. 创建专用用户和组 sudo useradd -r -s /bin/false myapp sudo mkdir -p /opt/myapp/{releases,config,logs} sudo chown -R myapp:myapp /opt/myapp sudo chmod 755 /opt/myapp # 2. 安装Temurin JDK 17官方推荐无License风险 wget https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.1%2B12/OpenJDK17U-jdk_x64_linux_hotspot_17.0.1_12.tar.gz sudo tar -zxf OpenJDK17U-jdk_x64_linux_hotspot_17.0.1_12.tar.gz -C /usr/lib/jvm/ sudo update-alternatives --install /usr/bin/java java /usr/lib/jvm/jdk-17.0.112/bin/java 1 sudo update-alternatives --config java # 选择JDK 17 # 3. 验证Java安装 java -version # 应输出 openjdk version 17.0.1 2021-10-19 # 4. 创建systemd服务文件粘贴上面的myapp.service内容 sudo tee /etc/systemd/system/myapp.service EOF [Unit] DescriptionMy Java Application Afternetwork.target [Service] Typesimple Usermyapp Groupmyapp WorkingDirectory/opt/myapp ExecStart/usr/bin/java -Xms512m -Xmx2g -XX:UseG1GC -Dspring.profiles.activeprod -jar /opt/myapp/current.jar Restartalways RestartSec10 SuccessExitStatus143 EnvironmentJAVA_HOME/usr/lib/jvm/jdk-17.0.112 EnvironmentSPRING_CONFIG_LOCATIONfile:/opt/myapp/config/application-prod.yml [Install] WantedBymulti-user.target EOF # 5. 重载systemd配置并启动 sudo systemctl daemon-reload sudo systemctl enable myapp sudo systemctl start myapp sudo systemctl status myapp # 检查是否active (running)提示如果systemctl status myapp显示failed第一件事是sudo journalctl -u myapp -n 50 --no-pager看最后50行日志。90%的问题是JAVA_HOME路径错误、current.jar不存在、或application-prod.yml里数据库密码为空。4.2 本地项目改造让Maven产出符合部署要求的jar包很多Java项目默认打包的jar无法直接运行需检查pom.xmlbuild plugins !-- 1. 必须使用spring-boot-maven-pluginSpring Boot项目 -- plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId configuration image builderpaketobuildpacks/builder-jammy-base:latest/builder /image /configuration /plugin !-- 2. 或使用maven-shade-plugin传统Java项目 -- plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-shade-plugin/artifactId version3.4.1/version executions execution phasepackage/phase goals goalshade/goal /goals configuration transformers transformer implementationorg.apache.maven.plugins.shade.resource.ManifestResourceTransformer mainClasscom.example.MyApplication/mainClass /transformer /transformers /configuration /execution /executions /plugin /plugins /buildSpring Boot项目spring-boot-maven-plugin是标配它会把所有依赖打包进fat jar并生成可执行的MANIFEST.MF传统Java项目用maven-shade-plugin关键是mainClass必须指定你的启动类全限定名验证本地执行mvn clean package后target/目录下应有且仅有一个jar包如myapp-1.0.0.jar且能直接运行java -jar target/myapp-1.0.0.jar常见问题java -jar xxx.jar报错no main manifest attribute。这是因为MANIFEST.MF里没写Main-Class。解决方案检查pom.xml中maven-shade-plugin的mainClass是否正确或确认Spring Boot项目是否误用了maven-jar-plugin。4.3 GitHub Actions工作流实战从编写到首次成功部署现在进入核心环节。在你的Java项目根目录创建.github/workflows/ci-cd.yml内容如下已整合前述所有要点name: Java CI/CD Pipeline on: push: branches: [main, develop] tags: [v*.*.*] pull_request: branches: [main, develop] env: JAVA_VERSION: 17 MAVEN_VERSION: 3.9.6 MAVEN_OPTS: -Xmx2g -XX:MaxMetaspaceSize512m jobs: build-and-test: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 with: fetch-depth: 0 - name: Set up JDK ${{ env.JAVA_VERSION }} uses: actions/setup-javav4 with: java-version: ${{ env.JAVA_VERSION }} distribution: temurin - name: Cache Maven dependencies uses: actions/cachev4 with: path: ~/.m2/repository key: ${{ runner.os }}-maven-${{ hashFiles(**/pom.xml) }} restore-keys: | ${{ runner.os }}-maven- - name: Build with Maven run: mvn -B clean verify -DskipTestsfalse -Pprod env: MAVEN_OPTS: ${{ env.MAVEN_OPTS }} - name: Upload artifact uses: actions/upload-artifactv4 with: name: java-app-jar path: target/*.jar deploy-to-prod: needs: build-and-test if: startsWith(github.event.ref, refs/tags/v) runs-on: ubuntu-latest steps: - name: Download artifact uses: actions/download-artifactv4 with: name: java-app-jar path: ./target - name: Deploy to Production Server uses: appleboy/scp-actionmaster with: host: ${{ secrets.HOST_PROD }} username: ${{ secrets.USERNAME_PROD }} key: ${{ secrets.PRIVATE_KEY_PROD }} source: target/*.jar target: /opt/myapp/releases/ - name: Remote deploy script uses: appleboy/ssh-actionmaster with: host: ${{ secrets.HOST_PROD }} username: ${{ secrets.USERNAME_PROD }} key: ${{ secrets.PRIVATE_KEY_PROD }} script: | cd /opt/myapp sudo systemctl stop myapp || true JAR_NAME$(ls -t releases/*.jar | head -n1) ln -sf $JAR_NAME current.jar sudo systemctl start myapp timeout 60 bash -c while [[ $(curl -s -o /dev/null -w %{http_code} http://localhost:8080/actuator/health) ! 200 ]]; do sleep 5; done然后按顺序执行在GitHub仓库Settings → Secrets and variables → Actions里添加四个SecretsHOST_PROD,USERNAME_PROD,PRIVATE_KEY_PROD,APP_DB_PASSWORD本地提交并推送git add .github/workflows/ci-cd.yml git commit -m chore: add GitHub Actions CI/CD workflow git push origin main此时build-and-testJob会触发等待约3分钟看到绿色勾✅表示构建成功打tag触发部署git tag v1.0.0 git push origin v1.0.0切换到GitHub Actions页面你会看到deploy-to-prodJob自动启动几秒后完成。此时登录服务器执行ls -l /opt/myapp/releases/ # 应看到类似 v1.0.0-abc123.jar 的文件 ls -l /opt/myapp/current.jar # 应指向刚上传的jar包 sudo systemctl status myapp # 应显示 active (running) curl http://localhost:8080/actuator/health # 应返回 {status:UP}实操心得首次部署失败90%概率在SSH密钥或路径权限。检查点①PRIVATE_KEY_PROD是否包含完整PEM头尾② 服务器上/opt/myapp/releases/目录是否属于myapp用户③ssh-action的username是否与服务器上创建的用户一致不是root。4.4 多环境支持如何扩展出develop预发环境生产环境有了预发环境develop同样重要。只需在workflow中增加一个Jobdeploy-to-develop: needs: build-and-test if: github.event_name push contains(github.event.head_commit.message, [deploy-dev]) runs-on: ubuntu-latest steps: - name: Download artifact uses: actions/download-artifactv4 with: name: java-app-jar path: ./target - name: Deploy to Develop Server uses: appleboy/scp-actionmaster with: host: ${{ secrets.HOST_DEV }} username: ${{ secrets.USERNAME_DEV }} key: ${{ secrets.PRIVATE_KEY_DEV }} source: target/*.jar target: /opt/myapp-dev/releases/ - name: Remote deploy script uses: appleboy/ssh-actionmaster with: host: ${{ secrets.HOST_DEV }} username: ${{ secrets.USERNAME_DEV }} key: ${{ secrets.PRIVATE_KEY_DEV }} script: | cd /opt/myapp-dev sudo systemctl stop myapp-dev || true JAR_NAME$(ls -t releases/*.jar | head -n1) ln -sf $JAR_NAME current.jar sudo systemctl start myapp-dev触发条件改为if: github.event_name push contains(github.event.head_commit.message, [deploy-dev])即只有commit message包含[deploy-dev]才部署到预发新增HOST_DEV,USERNAME_DEV,PRIVATE_KEY_DEV三个Secrets服务器端需创建/opt/myapp-dev/目录和对应的myapp-dev.service这样日常开发时你想部署到预发只需git commit -m [deploy-dev] feat: add user login validation git push origin develop既避免了预发环境被随意污染又给了开发快速验证的能力。5. 常见问题与排查技巧实录那些文档里不会写的坑5.1 Maven构建失败java: outofmemoryerror: insufficient memory这是Java CI中最经典的报错。表面看是内存不足但根因往往在配置。排查路径如下现象可能原因解决方案mvn clean compile失败MAVEN_OPTS未生效或-Xmx值过大导致系统内存不足在workflow中显式env: MAVEN_OPTS: ${{ env.MAVEN_OPTS }}将-Xmx2g改为-Xmx1gUbuntu运行器默认内存约7GBmvn test失败堆内存溢出测试用例本身内存泄漏或Mockito/PowerMock创建过多对象在pom.xml中为maven-surefire-plugin添加argLine-Xmx512m/argLine隔离测试JVM内存mvn verify失败Metaspace溢出项目模块过多或使用了大量注解处理器Lombok、MapStruct增加-XX:MaxMetaspaceSize512m并检查pom.xml中是否重复引入同一注解处理器我的实测数据一个12模块的Spring Boot项目在-Xmx2g -XX:MaxMetaspaceSize512m下稳定运行若开启Jacoco覆盖率报告需额外增加-XX:MaxMetaspaceSize1g。5.2 部署后服务启动失败Failed to start myapp.servicesystemctl status myapp显示failed但日志里只有Process exited with status 1。这是典型的“启动脚本静默失败”。按此顺序排查检查current.jar是否存在且可读ls -l /opt/myapp/current.jar # 应显示 - releases/myapp-1.0.0.jar file /opt/myapp/current.jar # 应显示 Java archive data (ZIP)手动执行ExecStart命令sudo -u myapp /usr/bin/java -Xms512m -Xmx2g -jar /opt/myapp/current.jar此时会直接输出Spring Boot启动日志错误一目了然如Caused by: java.lang.IllegalArgumentException: Could not resolve placeholder db.password说明application-prod.yml缺失检查SPRING_CONFIG_LOCATION路径sudo -u myapp ls -l /opt/myapp/config/application-prod.yml若文件不存在需提前在服务器上创建并确保myapp用户有读取权限检查端口占用sudo ss -tuln | grep :8080若被其他进程占用修改application-prod.yml中的server.port或杀掉冲突进程注意sudo -u myapp必须加上因为systemd服务是以myapp用户身份运行的权限和环境变量与root不同。跳过这步你看到的日志可能是误导性的。5.3 GitHub Actions执行超时The job running on ubuntu-latest has exceeded the maximum time of 3600 seconds免费版GitHub Actions单个Job最长60分钟。超时通常发生在测试用例执行过长在pom.xml中为maven-surefire-plugin添加超时plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-surefire-plugin/artifactId configuration forkCount1/forkCount reuseForksfalse/reuseForks argLine-Xmx1g/argLine testFailureIgnoretrue/testFailureIgnore trimStackTracefalse/trimStackTrace includes include**/*Test.java/include /includes excludes exclude**/integration/**/exclude /excludes !-- 关键设置单个测试类超时 -- parallelmethods/parallel threadCount2/threadCount perCoreThreadCounttrue/perCoreThreadCount forkedProcessTimeoutInSeconds300/forkedProcessTimeoutInSeconds /configuration /plugin依赖下载慢配置阿里云Maven镜像。在pom.xml中添加mirrors mirror idaliyunmaven/id