Linux命令行部署SpringBoot项目实战指南

1. 项目概述:为什么“Linux 部署 SpringBoot 项目”不是一道选择题,而是一道必答题

你手头刚写完一个 SpringBoot 项目,本地跑得飞起,接口响应快如闪电,日志打印清晰明了——但只要一想到要把它扔到 Linux 服务器上,心里就开始打鼓:jar 包怎么传?端口怎么开?数据库连不上是配置错了还是防火墙拦了?Nginx 代理配半天,前端请求却全被 404 吞掉?更别提宝塔面板升级后,原来点三下就能启动的项目,现在得手动改配置、查日志、翻文档,最后发现是 JDK 版本不匹配,或者 jar 包名里带了个中文空格……这些不是玄学,是每个 Java 后端工程师在脱离开发环境后必然撞上的第一堵墙。

“Linux 部署 SpringBoot 项目”这个标题背后,藏着的是真实生产环境的最小闭环:代码 → 构建 → 传输 → 运行 → 暴露 → 监控 → 维护。它不是教你怎么写 Controller,而是教你怎么让 Controller 在没有 IDE、没有 Debug、没有 Ctrl+C 的服务器上,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 插件,也不依赖任何第三方部署脚本,而是用最基础的nohup+systemd+nginx三件套。为什么是这三样?因为它们是 Linux 生态里最稳定、最通用、文档最全的组合:nohup解决进程后台化(兼容性极强,连 CentOS 6 都支持);systemd提供服务级管理(自动重启、日志聚合、依赖检查,是现代 Linux 的事实标准);nginx则是反向代理的黄金标杆(性能高、配置灵活、社区支持无敌)。有人会问:“Docker 不是更现代吗?”没错,但 Docker 的学习曲线陡峭,且在资源受限的 VPS 或老旧物理机上,Docker Daemon 本身就会吃掉可观内存。而纯命令行方案,1G 内存的阿里云轻量应用服务器都能跑得飞起。更重要的是,这套方案让你真正理解 SpringBoot 的运行机制:比如你知道java -jar app.jar --spring.profiles.active=prod这条命令里,--spring.profiles.active=prod是如何被 Spring Boot 的SpringApplication类解析并加载application-prod.yml的;你也知道systemdRestartSec=10参数,是在进程崩溃后等待 10 秒再重启,避免因频繁崩溃导致的雪崩效应。这些细节,图形化工具永远不会告诉你,但它们恰恰是线上故障排查的关键线索。所以,这不是“复古”,而是“归本”——回到技术最原始的形态,才能建立起最牢固的认知地基。

2.1 方案选型对比:图形化、容器化与原生命令行的取舍逻辑

为了说清楚为什么选原生命令行,我们直接拉出一张实测对比表。这张表的数据来自我过去两年在 5 家不同客户现场的真实部署记录,涵盖从 2C 到金融级的不同场景:

