news 2026/10/15 4:50:21

Ubuntu部署SpringBoot应用:从环境准备到systemd与Nginx完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Ubuntu部署SpringBoot应用:从环境准备到systemd与Nginx完整指南

在实际部署过程中,最容易被忽视的往往不是SpringBoot代码本身,而是服务器上的运行环境、进程托管方式、日志治理这些"看不见"的部分。恰好前两天帮一位朋友排查一个"应用在本地跑得好好的,上服务器就间歇性超时"的问题,最后定位到是Nginx代理超时设置和数据库连接池配置不匹配导致的。顺着这个事,我干脆把在Ubuntu上部署SpringBoot应用的完整过程重新梳理了一遍——从环境准备、构建上传、进程守护到反向代理和日志治理,把每一步背后的原因也说清楚。这篇内容适合那些已经能独立开发SpringBoot接口,但对Linux服务器部署还比较陌生,或者曾经"照着网上的命令敲完了却不知道为什么不生效"的Java开发者。

1. 部署前先想清楚这几件事:包形式、环境版本与目录规划

很多人在部署SpringBoot应用时第一个纠结的问题就是:到底打JAR包还是WAR包?我见过不少团队至今还在用WAR包——把应用塞进外部Tomcat的webapps目录里,然后去改Tomcat的server.xml。这种做法的历史原因是早期Java Web应用普遍依赖Servlet容器管理,但SpringBoot的出现已经彻底改变了这个局面。

SpringBoot内置了Tomcat、Jetty或Undertow,JAR包本身就是"应用+容器"的合体。打个不太恰当的比方:WAR包像是把厨房设备都搬进别人家的中央食堂,你得配合食堂的管理规则;而JAR包像是一个移动餐车,自带炉灶、水电和排风系统,给块空地就能开业。SpringBoot官方也是默认推荐JAR包形式的,spring-boot-maven-plugin这个插件就是为JAR包的可执行性而生的。

选定了JAR包之后,还有一个问题需要正视:SpringBoot版本对应的JDK版本到底匹配不匹配。例如Spring Boot 3.x要求JDK 17以上,Spring Boot 2.7则最低需要JDK 8。服务器上装JDK不是"装上就行",版本不匹配的情况在部署阶段的报错往往不是"版本不对"那么直接,而是启动时抛出各种奇怪的类加载异常或UnsupportedClassVersionError。所以搭建环境之前务必确认java -version的实际输出。

除了Java版本,有几个基础设施信息最好提前确认:

  • Ubuntu系统版本(18.04、20.04、22.04、24.04的行为差异主要在systemd和网络工具上)
  • 是否需要反向代理(通常需要,见第5部分)
  • 数据库版本和连接方式(注意MySQL 8+与Spring Boot默认驱动的兼容性)
  • 服务器开放哪些端口(sshd、HTTP、数据库等)

目录规划同样关键。我见过有人在root家目录下解压应用部署的,也有把应用放在/tmp下的——重启一次服务器应用就没了。比较推荐的做法是在/opt下按应用名建立一个专属目录:

/opt/myapp ├── app.jar ├── config/ │ └── application-prod.yml └── logs/

把配置文件和JAR包分开放是有好处的:升级应用时只需要替换app.jar,配置文件不受影响;日志单独放一个目录,后续做日志轮转和采集也更加方便。

2. 服务器基础环境配置:专用用户、安装JDK、时区与编码

Ubuntu上部署Java应用,第一步不是上传JAR包,而是把服务器环境"打扫干净"。这里说的干净包括三个层面:运行用户的隔离、JDK的安装方式、系统时区与字符集。

2.1 创建专用运行账号

直接用root跑应用是生产环境的大忌。不是说一定出事,而是万一应用被入侵、或者因为误操作改了系统文件,root权限会让影响范围成倍扩大。建议为每个应用创建一个独立的系统用户:

sudo useradd -M -s /usr/sbin/nologin appuser

其中-M表示不创建home目录,-s /usr/sbin/nologin表示禁止该用户登录shell——应用以这个身份运行,即使被攻破也无法直接拿到交互式shell。应用启动后涉及目录的读写权限:

sudo mkdir -p /opt/myapp/{config,logs} sudo chown -R appuser:appuser /opt/myapp sudo chmod -R 750 /opt/myapp

2.2 安装OpenJDK的两种方式

Ubuntu上装JDK主要有两条路线:直接用apt装发行版仓库里的OpenJDK,或者用SDKMAN这类版本管理工具。生产服务器我建议用apt安装,因为走的是系统源,后续apt upgrade可以自动打安全补丁:

