本地 IDE 里点一下 Run,三秒启动,你以为部署 JavaWeb 项目就是把打包好的文件丢到服务器上再运行一遍。直到第一次面对 Linux 服务器,你才会发现:JDK 版本、数据库连接、防火墙、日志路径、进程管理,每一个环节都能让你卡上半天。这篇博文就围绕 Linux 部署 JavaWeb 项目这条主线,从环境准备、JDK 安装、数据库对齐、打包上传、启动验证到问题排查,把每一步为什么这么做、做完该看到什么现象、出了问题去哪儿查,完整地过一遍。适合第一次接触服务器部署的 Java 开发,也适合那些本地跑得很顺、一上服务器就各种翻车的同学对照排查。
1. 动手之前:把版本、打包方式、服务器配置这三件事想清楚
很多部署翻车不是因为操作复杂,而是因为一开始没想清楚项目到底是什么形态、要跑在什么环境上。我建议不管多急,动手前先花十分钟确认三件事。
1.1 jar 还是 war:这决定了你的部署方式差了一整套
Spring Boot 2.x/3.x 项目默认打成可执行 jar 包,里面内嵌了 Tomcat,所以部署时只需要 JDK 环境,java -jar一条命令就能跑起来。传统 SSM 或者 SSH 结构的项目,通常打成 war 包,需要额外安装一个外置 Tomcat,把 war 丢到webapps目录下,由 Tomcat 负责解压和启动。
怎么判断你的项目属于哪一种?打开pom.xml,看<packaging>标签:
- 值是
jar:走java -jar路线。 - 值是
war:需要装 Tomcat,把 war 扔进webapps。
还有一类特殊情况:项目本身是 war 包,但里面又加了 Spring Boot 的启动类,这种也能用外置 Tomcat 跑。判断标准其实很简单——看你本地是怎么启动的。如果本地用Application.java的 main 方法启动,那多半是 jar 型;如果本地用 IDEA 配置了 Tomcat 来启动,那就是 war 型。部署方式和本地启动方式保持一致,基本不会错。
1.2 JDK 版本对齐:本地 8、服务器 11,不一定会报错,但我建议你保持一致
Java 编译出的 class 文件是有版本号的,低版本 JDK 跑高版本编译出来的 class,会直接抛UnsupportedClassVersionError。举个实际例子:本地用 JDK 11 编译,服务器装的是 JDK 8,一启动就报错,日志里会出现UnsupportedClassVersionError: xxx has been compiled by a more recent version of the Java Runtime。反过来,服务器 JDK 版本比本地高,一般能跑,但也不建议跨太多版本。
我的习惯是:看pom.xml里的<java.version>配置,服务器装同版本或者更高一个小版本。比如项目写的是 1.8,服务器装 OpenJDK 8;项目要求 11 或 17,就装对应的 11 或 17。切忌本地 JDK 8、服务器 JDK 17 这种组合,虽然大概率没事,但一旦用到一些依赖内部做了版本判断,排查起来非常痛苦。
1.3 服务器基本配置检查:内存和磁盘至少要心里有数
部署前登录服务器,先跑三个命令:
free -h # 看内存 df -h # 看磁盘 nproc # 看 CPU 核数为什么强调这个?因为 Spring Boot 默认堆内存大小是根据物理内存自动算的,默认取物理内存的四分之一。如果服务器只有 1G 内存,JVM 会默认分配 256M 堆,加上各种开销,再跑个 MySQL,很容易直接把机器压死。这类机器我建议启动时主动限制堆内存,比如-Xmx256m -Xms128m,别让 JVM 放开手脚吃内存。
磁盘也一样。日志文件如果不做轮转,可能几天就攒好几个 G。至少看一眼根目录剩余空间够不够,不要等部署完跑两天才发现磁盘写满。
2. JDK 安装:别被各种教程带偏,我推荐最省心的一种
JDK 安装方法网上五花八门,有下载 tar.gz 手动解压的,有配 Oracle 源装的,还有各种一键脚本。对于绝大多数部署场景,我推荐直接用 Linux 自带的包管理器安装,省事、稳定、卸载也干净。
2.1 两种主流方式的选择逻辑
包管理器安装(apt/yum)适合大多数场景,缺点是自带的版本可能不是最新,但部署用完全够了。tar.gz 解压安装适合两种情况:一是服务器是内网机器,无法访问外网软件源;二是你需要精确控制 JDK 的安装目录。如果你只是要把 JavaWeb 项目跑起来,无脑选包管理器。
2.2 Ubuntu/Debian 系安装 OpenJDK 11
登录服务器后执行:
sudo apt update sudo apt install -y openjdk-11-jdk java -version正常情况下你会看到类似这样的输出:
openjdk version "11.0.20" 2023-07-18 OpenJDK Runtime Environment (build 11.0.20+8-post-Ubuntu-1ubuntu1) OpenJDK 64-Bit Server VM (build 11.0.20+8-post-Ubuntu-1ubuntu1, mixed mode, sharing)注意看,java -version能跑说明 JRE 正常,但你还需要确认javac也在。有些镜像默认只装了 JRE,javac是不存在的,后面 Maven 打包会报错。所以装完顺手执行javac -version,如果提示找不到命令,说明装的是精简 JRE,需要另外安装openjdk-11-jdk的完整版。
2.3 CentOS/RHEL 7/8 系安装 OpenJDK 8
CentOS 7 默认 yum 源里可以直接装:
sudo yum install -y java-1.8.0-openjdk-devel java -version javac -version这里特别提一下,CentOS 下要装带-devel后缀的包,因为不带-devel的java-1.8.0-openjdk只有运行环境,没有 javac。很多教程只写java-1.8.0-openjdk,跟风装完才发现编译不了。
2.4 JAVA_HOME 建议顺手配置
有人觉得不配 JAVA_HOME 也能跑 jar,这话没毛病。但很多工具链,比如 Maven、Jenkins、以及一些项目自带的启动脚本,会显式读取JAVA_HOME环境变量。不配的话,后面大概率会在奇怪的地方踩坑。所以建议一次配好。
先找到 JDK 安装的真实路径:
readlink -f $(which java)比如得到/usr/lib/jvm/java-11-openjdk-amd64/bin/java,那么 JAVA_HOME 就是/usr/lib/jvm/java-11-openjdk-amd64。然后编辑/etc/profile,在文件末尾加上:
export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64 export PATH=$JAVA_HOME/bin:$PATH保存后执行source /etc/profile,再用echo $JAVA_HOME验证。配完之后当前会话和以后新开的会话都能直接用。
2.5 校验:java 和 javac 双确认
部署环境装完 JDK,我习惯把java -version和javac -version都跑一遍,确认两个版本一致。如果 java 是 11、javac 是 8,说明环境变量被改乱了,后面编译和运行各用各的版本,会埋下大坑。
3. 数据库这关最容易翻车:本地能连不等于服务器能连
我见过太多项目卡在这一步:jar 起来了、端口也通了,但页面上登录就报错,或者服务启动过程中直接失败。原因基本都是数据库连接不上。本地开发时用的是localhost,数据库密码、用户名都对,换到服务器上就全变了。
3.1 服务器上的 MySQL:用户、库、权限一次配齐
如果服务器的 MySQL 还没装,先装好并启动服务。用 root 登录后,我建议不要直接拿 root 给项目用,而是创建一个专用账号,这样以后出问题容易定位,权限也好控制。
CREATE USER 'app_user'@'%' IDENTIFIED BY 'YourStrongPass'; GRANT ALL PRIVILEGES ON your_db.* TO 'app_user'@'%'; FLUSH PRIVILEGES;'%'表示允许从任意主机登录,这是最宽松的写法。如果项目部署在同一台服务器上,更稳妥的写法是用'app_user'@'127.0.0.1'或内网 IP,只允许这一台机器连。这样即使数据库端口暴露到外网,账号也无法从别的机器登录。
另外,确认你项目的数据库名和这个库名完全一致,别出现本地库名叫demo_db、服务器上建的是demo这种低级错误。
3.2 配置文件里的连接地址要改
项目里的application.yml或者application.properties,默认大概率是:
spring: datasource: url: jdbc:mysql://localhost:3306/demo_db如果项目部署在与 MySQL 相同的服务器上,localhost其实也能用,问题不大。但如果 MySQL 在另一台机器,这里必须改成那台机器的 IP,公网 IP 或内网 IP 都行,取决于两台机器是否在同一个内网。
我踩过的典型坑是:本地 MySQL 用户里就一个 root 绑定了 localhost,服务器上项目用localhost连倒是连上了,但密码和本地不同,于是怎么都报Access denied。最后创建了一个专用账号,问题立刻解决。
3.3 serverTimezone 和 SSL:两个隐蔽但高频的坑
MySQL 驱动连接串里有几个参数,本地不需要,服务器上却经常必须加。我现在的标准写法是:
jdbc:mysql://127.0.0.1:3306/demo_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=trueserverTimezone=Asia/Shanghai是为了解决驱动与数据库时区不一致导致的The server time zone value ... is unrecognized报错。useSSL=false是关闭 SSL 校验,本地一般没问题,但服务器上如果 MySQL 开启了强制 SSL,或者驱动版本比较新,可能握手失败。allowPublicKeyRetrieval=true是配合 MySQL 8 的 caching_sha2_password 认证插件用的,不加偶尔会报错。
注意:如果项目用的老版本 mysql-connector-java 5.x,连接 MySQL 8 时密码认证很可能失败,因为 MySQL 8 默认的缓存认证方式和老驱动不兼容。要么升级驱动,要么在创建用户时指定
IDENTIFIED WITH mysql_native_password BY '...'。
3.4 防火墙和安全组:两层都要检查
数据库连不上,排除了账号密码和 URL 问题之后,就要查网络层面。Linux 自带的防火墙是第一层:
Ubuntu 上:
sudo ufw status sudo ufw allow 3306/tcpCentOS 7 上:
sudo firewall-cmd --list-all sudo firewall-cmd --permanent --add-port=3306/tcp sudo firewall-cmd --reload如果你用的是云服务器,还要登录云控制台,看安全组/防火墙规则里入方向是否放行了 3306 端口。这个特别容易漏,很多人只改了系统防火墙,忘了云控制台那边还有一道门。判断方法是:在服务器本机用mysql -h127.0.0.1 -uapp_user -p能连上,但在你自己电脑上用客户端连不上,基本就是安全组没放行。
4. 打包与上传:jar 包从本地 IDE 到服务器目录的完整路径
环境都准备好之后,接下来就是把项目从开发机搬到服务器。这一步看起来没什么技术含量,但目录规划、文件权限这些细节,做得不讲究会留下隐患。
4.1 Maven 打包,为什么我习惯加 -DskipTests
在项目根目录执行:
mvn clean package -DskipTests-DskipTests的意思是跳过测试用例的执行,但照样编译测试代码。部署时跳过测试主要是为了节省时间,避免因为某个无关测试失败导致打包中断。如果你有那种必须通过的冒烟测试,就别跳,按需取舍。
打包完成后,产物在target目录下,Spring Boot 项目通常叫xxx-0.0.1-SNAPSHOT.jar。先看一眼大小:
ls -lh target/*.jar如果 jar 只有十几 KB,基本说明没打成功,多半是打包成了普通 jar 而不是可执行 jar,检查一下 pom 里有没有配置 Spring Boot 的打包插件。
4.2 上传方式:scp 和 rz 选顺手的
最简单的上传方式是在本机终端用 scp:
scp target/demo-0.0.1-SNAPSHOT.jar root@服务器IP:/opt/apps/如果服务器不允许 root 远程登录,就用你自己的用户名。另一种方式是 Xshell、FinalShell 之类的 SSH 工具,在命令行敲rz回车,会弹出文件选择框。rz方式对不太熟悉命令行的人更友好,唯一的坑是上传大文件时容易中断,建议大 jar 包用 scp。上传完之后ls -lh看一眼远端文件大小,与本地对比一致才算传成功,这个习惯能帮你省很多无谓的排查时间。
4.3 目录规划:别把东西丢在 /root 和 /
我推荐的目录结构是:
/opt/apps ├── demo.jar └── logs # 运行日志目录创建并授权:
sudo mkdir -p /opt/apps/logs sudo chown -R $USER:$USER /opt/apps为什么不用/root?一是 root 家目录权限默认是 700,换了用户名启动时读不了;二是家目录容易被误清理。为什么不直接丢/?太乱了,项目一多就分不清谁是谁。统一放在/opt/apps或/usr/local/project,以后维护、备份都方便。
4.4 文件权限:jar 不需要可执行,目录权限别太随意
java -jar运行 jar 包时,只需要 JVM 有权限读取文件即可,jar 本身不需要+x。但所在目录需要让运行项目的用户有读写权限,否则日志文件写不进去。我习惯给目录755,jar 文件644,日志目录755。这样既能让部署账号正常读写,又不会让无关用户随便改文件。
5. 启动与验证:先前台跑一遍,再谈后台运行和开机自启
启动这一步,不少人的习惯是直接nohup ... &,跑完才发现日志里一片红色报错。我更推荐三步走:前台启动看现象,没问题再改后台,最后用 systemd 管起来。
5.1 第一步永远是前台启动
进入 jar 所在目录,直接运行:
cd /opt/apps java -jar demo-0.0.1-SNAPSHOT.jar此时日志会直接输出到终端。正常情况下,你会在最后几行看到类似这样的内容:
Tomcat started on port(s): 8080 (http) with context path '' Started Application in 8.5 seconds (process running for 9.1)看到Started Application才说明启动成功。这一步的价值在于,如果配置有问题,异常堆栈会直接打在屏幕上,不用去翻文件。确认没问题之后按Ctrl+C停掉,再进入下一步。千万不要省掉这一步,直接后台启动,否则错误被吞进日志文件,排查效率低很多。
5.2 正式后台运行:nohup 的组合参数怎么理解
前台验证通过后,用这条命令启动:
nohup java -jar demo-0.0.1-SNAPSHOT.jar --server.port=8080 --spring.profiles.active=prod > app.log 2>&1 &逐个解释一下:
nohup:忽略终端挂断信号,即使你关掉 SSH,进程也不会退出。--server.port=8080:在命令行覆盖配置文件里的端口。--spring.profiles.active=prod:激活 prod 环境配置。> app.log:把标准输出重定向到 app.log 文件。2>&1:把错误输出也合并到同一个文件。- 末尾的
&:让命令进入后台执行。
如果服务器内存比较紧张,建议加上 JVM 参数:
nohup java -Xms128m -Xmx256m -jar demo-0.0.1-SNAPSHOT.jar > app.log 2>&1 &启动后立刻tail -f app.log盯着,看有没有新的报错。
5.3 确认进程真的起来了:别只看 nohup 没报错
很多人后台启动后,看到没有报错就以为成功了,其实服务可能根本没起来。我用三条命令确认:
ps -ef | grep java jps -l ss -lntp | grep 8080jps是 JDK 自带的工具,jps -l会输出运行中的 JVM 进程和主类完整名,如果能看到你的 jar 路径,说明 JVM 起来了。ss -lntp | grep 8080确认端口在监听。最后在服务器本机做一次自测:
curl http://127.0.0.1:8080/你的项目路径本机能通,说明服务本身没问题;浏览器还是访问不了,那就是防火墙/安全组方向的问题,而不是项目问题。这个排查顺序特别重要,能帮你把问题精确隔离到具体环节。
5.4 让部署更正规一点:用 systemd 托管进程
nohup方式虽然能用,但有两个痛点:一是重启服务器后不会自动启动;二是进程挂掉了没人管。正规一点的部署我建议写一个 systemd 服务文件。
创建/etc/systemd/system/demo.service,内容如下:
[Unit] Description=Demo Java Web Service After=network.target [Service] Type=simple WorkingDirectory=/opt/apps ExecStart=/usr/bin/java -Xms128m -Xmx256m -jar /opt/apps/demo-0.0.1-SNAPSHOT.jar --server.port=8080 Restart=on-failure RestartSec=10 [Install] WantedBy=multi-user.targetExecStart的 JVM 路径建议用完整路径,可以先用which java查一下。配好之后执行:
sudo systemctl daemon-reload sudo systemctl start demo sudo systemctl status demo sudo systemctl enable demoenable是设置开机自启。查看日志用journalctl -u demo -f,和 nohup 的日志查看方式略有不同,注意不要混用。systemd 的好处是进程由系统托管,异常退出后 10 秒自动拉起,稳定性明显好于裸 nohup。
6. 服务器部署的典型问题排查:从日志到端口的完整思路
部署过程中遇到的坑,百分之八十是重复的。如果你能把下面这几类问题快速定位,基本就能应对大部分场景了。
6.1 优先看日志,别瞎猜
遇到部署失败,第一反应应该是看日志,而不是东猜西猜。前台启动直接看终端输出;后台进程就tail -n 100 app.log。重点关注几个信号:
APPLICATION FAILED TO START:Spring Boot 启动阶段就失败,后续一定有 Caused by。Exception、ERROR:具体异常类型。Caused by:这个最关键,它指向最底层的根因。
我曾经排查一个启动失败的问题,日志表面上是数据库连接超时,但顺着Caused by往下追,发现是防火墙没放行 3306,而不是密码错误。不仔细看日志,很容易在错误的方向上浪费几个小时。
6.2 端口占用:8080 被谁占了
启动时如果看到Port 8080 was already in use,说明端口被占了。查端口占用:
ss -lntp | grep 8080或者:
lsof -i:8080根据 PID 用kill -9 PID杀掉对应进程。如果你不想杀,也可以换一个端口启动:--server.port=8081。另外注意,除了应用端口,Tomcat 还有 shutdown 端口和 AJP 端口,多实例部署时也可能互相冲突,虽然 Spring Boot 内嵌 Tomcat 默认只开一个 HTTP 端口,但如果是外置 Tomcat 部署 war,这三个端口都要检查。
6.3 内存与 OOM:小机器最容易踩的坑
日志里出现java.lang.OutOfMemoryError: Java heap space,说明 JVM 堆内存不够。先看服务器整体内存:
free -h如果内存充足,调大-Xmx;如果机器总共只有 1G 内存,建议把堆限制在 256M 左右,同时检查是不是同时跑的进程太多。还有一种情况是日志文件疯狂增长,把磁盘写满,导致 JVM 写不了日志、服务假死。这种可以定期轮转日志,或者用nohup重定向时搭配cronolog之类的工具按天分文件。
6.4 本地能跑服务器跑不了:环境差异排查方向
这类问题最让人头疼。我的排查顺序是:
- JDK 版本差异:看
UnsupportedClassVersionError。 - 数据库 URL 差异:
serverTimezone、useSSL、allowPublicKeyRetrieval是否齐全,连接串里的 IP 是否可达。 - 文件路径差异:本地
D:/data/file这种写法在 Linux 下一定找不到,项目里凡是有硬编码路径的地方都要清一遍。 - 字符集差异:日志中文乱码,通常需要在启动命令加
-Dfile.encoding=utf-8。 - 防火墙差异:服务器本机 curl 通、外网不通,基本上是安全组问题。
按这个顺序排查,多数环境差异都能覆盖到。
6.5 一个稳妥的验收流程
部署完成后,别急着关电脑,按这个流程验收一遍:
- 服务器本机
curl http://127.0.0.1:8080通不通。 - 浏览器访问
http://服务器IP:8080通不通。 - 项目首页、静态资源、图片是否正常。
- 找一个依赖数据库的接口点一下,确认读写正常。
哪一步失败,就回到对应的环节排查。我见过不少人第一步过了,第二步卡住,最后发现是云控制台安全组没放行,白白折腾了一下午。把流程固定下来,部署会快很多。
最后分享一个我自己的习惯
刚接触 Linux 部署那会儿,我最怕的就是进程明明起来了,但日志文件里全是乱码和堆栈,根本不知道从哪里下手。后来养成了一个习惯:所有项目统一用 systemd 托管,启动命令、JVM 参数、环境变量全部写进 service 文件里,日志统一交给 journald 管理。这样不管换了多少台服务器,配置文件都是同一套逻辑,出问题也知道去哪个文件里找。如果你现阶段只有一台服务器、一个项目,用 nohup 完全够了;但只要项目数量多起来,强烈建议尽早切到 systemd,这个转换成本不高,后面省下的是持续的时间。部署这件事,难的不是某一条命令,而是你被坑过一次之后有没有把经验固化成自己的流程。希望这篇图文详解能帮你少走一点弯路,把精力放在真正该关注的事情上。