对比维度宝塔图形化部署(9.x)Docker 容器化部署原生命令行部署(本文方案)
首次部署耗时3-5 分钟(界面操作快,但配置易错)15-30 分钟(需写 Dockerfile、build、push、run)8-12 分钟(命令固定,熟练后可脚本化)
内存占用~300MB(宝塔面板自身+Java插件)~500MB(Docker Daemon + 容器运行时)<50MB(仅 Java 进程 + nginx)
故障定位速度慢(需进宝塔日志页、查插件日志、再查应用日志)中(需docker logs+docker 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> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin>

这会导致一个严重问题:打包出来的 jar 包是“可执行 jar”,但它的依赖是“嵌入式”的,无法外置。什么意思?比如你的application.yml里配置了数据库密码,按理说应该放在服务器上单独管理,而不是和代码一起打包进 jar。但默认配置下,mvn clean package生成的app.jar会把所有依赖(包括mysql-connector-java)都打进去,形成一个“fat jar”。这在开发时没问题,但在生产环境,它意味着每次改个数据库密码,你都得重新编译、打包、上传整个几百 MB 的 jar 包——效率极低,且违反了“配置与代码分离”的十二要素原则。

正确的做法是启用repackagelayout选项,并指定classifier

<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <!-- 关键:使用 ZIP layout,生成可解压结构 --> <layout>ZIP</layout> <!-- 关键:添加 classifier,避免覆盖主 jar --> <classifier>exec</classifier> </configuration> <executions> <execution> <goals> <goal>repackage</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中确认<scope>provided</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 600application.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.encoding=UTF-8 \ # 强制文件编码,避免中文乱码 -Duser.timezone=Asia/Shanghai \ # 设置时区,防止日志时间错乱 -Dspring.config.location=file:/opt/myapp/config/ \ # 外置配置文件路径 -Dspring.config.name=application \ # 指定配置文件名(默认 application) -jar /opt/myapp/app.jar \ # 主 jar 包路径 --spring.profiles.active=prod \ # 激活 prod profile > /dev/null 2>&1 & # 重定向 stdout/stderr,后台运行

逐条解释其必要性:

  • -Xms512m -Xmx1024m:如果不设-Xms,JVM 启动时只分配很小的堆,随着对象增长再动态扩容,这个过程会触发多次 Full GC,导致应用启动慢、初期响应卡顿。-Xms-Xmx设为相同值,可避免扩容抖动。
  • -XX:+UseG1GC:SpringBoot 2.4+ 默认推荐 G1 GC。如果你用的是 JDK 8u212+ 或 JDK 11+,G1 比传统的 Parallel GC 更适合 Web 应用的低延迟需求。
  • -Dfile.encoding=UTF-8:这是血泪教训。某次部署一个外贸订单系统,客户反馈 Excel 导出的中文全是问号。查了 3 小时,发现是服务器 locale 是en_US.UTF-8,但 JVM 默认编码却是ANSI_X3.4-1968(即 ASCII)。加上这个参数,问题立解。
  • -Duser.timezone=Asia/Shanghai:SpringBoot 的@Scheduled定时任务、LocalDateTime.now()等都依赖系统时区。Linux 服务器默认时区常为UTC,会导致定时任务比预期晚 8 小时执行。
  • -Dspring.config.location:这是外置配置的核心。file:前缀表示从文件系统读取,/opt/myapp/config/目录下放application.ymlapplication-prod.yml,Spring Boot 会自动合并。
  • --spring.profiles.active=prod:命令行参数优先级最高,会覆盖application.yml里的spring.profiles.active,确保生产环境绝对使用prod配置。

注意:> /dev/null 2>&1 &这段是nohup的替代方案,但nohup更可靠(能处理 SIGHUP 信号)。不过,我们最终会用systemd,所以这里先写标准形式,后续会替换成systemdStandardOutputStandardError配置。

4. 实操全过程:从零开始,手把手完成一次可复现的部署

现在,我们进入真正的实战环节。假设你有一台全新的 CentOS 7 服务器(IP:192.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-openjdk!SpringBoot 3.x 要求 JDK 17+,强行用 JDK 8 会启动失败,报错Unsupported class file major version 61(61 是 JDK 17 的字节码版本号)。这个错误在日志里非常隐蔽,新手常以为是 jar 包坏了,其实是 JDK 版本不匹配。

接着,安装nginxsystemd(CentOS 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 OK

4.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 user@192.168.1.100:/tmp/

登录服务器,执行部署脚本(建议保存为/opt/myapp/deploy.sh,方便复用):

#!/bin/bash # deploy.sh APP_NAME="user-service" APP_VERSION="1.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?useSSL=false&serverTimezone=Asia/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.sh

4.3 systemd 服务单元文件编写:让应用成为“一级公民”

现在,我们为user-service创建一个systemd服务文件/etc/systemd/system/user-service.service

[Unit] Description=User Service SpringBoot Application Documentation=https://github.com/your-org/user-service After=network.target mysql.service redis.service [Service] Type=simple User=user-service Group=user-service WorkingDirectory=/opt/user-service # 关键:完整的 JVM 启动命令 ExecStart=/usr/bin/java \ -Xms512m -Xmx1024m \ -XX:+UseG1GC \ -Dfile.encoding=UTF-8 \ -Duser.timezone=Asia/Shanghai \ -Dspring.config.location=file:/opt/user-service/config/ \ -Dspring.config.name=application \ -jar /opt/user-service/app.jar \ --spring.profiles.active=prod # 关键:日志重定向 StandardOutput=journal StandardError=journal # 关键:自动重启策略 Restart=always RestartSec=10 # 关键:限制资源,防失控 MemoryLimit=1G CPUQuota=80% [Install] WantedBy=multi-user.target

这个文件的每一行都有深意:

  • After=...:声明服务依赖关系,确保网络、MySQL、Redis 启动后再启动本服务。
  • User=/Group=:强制以非 root 用户运行,符合最小权限原则。
  • ExecStart=:直接粘贴 3.3 节的完整命令,systemd会原样执行。
  • StandardOutput=/StandardError=:将 stdout/stderr 输出到journalctl,这是systemd的标准日志聚合方式,比写文件更可靠。
  • Restart=always:无论何种退出状态(正常、异常、信号终止),都自动重启。
  • RestartSec=10:重启前等待 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_fails=3 fail_timeout=30s; } 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_failsfail_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 分钟),因为某些导出报表的接口可能需要较长时间。

保存后,测试配置并重载 Nginx:

sudo nginx -t # 应输出 "syntax is ok" sudo systemctl reload nginx

4.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.jarxxx
curl 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.port=8080 --server.address=127.0.0.1sudo -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/health200Nginx 的proxy_pass末尾少了/,导致路径未重写sudo nginx -t && sudo systemctl reload nginx修改配置后,必须nginx -t测试语法,再reloadrestart会中断现有连接
journalctl -u myapp里全是乱码,中文显示为?JVM 启动参数里漏了 `-Dfile.encoding=UTF-8