sudo apt update sudo apt install -y openjdk-17-jdk-headless java -version

-headless版本不包含图形相关库,服务器上用不到GUI相关能力,少装东西也意味着攻击面更小。如果多个应用需要不同JDK版本共存,再用SDKMAN也不迟,单应用场景下apt就足够了。

2.3 时区、字符集和文件描述符

时区不统一的问题往往在日志和数据库时间上才会暴露。如果应用里有定时任务依赖本地时间,会直接看到任务提前或延后数小时执行。统一设置:

sudo timedatectl set-timezone Asia/Shanghai

华为云、阿里云等各类云厂商提供的Ubuntu镜像默认可能又是UTC时区,这点很subtle,但上线后所有日志时间戳都会和实际时间差8个小时,排错时非常费劲。再顺手确认一下系统字符集:

locale

如果输出不是LANG=en_US.UTF-8或zh_CN.UTF-8这种UTF-8编码,后续读取含中文的文件可能出现乱码。SpringBoot读取application.yml里若有中文注释或字符串,就会碰到编码问题。

最后调整文件描述符上限。SpringBoot应用在较高并发下文件句柄消耗很快,默认的1024根本不够用。虽然systemd服务文件里可以单独配置LimitNOFILE,但最好系统层面也放开:

ulimit -n 65535

这条命令只影响当前会话,持久化修改需要在/etc/security/limits.conf中追加条目,或者依赖第4部分systemd服务脚本中的LimitNOFILE=65535设置。

3. 构建与上传:Maven打包细节、校验文件完整性

环境准备好后,接下来是本机开发环境中的构建工作。这一步看似简单——mvn clean package回车就完事——但实际构建时有一些细节会影响最终部署的成败。

3.1 配置生产环境的打包方式

构建前确认pom.xml中包含了SpringBoot的Maven插件:

<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>

这个插件的作用是把应用打成一个"可执行JAR",内部通过特殊的Spring-Boot-ClassLoader来加载BOOT-INF/classes下的类和BOOT-INF/lib下的依赖JAR。没有这个插件,mvn package打出来的JAR只是一个普通JAR,依赖不会包含进去,在服务器上运行时铁定报ClassNotFoundException。

对于多环境配置的情况,我习惯在构建时显式激活生产配置:

mvn clean package -DskipTests -Dspring.profiles.active=prod

不过更常见的做法是构建时不绑定profile,而是在服务器上通过环境变量或启动参数指定,这样同一个JAR可以灵活部署到不同环境。JAR内和JAR外配置的优先级关系往后靠一靠,部署参数部分再详细讲。

3.2 上传与完整性校验

构建完成后,target目录下会生成两个文件:一个myapp-0.0.1-SNAPSHOT.jar,一个以.original结尾的原始JAR。部署时使用的是前者。很多新手分不清两个文件的区别——前者是SpringBoot插件改造过、能直接java -jar运行的;后者是Maven默认打的原始JAR包,没有内嵌依赖,不能直接运行。

上传方式上,小文件用scp就够了:

scp target/myapp.jar appuser@服务器地址:/opt/myapp/app.jar

但生产环境我建议先传到临时目录,做校验之后再移动到应用目录。这样能避免"传了一半文件被应用读到导致启动失败"的问题。校验最简单的方式是比对MD5:

md5sum app.jar # 本机执行,记录值 md5sum /tmp/app.jar # 服务器上传后执行,检查是否一致

另外提醒一句:不要在目标服务器上用vim直接编辑JAR包里面的配置文件——JAR是zip压缩格式,直接编辑会破坏结构导致无法启动。

3.3 外部配置优先:application.yml与Config Data

SpringBoot的外部化配置有一个优先级顺序,命令行参数大于SPRING_APPLICATION_JSON环境变量,大于application-{profile}.yml,最后才是JAR包内的application.yml。这个特性非常实用,也是我推荐"JAR里不带生产配置"的原因。

生产环境的数据库连接、Redis地址这类敏感信息,最稳妥的放置方式是放到服务器上独立的配置文件里,用启动参数显式指定:

java -jar app.jar \ --spring.config.additional-location=file:/opt/myapp/config/application-prod.yml

这样JAR包本身可以在不同环境间复用,服务器上的配置文件也便于纳管和审计,换一台机器不需要重新构建。

4. 用systemd托管服务:进程挂掉自动拉起与开机自启

Java进程跑起来不难,难的是"服务器重启之后应用别消失""进程意外退出之后能自动拉起来"。这两件事如果在Windows上用服务,在Linux上最标准的答案就是systemd。

4.1 为什么要用systemd而不是nohup

