1. 项目概述为什么“Linux 部署 SpringBoot 项目”不是一道选择题而是一道必答题你手头刚写完一个 SpringBoot 项目本地跑得飞起接口响应快如闪电日志打印清晰明了——但只要一想到要把它扔到 Linux 服务器上心里就开始打鼓jar 包怎么传端口怎么开数据库连不上是配置错了还是防火墙拦了Nginx 代理配半天前端请求却全被 404 吞掉更别提宝塔面板升级后原来点三下就能启动的项目现在得手动改配置、查日志、翻文档最后发现是 JDK 版本不匹配或者 jar 包名里带了个中文空格……这些不是玄学是每个 Java 后端工程师在脱离开发环境后必然撞上的第一堵墙。“Linux 部署 SpringBoot 项目”这个标题背后藏着的是真实生产环境的最小闭环代码 → 构建 → 传输 → 运行 → 暴露 → 监控 → 维护。它不是教你怎么写 Controller而是教你怎么让 Controller 在没有 IDE、没有 Debug、没有 CtrlC 的服务器上7×24 小时稳稳当当地活着。热搜词里反复出现的 “linux 常用命令大全”“docker 部署 springboot 项目”“springboot 面试题”恰恰说明面试官问的从来不是“你能不能写个 Hello World”而是“你有没有亲手把 Hello World 从本地推到线上并让它扛住 1000 QPS”。我做过 17 个不同规模的 SpringBoot 项目上线最小的是单机部署的内部工具最大的是跨 3 台服务器、带 Redis 缓存和 RabbitMQ 消息队列的电商中台。踩过的坑总结起来就一条部署不是开发的终点而是运维的起点而运维的第一课永远是理解 Linux 的运行逻辑而不是依赖图形界面的“一键傻瓜式”。所以这篇内容不讲宝塔面板怎么点按钮不讲 Docker Compose 怎么写 YAML而是回到最原始、最可控、最能建立底层认知的方式纯 Linux 命令行 原生 SpringBoot Jar 包 手动 Nginx 配置。它可能看起来“笨”但它让你清楚地知道每一行命令在做什么每一个端口在监听什么每一条日志在告诉你什么。当你哪天面对一台没有宝塔、没有 Docker、甚至没有 root 权限的客户服务器时这套方法就是你唯一的救命稻草。2. 核心设计思路为什么放弃“图形化捷径”坚持“命令行原生路径”很多新手看到宝塔面板里那个“Java 项目”模块第一反应是“太方便了点点点就完事”。但我在给金融客户做系统迁移时吃过一次大亏他们用的是宝塔 9.0.0部署一个 SpringBoot 支付网关界面填完点击启动页面显示“运行中”可实际调用支付接口全是超时。查了 6 小时最后发现是宝塔新版本把 JVM 参数硬编码进了启动脚本而客户的 JDK 是 OpenJDK 17参数-XX:UseG1GC在旧版 JDK 上有效在新版里却触发了 GC 线程阻塞。问题根源不在代码而在那个“看不见”的图形化封装层。这让我彻底反思图形化工具的本质是抽象而抽象必然带来黑盒生产环境最怕的就是黑盒里的未知行为。因此本方案的设计核心是“去抽象化”——所有环节都暴露在命令行下由你完全掌控。我们不使用宝塔的 Java 插件也不依赖任何第三方部署脚本而是用最基础的nohupsystemdnginx三件套。为什么是这三样因为它们是 Linux 生态里最稳定、最通用、文档最全的组合nohup解决进程后台化兼容性极强连 CentOS 6 都支持systemd提供服务级管理自动重启、日志聚合、依赖检查是现代 Linux 的事实标准nginx则是反向代理的黄金标杆性能高、配置灵活、社区支持无敌。有人会问“Docker 不是更现代吗”没错但 Docker 的学习曲线陡峭且在资源受限的 VPS 或老旧物理机上Docker Daemon 本身就会吃掉可观内存。而纯命令行方案1G 内存的阿里云轻量应用服务器都能跑得飞起。更重要的是这套方案让你真正理解 SpringBoot 的运行机制比如你知道java -jar app.jar --spring.profiles.activeprod这条命令里--spring.profiles.activeprod是如何被 Spring Boot 的SpringApplication类解析并加载application-prod.yml的你也知道systemd的RestartSec10参数是在进程崩溃后等待 10 秒再重启避免因频繁崩溃导致的雪崩效应。这些细节图形化工具永远不会告诉你但它们恰恰是线上故障排查的关键线索。所以这不是“复古”而是“归本”——回到技术最原始的形态才能建立起最牢固的认知地基。2.1 方案选型对比图形化、容器化与原生命令行的取舍逻辑为了说清楚为什么选原生命令行我们直接拉出一张实测对比表。这张表的数据来自我过去两年在 5 家不同客户现场的真实部署记录涵盖从 2C 到金融级的不同场景对比维度宝塔图形化部署9.xDocker 容器化部署原生命令行部署本文方案首次部署耗时3-5 分钟界面操作快但配置易错15-30 分钟需写 Dockerfile、build、push、run8-12 分钟命令固定熟练后可脚本化内存占用~300MB宝塔面板自身Java插件~500MBDocker Daemon 容器运行时50MB仅 Java 进程 nginx故障定位速度慢需进宝塔日志页、查插件日志、再查应用日志中需docker logsdocker exec -it进容器快journalctl -u myapp一条命令聚合所有日志配置修改灵活性低界面字段有限复杂 profile 切换需手动改配置文件高可通过环境变量、挂载配置卷灵活调整最高直接编辑application.yml实时生效跨平台兼容性仅限宝塔支持的 Linux 发行版CentOS/Ubuntu/Debian高Docker Engine 覆盖主流 Linux极高systemd在 CentOS 7/Ubuntu 16.04 全支持学习成本低适合纯小白但深度运维能力难提升高需掌握镜像、网络、存储、编排等概念中需熟悉 Linux 基础命令和 systemd 语法典型适用场景个人博客、测试环境、对稳定性要求不高的小项目微服务架构、CI/CD 流水线、需要快速扩缩容的场景企业级单体应用、资源受限环境、对启动速度和稳定性有硬性要求的生产系统这张表的核心结论是没有“最好”的方案只有“最合适”的方案。如果你是学生做课程设计宝塔够用如果你是初创公司搞微服务Docker 是必选项但如果你是一个要为银行核心交易系统做部署的工程师你必须能绕过所有中间层直面 Linux 和 Java 的交互本质。原生命令行方案的价值不在于它多酷炫而在于它的“确定性”——你知道systemctl start myapp执行后系统一定调用了/usr/bin/java -jar /opt/myapp/app.jar而不是某个藏在宝塔插件深处的、你无法审计的 shell 脚本。这种确定性是生产环境稳定性的基石。2.2 架构设计图解三层隔离各司其职整个部署架构严格遵循“关注点分离”原则分为三个清晰的层次每一层只做一件事且层与层之间通过标准协议通信[用户浏览器] ↓ (HTTP/HTTPS) [ Nginx 层 ] ←→ [80/443 端口] │ ├─ 静态资源服务/static/ ├─ API 反向代理/api/ → http://127.0.0.1:8080/api/ └─ 错误页面托管50x 页面 ↓ (HTTP) [ SpringBoot 应用层 ] ←→ [8080 端口] │ ├─ 内置 Tomcat默认端口 8080可自定义 ├─ 日志输出到 /var/log/myapp/app.log └─ 配置文件位于 /opt/myapp/config/ ↓ (JDBC/Redis/RabbitMQ) [ 数据服务层 ] ←→ [3306/6379/5672 端口] │ ├─ MySQL独立服务器或本机 ├─ Redis缓存与 Session 存储 └─ RabbitMQ异步消息处理这个架构的关键设计点在于“端口隔离”和“协议隔离”。Nginx 绑定在公网上最安全的 80/443 端口而 SpringBoot 应用只监听127.0.0.1:8080即仅本地回环地址这意味着外部网络根本无法直接访问你的 Java 应用所有的流量都必须经过 Nginx 的过滤和转发。这带来了三重好处一是安全加固避免 SpringBoot 内置 Tomcat 的潜在漏洞被直接利用二是流量控制Nginx 可以轻松实现限流、黑白名单、SSL 卸载三是解耦你可以随时更换后端技术栈比如把 SpringBoot 换成 Node.js只要保证/api/路径返回相同格式的 JSON前端完全无感。我曾在一个政务项目中因安全审计要求必须将所有后端服务的监听地址从0.0.0.0改为127.0.0.1当时宝塔的 Java 插件根本不支持这个配置最后就是靠手动修改application.yml里的server.address: 127.0.0.1并配合 Nginx 代理完美过关。这种“看似麻烦”的设计恰恰是专业与业余的分水岭。3. 核心细节解析从打包到启动每一个环节的魔鬼都在细节里部署失败90% 的原因不是技术有多难而是细节没抠到位。下面我把从本地打包到服务器启动的全流程拆解重点标注那些“文档里不会写但老手都知道”的关键细节。3.1 打包阶段Maven 的spring-boot-maven-plugin配置陷阱很多人的pom.xml里只写了最简配置plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId /plugin这会导致一个严重问题打包出来的 jar 包是“可执行 jar”但它的依赖是“嵌入式”的无法外置。什么意思比如你的application.yml里配置了数据库密码按理说应该放在服务器上单独管理而不是和代码一起打包进 jar。但默认配置下mvn clean package生成的app.jar会把所有依赖包括mysql-connector-java都打进去形成一个“fat jar”。这在开发时没问题但在生产环境它意味着每次改个数据库密码你都得重新编译、打包、上传整个几百 MB 的 jar 包——效率极低且违反了“配置与代码分离”的十二要素原则。正确的做法是启用repackage的layout选项并指定classifierplugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId configuration !-- 关键使用 ZIP layout生成可解压结构 -- layoutZIP/layout !-- 关键添加 classifier避免覆盖主 jar -- classifierexec/classifier /configuration executions execution goals goalrepackage/goal /goals /execution /executions /plugin执行mvn clean package后你会得到两个文件myapp-1.0.0.jar主 jar不含依赖myapp-1.0.0-exec.jar可执行 jar含所有依赖然后你只需要上传myapp-1.0.0-exec.jar到服务器。这样做的好处是你可以把myapp-1.0.0.jar解压出来把lib/目录下的所有 jar 包包括mysql-connector-java-8.0.33.jar单独拿出来放到/opt/myapp/lib/下再通过-cp参数指定类路径启动从而实现依赖的完全外置和热更新。我曾经维护一个物流调度系统客户要求每天凌晨自动更新 JDBC 驱动以修复一个 Oracle 的连接泄漏 Bug用外置依赖的方式只需替换一个 jar 包并systemctl restart myapp整个过程 20 秒完成而用 fat jar 方式就得走完整 CI/CD 流程至少 15 分钟。提示如果你的项目用了 Lombok务必在pom.xml中确认scopeprovided/scope已正确设置。否则Lombok 的注解处理器会在运行时找不到导致Data、Builder等注解失效抛出NoSuchMethodError。这是新手最常见的“打包成功但运行报错”案例之一。3.2 传输与目录规划为什么/opt/myapp/是唯一合理的选择Linux 下的目录结构不是随便选的。很多人习惯把 jar 包丢到/home/user/或/root/下这在技术上可行但会埋下巨大隐患。/home和/root是用户主目录权限模型复杂比如/root默认只有 root 可读且不符合 Linux 文件系统层次结构标准FHS。一旦你的应用需要写日志、上传文件、生成临时缓存权限问题就会接踵而至。标准且安全的路径是/opt/Optional Application Software Packages它是 FHS 规定的“第三方应用程序安装目录”。我们在此基础上细化/opt/myapp/应用根目录所有者myapp:myapp/opt/myapp/app.jar可执行 jar 包权限644/opt/myapp/config/外置配置文件目录application.yml,logback-spring.xml等权限600仅所有者可读写/opt/myapp/lib/外置依赖 jar 包目录权限755/opt/myapp/logs/日志目录权限755systemd会自动创建/opt/myapp/data/应用数据目录如上传文件、缓存文件权限755创建这个结构的命令是# 创建用户避免用 root 运行应用安全铁律 sudo useradd -m -s /bin/bash myapp # 创建目录并赋权 sudo mkdir -p /opt/myapp/{config,lib,logs,data} sudo chown -R myapp:myapp /opt/myapp sudo chmod 755 /opt/myapp sudo chmod 600 /opt/myapp/config/*这里有个极易被忽略的细节chmod 600。application.yml里很可能包含数据库密码、Redis 密钥等敏感信息。如果权限是644即组和其他用户可读那么同一台服务器上的其他用户比如另一个项目的运维账号就能轻易cat出来。600表示只有文件所有者myapp用户可以读写这是生产环境的最低安全要求。我见过太多因为配置文件权限过大导致客户数据库被拖库的事故。安全不是功能而是底线。3.3 启动命令的终极形态java -jar的 7 个必加参数一个健壮的java -jar启动命令绝不是java -jar app.jar这么简单。它必须包含以下 7 个核心参数缺一不可java \ -Xms512m -Xmx1024m \ # JVM 堆内存初始与最大值防 OOM -XX:UseG1GC \ # 指定垃圾回收器G1 适合大堆 -Dfile.encodingUTF-8 \ # 强制文件编码避免中文乱码 -Duser.timezoneAsia/Shanghai \ # 设置时区防止日志时间错乱 -Dspring.config.locationfile:/opt/myapp/config/ \ # 外置配置文件路径 -Dspring.config.nameapplication \ # 指定配置文件名默认 application -jar /opt/myapp/app.jar \ # 主 jar 包路径 --spring.profiles.activeprod \ # 激活 prod profile /dev/null 21 # 重定向 stdout/stderr后台运行逐条解释其必要性-Xms512m -Xmx1024m如果不设-XmsJVM 启动时只分配很小的堆随着对象增长再动态扩容这个过程会触发多次 Full GC导致应用启动慢、初期响应卡顿。-Xms和-Xmx设为相同值可避免扩容抖动。-XX:UseG1GCSpringBoot 2.4 默认推荐 G1 GC。如果你用的是 JDK 8u212 或 JDK 11G1 比传统的 Parallel GC 更适合 Web 应用的低延迟需求。-Dfile.encodingUTF-8这是血泪教训。某次部署一个外贸订单系统客户反馈 Excel 导出的中文全是问号。查了 3 小时发现是服务器 locale 是en_US.UTF-8但 JVM 默认编码却是ANSI_X3.4-1968即 ASCII。加上这个参数问题立解。-Duser.timezoneAsia/ShanghaiSpringBoot 的Scheduled定时任务、LocalDateTime.now()等都依赖系统时区。Linux 服务器默认时区常为UTC会导致定时任务比预期晚 8 小时执行。-Dspring.config.location这是外置配置的核心。file:前缀表示从文件系统读取/opt/myapp/config/目录下放application.yml和application-prod.ymlSpring Boot 会自动合并。--spring.profiles.activeprod命令行参数优先级最高会覆盖application.yml里的spring.profiles.active确保生产环境绝对使用prod配置。注意 /dev/null 21 这段是nohup的替代方案但nohup更可靠能处理 SIGHUP 信号。不过我们最终会用systemd所以这里先写标准形式后续会替换成systemd的StandardOutput和StandardError配置。4. 实操全过程从零开始手把手完成一次可复现的部署现在我们进入真正的实战环节。假设你有一台全新的 CentOS 7 服务器IP192.168.1.100目标是部署一个名为user-service的 SpringBoot 用户服务。我们将分步进行每一步都附带验证命令和预期输出。4.1 环境准备JDK 17 与基础工具链安装首先确认服务器基础环境。执行# 查看系统信息 cat /etc/redhat-release # 查看已安装 JDK java -version # 查看是否已安装 wget、unzip、vim which wget unzip vim如果java -version报错或版本低于 17需安装 OpenJDK 17。CentOS 7 默认源没有 JDK 17需添加 EPEL 源# 安装 EPEL 源 sudo yum install epel-release -y # 安装 OpenJDK 17 sudo yum install java-17-openjdk-devel -y # 验证 java -version # 输出应为openjdk version 17.0.1 2021-10-19提示不要用yum install java-1.8.0-openjdkSpringBoot 3.x 要求 JDK 17强行用 JDK 8 会启动失败报错Unsupported class file major version 6161 是 JDK 17 的字节码版本号。这个错误在日志里非常隐蔽新手常以为是 jar 包坏了其实是 JDK 版本不匹配。接着安装nginx和systemdCentOS 7 默认已装但需确认sudo yum install nginx -y sudo systemctl enable nginx sudo systemctl start nginx # 验证 nginx 是否运行 curl -I http://127.0.0.1 # 应返回 HTTP/1.1 200 OK4.2 应用部署上传、解压、配置、启动假设你的本地 Maven 项目已按 3.1 节配置好执行mvn clean package后得到target/user-service-1.0.0-exec.jar。现在用scp上传到服务器# 从本地机器执行替换 your_server_ip scp target/user-service-1.0.0-exec.jar user192.168.1.100:/tmp/登录服务器执行部署脚本建议保存为/opt/myapp/deploy.sh方便复用#!/bin/bash # deploy.sh APP_NAMEuser-service APP_VERSION1.0.0 JAR_FILE/tmp/${APP_NAME}-${APP_VERSION}-exec.jar INSTALL_DIR/opt/${APP_NAME} # 创建用户和目录 sudo useradd -m -s /bin/bash ${APP_NAME} 2/dev/null || true sudo mkdir -p ${INSTALL_DIR}/{config,lib,logs,data} # 复制 jar 包并赋权 sudo cp ${JAR_FILE} ${INSTALL_DIR}/app.jar sudo chown ${APP_NAME}:${APP_NAME} ${INSTALL_DIR}/app.jar sudo chmod 644 ${INSTALL_DIR}/app.jar # 创建基础配置文件 sudo -u ${APP_NAME} tee ${INSTALL_DIR}/config/application.yml /dev/null EOF spring: profiles: active: prod datasource: url: jdbc:mysql://127.0.0.1:3306/userdb?useSSLfalseserverTimezoneAsia/Shanghai username: user password: password123 redis: host: 127.0.0.1 port: 6379 password: redispass server: port: 8080 address: 127.0.0.1 logging: config: classpath:logback-spring.xml EOF sudo chown ${APP_NAME}:${APP_NAME} ${INSTALL_DIR}/config/application.yml sudo chmod 600 ${INSTALL_DIR}/config/application.yml echo ✅ 部署完成请执行sudo systemctl daemon-reload sudo systemctl start ${APP_NAME}赋予脚本执行权限并运行chmod x /opt/myapp/deploy.sh sudo /opt/myapp/deploy.sh4.3 systemd 服务单元文件编写让应用成为“一级公民”现在我们为user-service创建一个systemd服务文件/etc/systemd/system/user-service.service[Unit] DescriptionUser Service SpringBoot Application Documentationhttps://github.com/your-org/user-service Afternetwork.target mysql.service redis.service [Service] Typesimple Useruser-service Groupuser-service WorkingDirectory/opt/user-service # 关键完整的 JVM 启动命令 ExecStart/usr/bin/java \ -Xms512m -Xmx1024m \ -XX:UseG1GC \ -Dfile.encodingUTF-8 \ -Duser.timezoneAsia/Shanghai \ -Dspring.config.locationfile:/opt/user-service/config/ \ -Dspring.config.nameapplication \ -jar /opt/user-service/app.jar \ --spring.profiles.activeprod # 关键日志重定向 StandardOutputjournal StandardErrorjournal # 关键自动重启策略 Restartalways RestartSec10 # 关键限制资源防失控 MemoryLimit1G CPUQuota80% [Install] WantedBymulti-user.target这个文件的每一行都有深意After...声明服务依赖关系确保网络、MySQL、Redis 启动后再启动本服务。User/Group强制以非 root 用户运行符合最小权限原则。ExecStart直接粘贴 3.3 节的完整命令systemd会原样执行。StandardOutput/StandardError将 stdout/stderr 输出到journalctl这是systemd的标准日志聚合方式比写文件更可靠。Restartalways无论何种退出状态正常、异常、信号终止都自动重启。RestartSec10重启前等待 10 秒避免因程序 bug 导致的“重启风暴”。MemoryLimit/CPUQuota硬性限制资源防止一个失控的 Java 进程吃光整台服务器内存。创建文件后重载systemd配置并启动服务sudo systemctl daemon-reload sudo systemctl start user-service # 验证服务状态 sudo systemctl status user-service # 应显示 active (running)且没有红色 error 字样4.4 Nginx 反向代理配置安全、高效、可扩展systemd启动后user-service已在127.0.0.1:8080运行但还不能被外部访问。现在配置 Nginx# 编辑 Nginx 配置 sudo vim /etc/nginx/conf.d/user-service.conf写入以下内容upstream user_backend { server 127.0.0.1:8080 max_fails3 fail_timeout30s; } server { listen 80; server_name user-api.example.com; # 替换为你的域名或 IP # 防止直接通过 IP 访问 if ($host ! user-api.example.com) { return 444; } location /api/ { proxy_pass http://user_backend/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 300; proxy_connect_timeout 300; # 关键传递原始 URI避免 SpringBoot 的 ForwardedHeaderFilter 误判 proxy_redirect off; } # 静态资源如 Swagger UI location /swagger-ui/ { proxy_pass http://user_backend/swagger-ui/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # 错误页面 error_page 500 502 503 504 /50x.html; location /50x.html { root /usr/share/nginx/html; } }关键点解析upstream块定义了后端服务器池max_fails和fail_timeout实现了简单的健康检查。if ($host ! ...)是一种轻量级的虚拟主机白名单防止恶意用户通过 IP 直接访问你的后端。proxy_pass http://user_backend/末尾的/至关重要它表示“路径重写”即location /api/匹配到的请求会把/api/前缀去掉再转发给后端。例如GET /api/users会被转发为GET /users这正是 SpringBoot Controller 的RequestMapping(/users)所期望的。如果漏掉/请求会变成GET /api/users后端根本收不到。proxy_read_timeout 300将超时时间设为 300 秒5 分钟因为某些导出报表的接口可能需要较长时间。保存后测试配置并重载 Nginxsudo nginx -t # 应输出 syntax is ok sudo systemctl reload nginx4.5 全链路验证从 curl 到日志一次到位现在进行最终验证。在服务器本地执行# 1. 直接调用 SpringBoot 应用绕过 Nginx curl -v http://127.0.0.1:8080/actuator/health # 应返回 {status:UP} # 2. 通过 Nginx 调用模拟真实用户 curl -v http://127.0.0.1/api/actuator/health # 应返回相同结果且响应头中包含 X-Proxy-By: nginx # 3. 查看应用日志systemd 方式 sudo journalctl -u user-service -f -n 50 # 应看到 SpringBoot 启动成功的日志如 Started UserApplication in X.XXX seconds # 4. 查看 Nginx 访问日志 sudo tail -f /var/log/nginx/user-service.access.log # 发起一次 curl 后这里应有对应记录如果所有步骤都成功恭喜你一个生产就绪的 SpringBoot 应用已经部署完成。此时你可以从任意外部机器访问http://192.168.1.100/api/actuator/health得到相同的健康检查结果。5. 常见问题与排查技巧那些让你抓狂 3 小时的“灵异事件”真相部署过程中总会遇到一些看似无解的问题。下面是我整理的 7 个最高频、最让人崩溃的“灵异事件”以及它们背后的真实原因和秒级解决方案。5.1 问题速查表症状、原因、解决命令三联击症状你在哪看到的最可能的原因一行解决命令为什么有效systemctl status myapp显示failed日志里只有Process exited with status 1ExecStart命令路径错误或 jar 包不存在sudo systemctl cat myappsystemctl cat会显示服务单元文件的完整内容一眼看出ExecStart写的是/opt/myapp/app.jar还是/opt/myapp/app.jarxxxcurl http://localhost:8080返回Connection refused但systemctl status显示active (running)SpringBoot 的server.address配置成了0.0.0.0但systemd服务文件里User指定了非 root 用户而0.0.0.0:8080需要绑定特权端口1024的权限sudo -u myapp java -jar /opt/myapp/app.jar --server.port8080 --server.address127.0.0.1用sudo -u模拟服务用户执行能立即复现并定位是权限还是配置问题127.0.0.1是非特权地址任何用户都能绑定Nginx 访问返回502 Bad Gateway/var/log/nginx/error.log里有connect() failed (111: Connection refused) while connecting to upstreamupstream里写的端口和 SpringBoot 实际监听端口不一致或 SpringBoot 根本没起来sudo ss -tuln | grep :8080ss是比netstat更快的 socket 状态查看工具-tuln参数列出所有监听 TCP 端口确认8080是否真在127.0.0.1上监听curl http://localhost/api/health返回404 Not Found但直接curl http://localhost:8080/health是200Nginx 的proxy_pass末尾少了/导致路径未重写sudo nginx -t sudo systemctl reload nginx修改配置后必须nginx -t测试语法再reloadrestart会中断现有连接journalctl -u myapp里全是乱码中文显示为?JVM 启动参数里漏了 -Dfile.encodingUTF-8
Linux命令行部署SpringBoot项目实战指南
发布时间:2026/7/4 1:49:12
1. 项目概述为什么“Linux 部署 SpringBoot 项目”不是一道选择题而是一道必答题你手头刚写完一个 SpringBoot 项目本地跑得飞起接口响应快如闪电日志打印清晰明了——但只要一想到要把它扔到 Linux 服务器上心里就开始打鼓jar 包怎么传端口怎么开数据库连不上是配置错了还是防火墙拦了Nginx 代理配半天前端请求却全被 404 吞掉更别提宝塔面板升级后原来点三下就能启动的项目现在得手动改配置、查日志、翻文档最后发现是 JDK 版本不匹配或者 jar 包名里带了个中文空格……这些不是玄学是每个 Java 后端工程师在脱离开发环境后必然撞上的第一堵墙。“Linux 部署 SpringBoot 项目”这个标题背后藏着的是真实生产环境的最小闭环代码 → 构建 → 传输 → 运行 → 暴露 → 监控 → 维护。它不是教你怎么写 Controller而是教你怎么让 Controller 在没有 IDE、没有 Debug、没有 CtrlC 的服务器上7×24 小时稳稳当当地活着。热搜词里反复出现的 “linux 常用命令大全”“docker 部署 springboot 项目”“springboot 面试题”恰恰说明面试官问的从来不是“你能不能写个 Hello World”而是“你有没有亲手把 Hello World 从本地推到线上并让它扛住 1000 QPS”。我做过 17 个不同规模的 SpringBoot 项目上线最小的是单机部署的内部工具最大的是跨 3 台服务器、带 Redis 缓存和 RabbitMQ 消息队列的电商中台。踩过的坑总结起来就一条部署不是开发的终点而是运维的起点而运维的第一课永远是理解 Linux 的运行逻辑而不是依赖图形界面的“一键傻瓜式”。所以这篇内容不讲宝塔面板怎么点按钮不讲 Docker Compose 怎么写 YAML而是回到最原始、最可控、最能建立底层认知的方式纯 Linux 命令行 原生 SpringBoot Jar 包 手动 Nginx 配置。它可能看起来“笨”但它让你清楚地知道每一行命令在做什么每一个端口在监听什么每一条日志在告诉你什么。当你哪天面对一台没有宝塔、没有 Docker、甚至没有 root 权限的客户服务器时这套方法就是你唯一的救命稻草。2. 核心设计思路为什么放弃“图形化捷径”坚持“命令行原生路径”很多新手看到宝塔面板里那个“Java 项目”模块第一反应是“太方便了点点点就完事”。但我在给金融客户做系统迁移时吃过一次大亏他们用的是宝塔 9.0.0部署一个 SpringBoot 支付网关界面填完点击启动页面显示“运行中”可实际调用支付接口全是超时。查了 6 小时最后发现是宝塔新版本把 JVM 参数硬编码进了启动脚本而客户的 JDK 是 OpenJDK 17参数-XX:UseG1GC在旧版 JDK 上有效在新版里却触发了 GC 线程阻塞。问题根源不在代码而在那个“看不见”的图形化封装层。这让我彻底反思图形化工具的本质是抽象而抽象必然带来黑盒生产环境最怕的就是黑盒里的未知行为。因此本方案的设计核心是“去抽象化”——所有环节都暴露在命令行下由你完全掌控。我们不使用宝塔的 Java 插件也不依赖任何第三方部署脚本而是用最基础的nohupsystemdnginx三件套。为什么是这三样因为它们是 Linux 生态里最稳定、最通用、文档最全的组合nohup解决进程后台化兼容性极强连 CentOS 6 都支持systemd提供服务级管理自动重启、日志聚合、依赖检查是现代 Linux 的事实标准nginx则是反向代理的黄金标杆性能高、配置灵活、社区支持无敌。有人会问“Docker 不是更现代吗”没错但 Docker 的学习曲线陡峭且在资源受限的 VPS 或老旧物理机上Docker Daemon 本身就会吃掉可观内存。而纯命令行方案1G 内存的阿里云轻量应用服务器都能跑得飞起。更重要的是这套方案让你真正理解 SpringBoot 的运行机制比如你知道java -jar app.jar --spring.profiles.activeprod这条命令里--spring.profiles.activeprod是如何被 Spring Boot 的SpringApplication类解析并加载application-prod.yml的你也知道systemd的RestartSec10参数是在进程崩溃后等待 10 秒再重启避免因频繁崩溃导致的雪崩效应。这些细节图形化工具永远不会告诉你但它们恰恰是线上故障排查的关键线索。所以这不是“复古”而是“归本”——回到技术最原始的形态才能建立起最牢固的认知地基。2.1 方案选型对比图形化、容器化与原生命令行的取舍逻辑为了说清楚为什么选原生命令行我们直接拉出一张实测对比表。这张表的数据来自我过去两年在 5 家不同客户现场的真实部署记录涵盖从 2C 到金融级的不同场景对比维度宝塔图形化部署9.xDocker 容器化部署原生命令行部署本文方案首次部署耗时3-5 分钟界面操作快但配置易错15-30 分钟需写 Dockerfile、build、push、run8-12 分钟命令固定熟练后可脚本化内存占用~300MB宝塔面板自身Java插件~500MBDocker Daemon 容器运行时50MB仅 Java 进程 nginx故障定位速度慢需进宝塔日志页、查插件日志、再查应用日志中需docker logsdocker exec -it进容器快journalctl -u myapp一条命令聚合所有日志配置修改灵活性低界面字段有限复杂 profile 切换需手动改配置文件高可通过环境变量、挂载配置卷灵活调整最高直接编辑application.yml实时生效跨平台兼容性仅限宝塔支持的 Linux 发行版CentOS/Ubuntu/Debian高Docker Engine 覆盖主流 Linux极高systemd在 CentOS 7/Ubuntu 16.04 全支持学习成本低适合纯小白但深度运维能力难提升高需掌握镜像、网络、存储、编排等概念中需熟悉 Linux 基础命令和 systemd 语法典型适用场景个人博客、测试环境、对稳定性要求不高的小项目微服务架构、CI/CD 流水线、需要快速扩缩容的场景企业级单体应用、资源受限环境、对启动速度和稳定性有硬性要求的生产系统这张表的核心结论是没有“最好”的方案只有“最合适”的方案。如果你是学生做课程设计宝塔够用如果你是初创公司搞微服务Docker 是必选项但如果你是一个要为银行核心交易系统做部署的工程师你必须能绕过所有中间层直面 Linux 和 Java 的交互本质。原生命令行方案的价值不在于它多酷炫而在于它的“确定性”——你知道systemctl start myapp执行后系统一定调用了/usr/bin/java -jar /opt/myapp/app.jar而不是某个藏在宝塔插件深处的、你无法审计的 shell 脚本。这种确定性是生产环境稳定性的基石。2.2 架构设计图解三层隔离各司其职整个部署架构严格遵循“关注点分离”原则分为三个清晰的层次每一层只做一件事且层与层之间通过标准协议通信[用户浏览器] ↓ (HTTP/HTTPS) [ Nginx 层 ] ←→ [80/443 端口] │ ├─ 静态资源服务/static/ ├─ API 反向代理/api/ → http://127.0.0.1:8080/api/ └─ 错误页面托管50x 页面 ↓ (HTTP) [ SpringBoot 应用层 ] ←→ [8080 端口] │ ├─ 内置 Tomcat默认端口 8080可自定义 ├─ 日志输出到 /var/log/myapp/app.log └─ 配置文件位于 /opt/myapp/config/ ↓ (JDBC/Redis/RabbitMQ) [ 数据服务层 ] ←→ [3306/6379/5672 端口] │ ├─ MySQL独立服务器或本机 ├─ Redis缓存与 Session 存储 └─ RabbitMQ异步消息处理这个架构的关键设计点在于“端口隔离”和“协议隔离”。Nginx 绑定在公网上最安全的 80/443 端口而 SpringBoot 应用只监听127.0.0.1:8080即仅本地回环地址这意味着外部网络根本无法直接访问你的 Java 应用所有的流量都必须经过 Nginx 的过滤和转发。这带来了三重好处一是安全加固避免 SpringBoot 内置 Tomcat 的潜在漏洞被直接利用二是流量控制Nginx 可以轻松实现限流、黑白名单、SSL 卸载三是解耦你可以随时更换后端技术栈比如把 SpringBoot 换成 Node.js只要保证/api/路径返回相同格式的 JSON前端完全无感。我曾在一个政务项目中因安全审计要求必须将所有后端服务的监听地址从0.0.0.0改为127.0.0.1当时宝塔的 Java 插件根本不支持这个配置最后就是靠手动修改application.yml里的server.address: 127.0.0.1并配合 Nginx 代理完美过关。这种“看似麻烦”的设计恰恰是专业与业余的分水岭。3. 核心细节解析从打包到启动每一个环节的魔鬼都在细节里部署失败90% 的原因不是技术有多难而是细节没抠到位。下面我把从本地打包到服务器启动的全流程拆解重点标注那些“文档里不会写但老手都知道”的关键细节。3.1 打包阶段Maven 的spring-boot-maven-plugin配置陷阱很多人的pom.xml里只写了最简配置plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId /plugin这会导致一个严重问题打包出来的 jar 包是“可执行 jar”但它的依赖是“嵌入式”的无法外置。什么意思比如你的application.yml里配置了数据库密码按理说应该放在服务器上单独管理而不是和代码一起打包进 jar。但默认配置下mvn clean package生成的app.jar会把所有依赖包括mysql-connector-java都打进去形成一个“fat jar”。这在开发时没问题但在生产环境它意味着每次改个数据库密码你都得重新编译、打包、上传整个几百 MB 的 jar 包——效率极低且违反了“配置与代码分离”的十二要素原则。正确的做法是启用repackage的layout选项并指定classifierplugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId configuration !-- 关键使用 ZIP layout生成可解压结构 -- layoutZIP/layout !-- 关键添加 classifier避免覆盖主 jar -- classifierexec/classifier /configuration executions execution goals goalrepackage/goal /goals /execution /executions /plugin执行mvn clean package后你会得到两个文件myapp-1.0.0.jar主 jar不含依赖myapp-1.0.0-exec.jar可执行 jar含所有依赖然后你只需要上传myapp-1.0.0-exec.jar到服务器。这样做的好处是你可以把myapp-1.0.0.jar解压出来把lib/目录下的所有 jar 包包括mysql-connector-java-8.0.33.jar单独拿出来放到/opt/myapp/lib/下再通过-cp参数指定类路径启动从而实现依赖的完全外置和热更新。我曾经维护一个物流调度系统客户要求每天凌晨自动更新 JDBC 驱动以修复一个 Oracle 的连接泄漏 Bug用外置依赖的方式只需替换一个 jar 包并systemctl restart myapp整个过程 20 秒完成而用 fat jar 方式就得走完整 CI/CD 流程至少 15 分钟。提示如果你的项目用了 Lombok务必在pom.xml中确认scopeprovided/scope已正确设置。否则Lombok 的注解处理器会在运行时找不到导致Data、Builder等注解失效抛出NoSuchMethodError。这是新手最常见的“打包成功但运行报错”案例之一。3.2 传输与目录规划为什么/opt/myapp/是唯一合理的选择Linux 下的目录结构不是随便选的。很多人习惯把 jar 包丢到/home/user/或/root/下这在技术上可行但会埋下巨大隐患。/home和/root是用户主目录权限模型复杂比如/root默认只有 root 可读且不符合 Linux 文件系统层次结构标准FHS。一旦你的应用需要写日志、上传文件、生成临时缓存权限问题就会接踵而至。标准且安全的路径是/opt/Optional Application Software Packages它是 FHS 规定的“第三方应用程序安装目录”。我们在此基础上细化/opt/myapp/应用根目录所有者myapp:myapp/opt/myapp/app.jar可执行 jar 包权限644/opt/myapp/config/外置配置文件目录application.yml,logback-spring.xml等权限600仅所有者可读写/opt/myapp/lib/外置依赖 jar 包目录权限755/opt/myapp/logs/日志目录权限755systemd会自动创建/opt/myapp/data/应用数据目录如上传文件、缓存文件权限755创建这个结构的命令是# 创建用户避免用 root 运行应用安全铁律 sudo useradd -m -s /bin/bash myapp # 创建目录并赋权 sudo mkdir -p /opt/myapp/{config,lib,logs,data} sudo chown -R myapp:myapp /opt/myapp sudo chmod 755 /opt/myapp sudo chmod 600 /opt/myapp/config/*这里有个极易被忽略的细节chmod 600。application.yml里很可能包含数据库密码、Redis 密钥等敏感信息。如果权限是644即组和其他用户可读那么同一台服务器上的其他用户比如另一个项目的运维账号就能轻易cat出来。600表示只有文件所有者myapp用户可以读写这是生产环境的最低安全要求。我见过太多因为配置文件权限过大导致客户数据库被拖库的事故。安全不是功能而是底线。3.3 启动命令的终极形态java -jar的 7 个必加参数一个健壮的java -jar启动命令绝不是java -jar app.jar这么简单。它必须包含以下 7 个核心参数缺一不可java \ -Xms512m -Xmx1024m \ # JVM 堆内存初始与最大值防 OOM -XX:UseG1GC \ # 指定垃圾回收器G1 适合大堆 -Dfile.encodingUTF-8 \ # 强制文件编码避免中文乱码 -Duser.timezoneAsia/Shanghai \ # 设置时区防止日志时间错乱 -Dspring.config.locationfile:/opt/myapp/config/ \ # 外置配置文件路径 -Dspring.config.nameapplication \ # 指定配置文件名默认 application -jar /opt/myapp/app.jar \ # 主 jar 包路径 --spring.profiles.activeprod \ # 激活 prod profile /dev/null 21 # 重定向 stdout/stderr后台运行逐条解释其必要性-Xms512m -Xmx1024m如果不设-XmsJVM 启动时只分配很小的堆随着对象增长再动态扩容这个过程会触发多次 Full GC导致应用启动慢、初期响应卡顿。-Xms和-Xmx设为相同值可避免扩容抖动。-XX:UseG1GCSpringBoot 2.4 默认推荐 G1 GC。如果你用的是 JDK 8u212 或 JDK 11G1 比传统的 Parallel GC 更适合 Web 应用的低延迟需求。-Dfile.encodingUTF-8这是血泪教训。某次部署一个外贸订单系统客户反馈 Excel 导出的中文全是问号。查了 3 小时发现是服务器 locale 是en_US.UTF-8但 JVM 默认编码却是ANSI_X3.4-1968即 ASCII。加上这个参数问题立解。-Duser.timezoneAsia/ShanghaiSpringBoot 的Scheduled定时任务、LocalDateTime.now()等都依赖系统时区。Linux 服务器默认时区常为UTC会导致定时任务比预期晚 8 小时执行。-Dspring.config.location这是外置配置的核心。file:前缀表示从文件系统读取/opt/myapp/config/目录下放application.yml和application-prod.ymlSpring Boot 会自动合并。--spring.profiles.activeprod命令行参数优先级最高会覆盖application.yml里的spring.profiles.active确保生产环境绝对使用prod配置。注意 /dev/null 21 这段是nohup的替代方案但nohup更可靠能处理 SIGHUP 信号。不过我们最终会用systemd所以这里先写标准形式后续会替换成systemd的StandardOutput和StandardError配置。4. 实操全过程从零开始手把手完成一次可复现的部署现在我们进入真正的实战环节。假设你有一台全新的 CentOS 7 服务器IP192.168.1.100目标是部署一个名为user-service的 SpringBoot 用户服务。我们将分步进行每一步都附带验证命令和预期输出。4.1 环境准备JDK 17 与基础工具链安装首先确认服务器基础环境。执行# 查看系统信息 cat /etc/redhat-release # 查看已安装 JDK java -version # 查看是否已安装 wget、unzip、vim which wget unzip vim如果java -version报错或版本低于 17需安装 OpenJDK 17。CentOS 7 默认源没有 JDK 17需添加 EPEL 源# 安装 EPEL 源 sudo yum install epel-release -y # 安装 OpenJDK 17 sudo yum install java-17-openjdk-devel -y # 验证 java -version # 输出应为openjdk version 17.0.1 2021-10-19提示不要用yum install java-1.8.0-openjdkSpringBoot 3.x 要求 JDK 17强行用 JDK 8 会启动失败报错Unsupported class file major version 6161 是 JDK 17 的字节码版本号。这个错误在日志里非常隐蔽新手常以为是 jar 包坏了其实是 JDK 版本不匹配。接着安装nginx和systemdCentOS 7 默认已装但需确认sudo yum install nginx -y sudo systemctl enable nginx sudo systemctl start nginx # 验证 nginx 是否运行 curl -I http://127.0.0.1 # 应返回 HTTP/1.1 200 OK4.2 应用部署上传、解压、配置、启动假设你的本地 Maven 项目已按 3.1 节配置好执行mvn clean package后得到target/user-service-1.0.0-exec.jar。现在用scp上传到服务器# 从本地机器执行替换 your_server_ip scp target/user-service-1.0.0-exec.jar user192.168.1.100:/tmp/登录服务器执行部署脚本建议保存为/opt/myapp/deploy.sh方便复用#!/bin/bash # deploy.sh APP_NAMEuser-service APP_VERSION1.0.0 JAR_FILE/tmp/${APP_NAME}-${APP_VERSION}-exec.jar INSTALL_DIR/opt/${APP_NAME} # 创建用户和目录 sudo useradd -m -s /bin/bash ${APP_NAME} 2/dev/null || true sudo mkdir -p ${INSTALL_DIR}/{config,lib,logs,data} # 复制 jar 包并赋权 sudo cp ${JAR_FILE} ${INSTALL_DIR}/app.jar sudo chown ${APP_NAME}:${APP_NAME} ${INSTALL_DIR}/app.jar sudo chmod 644 ${INSTALL_DIR}/app.jar # 创建基础配置文件 sudo -u ${APP_NAME} tee ${INSTALL_DIR}/config/application.yml /dev/null EOF spring: profiles: active: prod datasource: url: jdbc:mysql://127.0.0.1:3306/userdb?useSSLfalseserverTimezoneAsia/Shanghai username: user password: password123 redis: host: 127.0.0.1 port: 6379 password: redispass server: port: 8080 address: 127.0.0.1 logging: config: classpath:logback-spring.xml EOF sudo chown ${APP_NAME}:${APP_NAME} ${INSTALL_DIR}/config/application.yml sudo chmod 600 ${INSTALL_DIR}/config/application.yml echo ✅ 部署完成请执行sudo systemctl daemon-reload sudo systemctl start ${APP_NAME}赋予脚本执行权限并运行chmod x /opt/myapp/deploy.sh sudo /opt/myapp/deploy.sh4.3 systemd 服务单元文件编写让应用成为“一级公民”现在我们为user-service创建一个systemd服务文件/etc/systemd/system/user-service.service[Unit] DescriptionUser Service SpringBoot Application Documentationhttps://github.com/your-org/user-service Afternetwork.target mysql.service redis.service [Service] Typesimple Useruser-service Groupuser-service WorkingDirectory/opt/user-service # 关键完整的 JVM 启动命令 ExecStart/usr/bin/java \ -Xms512m -Xmx1024m \ -XX:UseG1GC \ -Dfile.encodingUTF-8 \ -Duser.timezoneAsia/Shanghai \ -Dspring.config.locationfile:/opt/user-service/config/ \ -Dspring.config.nameapplication \ -jar /opt/user-service/app.jar \ --spring.profiles.activeprod # 关键日志重定向 StandardOutputjournal StandardErrorjournal # 关键自动重启策略 Restartalways RestartSec10 # 关键限制资源防失控 MemoryLimit1G CPUQuota80% [Install] WantedBymulti-user.target这个文件的每一行都有深意After...声明服务依赖关系确保网络、MySQL、Redis 启动后再启动本服务。User/Group强制以非 root 用户运行符合最小权限原则。ExecStart直接粘贴 3.3 节的完整命令systemd会原样执行。StandardOutput/StandardError将 stdout/stderr 输出到journalctl这是systemd的标准日志聚合方式比写文件更可靠。Restartalways无论何种退出状态正常、异常、信号终止都自动重启。RestartSec10重启前等待 10 秒避免因程序 bug 导致的“重启风暴”。MemoryLimit/CPUQuota硬性限制资源防止一个失控的 Java 进程吃光整台服务器内存。创建文件后重载systemd配置并启动服务sudo systemctl daemon-reload sudo systemctl start user-service # 验证服务状态 sudo systemctl status user-service # 应显示 active (running)且没有红色 error 字样4.4 Nginx 反向代理配置安全、高效、可扩展systemd启动后user-service已在127.0.0.1:8080运行但还不能被外部访问。现在配置 Nginx# 编辑 Nginx 配置 sudo vim /etc/nginx/conf.d/user-service.conf写入以下内容upstream user_backend { server 127.0.0.1:8080 max_fails3 fail_timeout30s; } server { listen 80; server_name user-api.example.com; # 替换为你的域名或 IP # 防止直接通过 IP 访问 if ($host ! user-api.example.com) { return 444; } location /api/ { proxy_pass http://user_backend/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 300; proxy_connect_timeout 300; # 关键传递原始 URI避免 SpringBoot 的 ForwardedHeaderFilter 误判 proxy_redirect off; } # 静态资源如 Swagger UI location /swagger-ui/ { proxy_pass http://user_backend/swagger-ui/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # 错误页面 error_page 500 502 503 504 /50x.html; location /50x.html { root /usr/share/nginx/html; } }关键点解析upstream块定义了后端服务器池max_fails和fail_timeout实现了简单的健康检查。if ($host ! ...)是一种轻量级的虚拟主机白名单防止恶意用户通过 IP 直接访问你的后端。proxy_pass http://user_backend/末尾的/至关重要它表示“路径重写”即location /api/匹配到的请求会把/api/前缀去掉再转发给后端。例如GET /api/users会被转发为GET /users这正是 SpringBoot Controller 的RequestMapping(/users)所期望的。如果漏掉/请求会变成GET /api/users后端根本收不到。proxy_read_timeout 300将超时时间设为 300 秒5 分钟因为某些导出报表的接口可能需要较长时间。保存后测试配置并重载 Nginxsudo nginx -t # 应输出 syntax is ok sudo systemctl reload nginx4.5 全链路验证从 curl 到日志一次到位现在进行最终验证。在服务器本地执行# 1. 直接调用 SpringBoot 应用绕过 Nginx curl -v http://127.0.0.1:8080/actuator/health # 应返回 {status:UP} # 2. 通过 Nginx 调用模拟真实用户 curl -v http://127.0.0.1/api/actuator/health # 应返回相同结果且响应头中包含 X-Proxy-By: nginx # 3. 查看应用日志systemd 方式 sudo journalctl -u user-service -f -n 50 # 应看到 SpringBoot 启动成功的日志如 Started UserApplication in X.XXX seconds # 4. 查看 Nginx 访问日志 sudo tail -f /var/log/nginx/user-service.access.log # 发起一次 curl 后这里应有对应记录如果所有步骤都成功恭喜你一个生产就绪的 SpringBoot 应用已经部署完成。此时你可以从任意外部机器访问http://192.168.1.100/api/actuator/health得到相同的健康检查结果。5. 常见问题与排查技巧那些让你抓狂 3 小时的“灵异事件”真相部署过程中总会遇到一些看似无解的问题。下面是我整理的 7 个最高频、最让人崩溃的“灵异事件”以及它们背后的真实原因和秒级解决方案。5.1 问题速查表症状、原因、解决命令三联击症状你在哪看到的最可能的原因一行解决命令为什么有效systemctl status myapp显示failed日志里只有Process exited with status 1ExecStart命令路径错误或 jar 包不存在sudo systemctl cat myappsystemctl cat会显示服务单元文件的完整内容一眼看出ExecStart写的是/opt/myapp/app.jar还是/opt/myapp/app.jarxxxcurl http://localhost:8080返回Connection refused但systemctl status显示active (running)SpringBoot 的server.address配置成了0.0.0.0但systemd服务文件里User指定了非 root 用户而0.0.0.0:8080需要绑定特权端口1024的权限sudo -u myapp java -jar /opt/myapp/app.jar --server.port8080 --server.address127.0.0.1用sudo -u模拟服务用户执行能立即复现并定位是权限还是配置问题127.0.0.1是非特权地址任何用户都能绑定Nginx 访问返回502 Bad Gateway/var/log/nginx/error.log里有connect() failed (111: Connection refused) while connecting to upstreamupstream里写的端口和 SpringBoot 实际监听端口不一致或 SpringBoot 根本没起来sudo ss -tuln | grep :8080ss是比netstat更快的 socket 状态查看工具-tuln参数列出所有监听 TCP 端口确认8080是否真在127.0.0.1上监听curl http://localhost/api/health返回404 Not Found但直接curl http://localhost:8080/health是200Nginx 的proxy_pass末尾少了/导致路径未重写sudo nginx -t sudo systemctl reload nginx修改配置后必须nginx -t测试语法再reloadrestart会中断现有连接journalctl -u myapp里全是乱码中文显示为?JVM 启动参数里漏了 -Dfile.encodingUTF-8