我看到网上很多教程还在用类似nohup java -jar app.jar > /dev/null 2>&1 &的方式。这种方式的缺点很明显:进程没有监督机制,挂了就是挂了;开机不会自动启动;日志管理混乱。systemd作为Linux主流系统的服务管理器,提供了进程守护、自动重启、开机自启、日志统一收集这些能力,生产级部署没有理由不用它。

4.2 编写service文件

在/etc/systemd/system/下新建myapp.service:

[Unit] Description=My Spring Boot Application After=network.target mysql.service Wants=mysql.service [Service] User=appuser Group=appuser WorkingDirectory=/opt/myapp Environment="JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64" Environment="SPRING_PROFILES_ACTIVE=prod" Environment="SERVER_PORT=8080" ExecStart=/usr/bin/java -Xms512m -Xmx1024m -jar /opt/myapp/app.jar Restart=on-failure RestartSec=10 SuccessExitStatus=143 StandardOutput=journal StandardError=journal LimitNOFILE=65535 [Install] WantedBy=multi-user.target

逐项解释一下关键点:

  • After=network.target mysql.service:声明应用应该在网络服务和MySQL服务启动后再启动。MySQL服务名在不同Ubuntu版本上可能叫mysql.service或mysqld.service,根据实际情况调整。
  • Restart=on-failure:非正常退出(退出码非0)时自动重启。某些情况下应用主动退出且退出码为0时,on-failure不会触发重启,这通常是符合预期的。
  • SuccessExitStatus=143:143是SIGTERM信号对应的退出码,systemd执行stop操作时会发SIGTERM,如果SpringBoot正常处理优雅退出返回143,会被systemd视为正常退出,不触发Restart逻辑。
  • LimitNOFILE=65535:等价于替这个服务设置了ulimit,不需要依赖limits.conf,这是systemd服务单独隔离的优势。
  • StandardOutput=journal:应用stdout和stderr输出重定向到journald,统一日志管理基础。

一个常见的疑问是ExecStart里/usr/bin/java这个路径。实际上配置JDK后也可以直接用java命令,但systemd服务默认的PATH可能没有包含JDK目录,所以在service文件里写完整路径是最稳妥的。想知道Java安装路径,执行readlink -f $(which java)即可。

4.3 systemd服务启用与常用操作

编写完成后依次执行:

sudo systemctl daemon-reload sudo systemctl enable myapp sudo systemctl start myapp

enable的作用是创建开机启动的符号链接。如果立即需要查看应用启动日志(相当于以前的tail -f),用:

sudo journalctl -u myapp -f

平时需要查看服务状态的:

sudo systemctl status myapp

如果想在服务还正常运行的情况下看某一次最新启动日志:

sudo journalctl -u myapp -n 100 --since "5 min ago"

4.4 应用的新版本上线,如何平滑滚动发布

ssh在线发布时可以直接替换JAR包后重启服务:

sudo systemctl stop myapp # 上传新的app.jar并覆盖 sudo chown appuser:appuser /opt/myapp/app.jar sudo systemctl start myapp

如果应用流量较大、不能允许较长停机时间,可以考虑设置Restart=always,配合systemctl restart来做到快速重启。但注意SpringBoot默认情况下停机是优雅的——会等处理中的请求完成再退出,因此重启耗时会比强杀长一些,但安全性更高。

5. Nginx反向代理与数据库连接:两个最容易踩坑的地方

SpringBoot内嵌的Tomcat默认只监听8080端口,直接把8080暴露给外部访问不是不可以,但有几个问题:一是没法同时托管多个域名和多个应用;二是不好处理HTTPS证书;三是应用层的请求日志和限流能力都比较弱。Nginx加一层反向代理是生产环境的标准姿势。

5.1 Nginx配置最小闭环

安装Nginx:

sudo apt install -y nginx

在/etc/nginx/sites-available/下新建myapp.conf:

server { listen 80; server_name myapp.example.com; client_max_body_size 50m; location / { proxy_pass http://127.0.0.1:8080; 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_connect_timeout 5s; proxy_read_timeout 60s; proxy_send_timeout 60s; } }

几个容易出错的地方:

  • proxy_pass http://127.0.0.1:8080;结尾没有斜杠,与DSL方式proxy_pass http://127.0.0.1:8080/;行为有差别。带斜杠会把匹配到的location前缀从转发路径中去除,不带斜杠则保留原始URI。先搞清楚自己的URI设计再决定要不要加斜杠。
  • server_name如果直接填IP访问测试,也可以用_作通配。
  • client_max_body_size不设置的话默认1m,超过1MB的请求体直接413,很多人第一次上传文件时遇到的就是这个坑。
  • 如果应用通过request.getRemoteAddr()获取客户端IP,会发现始终是127.0.0.1——因为后端看到的是代理服务器的连接。此时SpringBoot需要配合X-Forwarded-For处理,最简单的方法是在配置文件里设:
server: forward-headers-strategy: framework

这样内嵌Tomcat会解析Nginx传入的header,正确设置真实客户端IP。

启用站点:

sudo ln -s /etc/nginx/sites-available/myapp.conf /etc/nginx/sites-enabled/myapp.conf sudo nginx -t sudo systemctl reload nginx

5.2 MySQL连接的时区与公钥问题

如果说Nginx配置是"入口的坑",那数据库连接就是"应用的坑"。最常见的两个报错场景:

  • The server time zone value 'CST' is unrecognized or represents more than one time zone。这是MySQL 8和JDBC驱动之间时区协商的经典问题。解决方案是在JDBC URL中明确指定时区:
jdbc:mysql://127.0.0.1:3306/mydb?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true

allowPublicKeyRetrieval=true单独说一下:MySQL 8默认使用caching_sha2_password认证,非SSL连接时客户端需要先拿到服务端公钥才能进行rsa加密密码传输。本地开发可能由于缓存公钥一直没报错,换个环境就抛Public Key Retrieval is not allowed异常。这个参数就是允许JDBC客户端主动向服务端请求公钥,开发环境可以开,生产环境如果走内网且不是严格安全域,问题不大;如果安全审计要求高,更推荐直接启用SSL连接。

  • 连接池配置不合理导致间歇性超时。SpringBoot默认的HikariCP连接池很优秀,但默认的maximumPoolSize=10可能偏小,而connectionTimeout=30000偏长。如果是低并发但多接口内多次查库的场景,连接池排队时间会被放大。我个人的经验值作为参考:
spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 idle-timeout: 30000 connection-timeout: 5000 max-lifetime: 1800000

其中max-lifetime应该比数据库侧的wait_timeout略短,例如MySQLwait_timeout默认8小时,那么连接池max-lifetime建议不超过2小时(30分钟=1800000ms),这样能保证连接在数据库主动断开之前被池回收重建。

6. 日志治理与上线后第一周要盯着的事

服务起来了、Nginx转发正常了、数据库连上了——剩下的事情才是决定这个应用能不能长期稳定跑下去的关键:日志能不能找到、满了怎么办、挂了之后发生了什么。这些经验不是从官方文档里能直接学到的,更多是踩坑踩出来的。

6.1 SpringBoot日志落盘

SpringBoot默认只把日志打到控制台,配合systemd的journald可以看,但长期运行后日志文件管理和检索能力都很弱。更推荐在application-prod.yml中直接配置Logback输出到文件:

logging: file: name: /opt/myapp/logs/app.log logback: rollingpolicy: max-file-size: 100MB max-history: 15 total-size-cap: 2GB

文件滚动策略的作用直观来说就是:单个日志到了100MB就滚动到下一个文件,保留最近15个文件,所有日志总量限制在2GB以内。不设置的话,日志文件可能无限增长,最终把磁盘撑爆。

6.2 磁盘空间告警

磁盘满是个非常隐蔽的故障源。应用可能不会立刻崩,但会出现很古怪的现象——写日志失败但接口仍在处理、上传文件报磁盘容量不足、MySQL突然拒写。建议上线后加一个简单的定时任务检查磁盘:

df -h /opt

如果日志增长快、磁盘剩余告急,优先级最高的排查顺序是:

  1. journalctl占用的日志量(systemd日志默认可能积累到GB级别)
  2. 应用logback的滚动策略有没有生效
  3. 数据库自身的binlog、慢日志、错误日志大小

systemd的journal占用可以通过journalctl --disk-usage查看,再通过配置文件或journalctl --vacuum-size=500M清理。

6.3 上线后第一周必须盯的几个指标

部署完成不是结束,前一周的观察期里,这四类信号是最有价值的:

  • 健康检查接口:SpringBoot Actuator如果引入了,直接访问/actuator/health确认UP状态。没有引入的话,至少保证有一个简单接口可以做存活探测。
  • 启动耗时:ExecStart命令跑完后,journal里会看到Started My Spring Boot Application。如果启动时间超过30秒,检查是否网络依赖等待超时、连接池初始化太慢。
  • 内存占用趋势:systemctl show myapp -p MemoryCurrent可以看到当前使用内存。如果持续上涨不回落,大概率是代码里有内存泄漏,需要结合堆内存监控进一步排查。
  • 线程池与连接池饱和:通过/actuator/metrics可以看hikaricp连接池的使用情况,连接数持续逼近最大值时就要考虑调参或扩容。

6.4 一个常见的排错链路,供参考

假设应用上线后第二天发现接口偶尔500,你可以在服务器上按这个顺序排查:

  1. 看journalctl -u myapp --since "30 min ago",查应用日志中是否有异常堆栈。
  2. 如果日志正常,再看journalctl -u nginx --since "30 min ago",确认Nginx有没有报upstream timed out或upstream prematurely closed。
  3. 是upstream报错的话,检查数据库连接池和依赖的Redis等服务状态。
  4. 数据库连接池满了,则看当前active连接数,配合SHOW PROCESSLIST确认是否有慢SQL占用连接不释放。
  5. 慢SQL的修复方向通常是加索引、优化查询或调整reduce连接占用时间。

这个链路从"症状"到"直接原因"到"根因",是一个标准的从现象到定位的思路。实际排查时大多数问题都能在第二步和第三步之间定位:要么应用本身抛异常,要么依赖的基础组件出问题,很少只停留在"不知道从哪里看起"的状态。

我在实际部署中最大的体会是:SpringBoot应用本身的部署难度其实不高,真正决定体验的是服务器侧的环境规划和进程治理是否到位。一个由systemd接管、Nginx前置、日志有滚动、磁盘有告警的部署形态,和"nohup挂起来就完事"的部署形态,在运行一个月后会产生巨大的维护成本差异。如果你要部署的应用会长期运行,值得把这些环节一次做对。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 4:50:21

DTMB帧结构详解:从PN序列到TDS-OFDM的同步与模式识别

简介&#xff1a;中国数字地面多媒体广播&#xff08;DTMB&#xff09;的帧结构是保障系统快速稳定同步的关键设计。PDF文档系统梳理了从信号帧、超帧、分帧到日帧的分级帧结构&#xff0c;并逐项解析三种可选帧头模式&#xff08;PN420、PN595、PN945&#xff09;的序列构成、…

作者头像 李华
网站建设 2026/10/15 4:50:01

CAS单点登录REST认证实战:从票据流转到接口联调

1. 从一次接口联调说起&#xff1a;REST认证到底卡在哪前阵子帮一个朋友排查他们内部系统的登录问题&#xff0c;场景很典型&#xff1a;后端是一套基于CAS的单点登录体系&#xff0c;前端除了常规的浏览器页面&#xff0c;还有一堆移动端App、第三方合作方的服务端调用&#x…

作者头像 李华
网站建设 2026/10/15 4:48:57

档案库房多类型环境一体机选型对比:加湿/除湿/消毒/净化/四合一方案

档案库房多类型环境一体机选型对比&#xff1a;加湿/除湿/消毒/净化/四合一方案物联网 智慧档案馆 环境监控 盛世宏博档案库房环境治理涉及加湿、除湿、消毒、空气净化等多项功能&#xff0c;市场上的环境一体机也因此分成多种类型&#xff1a;只做加湿的、只做除湿的、除湿…

作者头像 李华
网站建设 2026/10/15 4:47:47

精读rtl8370n数据手册:寄存器映射与EEPROM配置的调试要点

简介&#xff1a;这是一份Realtek RTL8370NI-VB-CG芯片的英文原版数据手册PDF&#xff08;Rev.0.1&#xff0c;2019年8月发布&#xff09;&#xff0c;面向交换机固件开发、网络设备硬件设计及运维调试人员&#xff0c;用于解决Layer 2管理型8端口10/100/1000M以太网交换控制器…

作者头像 李华
网站建设 2026/10/15 4:46:56

人类文明演进的核心困境:时间差、复杂度与集体行动的深层机制

“人类文明演进的核心困境”这个题目&#xff0c;我琢磨了很久。每天打开资讯&#xff0c;一边是技术突破的消息接连不断&#xff0c;一边是各种社会矛盾与观念冲突让人疲惫&#xff1b;我们享受着空前便利的生活&#xff0c;却时常在深夜里感到一种说不清的停滞感——好像一切…

作者头像 李华
网站建设 2026/10/15 4:46:41

栈溢出漏洞利用与ROP技术实战解析

1. 题目背景与核心挑战"jarvisoj_level2 1"是网络安全领域的一道经典PWN题目&#xff0c;主要考察二进制漏洞利用中的栈溢出技术。这类题目通常来自CTF&#xff08;Capture The Flag&#xff09;竞赛&#xff0c;要求选手通过逆向工程和漏洞利用技术获取目标系统的控…

作者头像 李华