把 jar 包扔到 CentOS 7 上跑起来这件事听起来简单真上手了才发现坑一点不少。我见过太多人第一次部署时卡在nohup日志不输出、端口被防火墙拦死、java -jar一关终端就断这些地方。这篇内容就是把我这些年反复部署 Spring Boot 打出来的 jar 包时踩过的坑、总结出来的稳定套路从头到尾捋一遍。不管你是刚接触 Linux 服务端的新手还是已经能跑通但总在细节上翻车的朋友下面这些内容都能直接拿去用。核心围绕jar 包部署、CentOS 7 环境准备、systemd 服务托管、防火墙端口放行、日志与 JVM 参数调优这几个关键词展开讲清楚每一步为什么这么做、不做会出什么问题。1. 部署之前先把 CentOS 7 的底子摸清楚很多人拿到一台新机器第一反应就是scp传包然后java -jar结果报java: command not found或者跑起来内存直接爆掉。部署 jar 包本质上是给 JVM 找一个稳定、可管理、可观测的运行环境CentOS 7 作为载体它的几个特性必须先确认。1.1 确认系统版本与架构别在 32 位上装 64 位 JDKCentOS 7 虽然是个老版本但市面上存量机器极多尤其是各种云主机和内部测试机。第一步永远是确认系统信息cat /etc/centos-release uname -m第一条输出类似CentOS Linux release 7.9.2009 (Core)第二条输出x86_64或aarch64。这个信息决定了你后面下载 JDK 的包名。我遇到过有人在 ARM 架构的机器上照着 x86 的教程下载 JDK解压后java -version直接报cannot execute binary file折腾半天以为是权限问题其实是架构不匹配。CentOS 7 默认的软件源里 JDK 版本偏老yum install java-1.8.0-openjdk装出来的是 OpenJDK 8对于大多数 Spring Boot 2.x 项目够用但如果你用的是 Spring Boot 3.x它要求 JDK 17 起步这时候 yum 源里就没有合适的包了必须手动下载 JDK 17 的压缩包解压配置。所以先想清楚你的 jar 包是用哪个 JDK 版本编译的MANIFEST.MF里或者项目pom.xml里都有线索。提示用unzip -p your-app.jar META-INF/MANIFEST.MF可以快速查看 jar 包的构建信息虽然不一定直接写 JDK 版本但能确认主类和构建工具。1.2 JDK 安装的两条路线yum 与手动解压路线一yum 安装 OpenJDK 8适合传统项目省事yum install -y java-1.8.0-openjdk java-1.8.0-openjdk-devel java -version装完java和javac都在/usr/bin下环境变量基本不用配。缺点是版本固定想换版本麻烦。路线二手动下载 tar.gz 解压适合需要特定版本如 JDK 17、JDK 21的场景mkdir -p /usr/local/java tar -zxvf jdk-17_linux-x64_bin.tar.gz -C /usr/local/java/然后配置环境变量编辑/etc/profileexport JAVA_HOME/usr/local/java/jdk-17 export PATH$JAVA_HOME/bin:$PATH执行source /etc/profile生效。这里有个细节JAVA_HOME路径一定要指到 JDK 根目录不要指到bin目录否则一些依赖JAVA_HOME的脚本会找不到lib下的工具。我见过有人配成/usr/local/java/jdk-17/bin结果jps、jmap这些诊断工具全用不了排查问题时抓瞎。手动安装还有个容易忽略的点CentOS 7 自带的alternatives机制可能会让java命令指向系统预装的旧版本。装完后用which java确认一下指向如果是/usr/bin/java而不是你JAVA_HOME/bin/java需要调整PATH顺序或者用alternatives --config java切换。1.3 创建专用运行用户别拿 root 裸奔生产环境直接用 root 跑 jar 包是坏习惯。一旦应用被攻破攻击者直接拿到最高权限。正确做法是建一个专用用户useradd -r -s /sbin/nologin appuser mkdir -p /opt/myapp chown -R appuser:appuser /opt/myapp-r表示系统用户-s /sbin/nologin禁止登录 shell只用来跑服务。jar 包、日志目录、配置文件都放到/opt/myapp下属主设为appuser。这样即使应用有漏洞影响范围也被限制在这个用户权限内。这里有个实操细节日志目录如果由应用自己创建appuser需要有写权限如果日志目录是提前建好的记得chown到位。我踩过一次坑日志目录属主是 root应用启动后写日志报Permission denied但进程没退出只是日志静默丢失排查了半天才发现是权限问题。2. 把 jar 包送上去并做启动前的体检jar 包传到服务器只是第一步传完之后不检查直接启动等于闭着眼睛开车。这一节讲传输方式的选择、启动前的自检清单以及第一次手动启动该怎么观察。2.1 传输方式scp、sftp 与内网分发最常用的是scpscp -P 22 ./myapp.jar appuser192.168.1.100:/opt/myapp/注意-P是大写指定端口小写-p是保留文件时间戳别搞混。如果服务器改了 SSH 端口不加-P会一直卡在连接阶段。文件大的话几百 MB 的 fat jarscp中途断了很烦可以用rsync支持断点续传rsync -avz --progress -e ssh -p 22 ./myapp.jar appuser192.168.1.100:/opt/myapp/如果是多台机器部署别一台台传先传到一台跳板机或者内网文件服务器再用内网scp分发速度快很多。内网传输带宽跑满千兆的话几百 MB 也就几秒。传完之后一定要校验文件完整性尤其是大文件传输中断过的情况# 本地 md5sum myapp.jar # 服务器 md5sum /opt/myapp/myapp.jar两个值对不上就重新传别心存侥幸。我遇到过 jar 包传了一半导致启动时报Invalid or corrupt jarfile当时以为是打包问题重新打包好几次最后发现是传输截断。2.2 启动前的五项自检在敲下java -jar之前花两分钟过一遍这个清单能省掉后面大量排查时间检查项命令期望结果JDK 可用java -version版本与编译版本匹配文件属主ls -l /opt/myapp/myapp.jar属主为 appuser端口占用ss -tlnp | grep 8080无输出端口空闲配置文件ls /opt/myapp/config/外部配置存在且可读磁盘空间df -h /opt剩余空间充足端口占用这条特别重要。CentOS 7 上如果之前跑过同端口的服务没清理干净新进程启动会报Address already in use但有些框架的报错信息很隐晦表现为启动到一半卡住然后退出。用ss -tlnp比netstat快CentOS 7 默认装了iproute包ss直接可用。配置文件这块Spring Boot 支持外部配置覆盖 jar 内部配置优先级从高到低大致是命令行参数 外部application.yml jar 内部配置。把生产配置放在/opt/myapp/config/application.yml启动时 Spring Boot 会自动加载同目录下config/子目录的配置不用额外指定--spring.config.location。这个机制很多人不知道结果把配置打进 jar 包里改个数据库地址还得重新打包。2.3 第一次手动启动前台跑看清楚了再托管第一次启动建议前台运行把日志直接打到终端方便观察cd /opt/myapp java -jar myapp.jar观察几个关键信号Spring Boot 的 banner 是否正常打印、数据源连接是否成功、端口是否监听成功、有没有异常堆栈。启动完成后用另一个终端curl http://127.0.0.1:8080/actuator/health如果集成了 actuator或者直接访问业务接口验证。前台跑还有一个好处能直观看到启动耗时和内存占用。启动完成后按CtrlC停掉再进入正式的后台托管。别一上来就用nohup ... 出问题了日志在文件里翻半天不如前台看一眼来得快。注意前台启动时如果终端断开比如 SSH 超时进程会收到 SIGHUP 信号退出。所以前台只用于调试正式运行必须用 systemd 或 nohup 托管。3. 后台运行的三种姿势为什么我最终选了 systemd让 jar 包在后台稳定运行常见做法有三种nohup 、screen/tmux会话、systemd服务。这三种我都用过最后全部迁移到了 systemd原因下面逐个说。3.1 nohup 的局限能跑但不好管最原始的方式nohup java -jar /opt/myapp/myapp.jar /opt/myapp/logs/app.log 21 nohup让进程忽略 SIGHUP放后台 ... 21把标准输出和错误都重定向到日志文件。这套组合能跑但问题一堆进程管理靠 PID 文件得自己echo $! app.pid记录 PID停止时kill $(cat app.pid)。如果 PID 文件丢了或者进程被 OOM killer 干掉PID 文件就成了脏数据下次kill可能误杀别的进程。开机不自启服务器重启后得手动再跑一遍或者写进/etc/rc.local但 CentOS 7 的rc.local默认没有执行权限还得chmod x /etc/rc.d/rc.local。日志不会轮转app.log会无限增长几天就把磁盘撑满。得额外配logrotate。崩溃不自动重启进程挂了就挂了没有任何自愈能力。我早期项目用 nohup有次半夜应用 OOM 退出第二天早上才发现业务停了几个小时。从那以后就下决心换 systemd。3.2 screen/tmux适合临时调试不适合生产screen -S myapp开个会话里面跑java -jarCtrlA D分离。这种方式的好处是随时能 attach 回去看输出适合调试。但生产环境用它有几个硬伤会话管理依赖人工服务器重启后会话全没了多用户环境下容易误操作别人的会话日志还是得自己重定向。所以 screen/tmux 我只在临时验证、演示环境用正式服务一律 systemd。3.3 systemd 单元文件一次配置长期省心systemd 是 CentOS 7 的默认服务管理器用它托管 jar 包能同时解决自启、崩溃重启、日志收集、资源限制这几个问题。先看一个完整的单元文件/etc/systemd/system/myapp.service[Unit] DescriptionMy Spring Boot Application Afternetwork.target [Service] Typesimple Userappuser Groupappuser WorkingDirectory/opt/myapp ExecStart/usr/local/java/jdk-17/bin/java -jar /opt/myapp/myapp.jar SuccessExitStatus143 Restarton-failure RestartSec10 StandardOutputjournal StandardErrorjournal EnvironmentJAVA_OPTS-Xms512m -Xmx1024m [Install] WantedBymulti-user.target逐项解释关键配置的用意Afternetwork.target确保网络就绪后再启动避免应用启动时连不上数据库。Typesimple默认类型ExecStart启动的进程就是主进程。Spring Boot 前台运行符合这个模型。User/Group用专用用户运行前面建好的appuser。WorkingDirectory工作目录设为/opt/myapp这样相对路径的配置、日志都基于这个目录。SuccessExitStatus143143 是 12815即进程收到 SIGTERM 正常退出的退出码。不加这个systemctl stop会被判定为失败。Restarton-failure非正常退出时自动重启。配合RestartSec10间隔 10 秒避免频繁重启打爆日志。StandardOutputjournal日志交给 journald 管理用journalctl -u myapp查看自带轮转。配置好后执行systemctl daemon-reload systemctl enable myapp systemctl start myapp systemctl status myappdaemon-reload是必须的改了单元文件不 reloadsystemd 用的还是旧配置。enable设置开机自启start立即启动status看运行状态。这里有个我踩过的坑ExecStart里的java命令最好写绝对路径。systemd 的PATH环境和登录 shell 不一样写java可能找不到。我一开始写相对路径systemctl start报Failed to locate executable改成/usr/local/java/jdk-17/bin/java就好了。3.4 journalctl 日志查看的实用技巧用 systemd 托管后日志在 journald 里查看命令# 实时跟踪 journalctl -u myapp -f # 看最近 100 行 journalctl -u myapp -n 100 # 按时间过滤 journalctl -u myapp --since 2024-01-01 10:00:00 # 只看错误级别 journalctl -u myapp -p errjournald 默认会占用磁盘空间CentOS 7 上默认配置在/etc/systemd/journald.conf可以限制大小SystemMaxUse2G改完systemctl restart systemd-journald生效。如果不限制日志可能吃掉几个 G 磁盘尤其是应用疯狂打日志的时候。不过 journald 的日志格式对业务排查不太友好很多团队还是习惯应用自己写文件日志。这时候可以在ExecStart里加日志重定向或者用 logback 配置输出到文件systemd 只管进程生命周期。两种方式可以并存应用日志写文件systemd 日志记录启动、停止、崩溃这些系统级事件。4. 防火墙与 SELinuxCentOS 7 上最容易被忽略的两道坎应用在服务器上跑起来了本地curl也通了但从外部访问就是连不上。十有八九是防火墙或者 SELinux 在拦。这两个是 CentOS 7 部署 jar 包时最高频的隐形杀手。4.1 firewalld 放行端口的标准操作CentOS 7 默认用 firewalld查看状态systemctl status firewalld firewall-cmd --state如果防火墙开着放行端口# 永久放行 8080 端口 firewall-cmd --permanent --add-port8080/tcp # 重载生效 firewall-cmd --reload # 确认 firewall-cmd --list-ports--permanent和--reload这两个必须配合使用。只加--permanent不 reload规则不生效只加临时的不加--permanent重启后丢失。我见过有人加了规则没 reload以为防火墙没生效其实是规则还在待加载状态。如果应用用了多个端口比如 HTTP 8080 管理端口 8081逐个放行。也可以按服务名放行但自定义端口用--add-port更直接。排查时可以用firewall-cmd --list-all看当前区域的所有规则确认端口在不在列表里。如果还是不通用tcpdump抓包确认请求有没有到达服务器tcpdump -i eth0 port 8080 -nn有包进来但没响应说明是应用层问题没包进来说明卡在防火墙或更上层。4.2 SELinux 的两种处理策略SELinux 是 CentOS 7 默认开启的强制访问控制机制。它可能阻止应用绑定非标准端口、读写某些目录。查看状态getenforce输出Enforcing表示强制模式Permissive表示只记录不拦截Disabled表示关闭。处理策略有两种策略一临时设为 Permissive 排查setenforce 0如果设为 Permissive 后问题消失说明确实是 SELinux 拦的。这只是临时排查手段重启后恢复 Enforcing。策略二针对性放行推荐如果应用需要绑定 8080 端口而 SELinux 默认只允许 httpd 绑定 80、443 等端口需要把 8080 加入允许列表semanage port -a -t http_port_t -p tcp 8080semanage命令来自policycoreutils-python包没装的话先yum install -y policycoreutils-python。如果应用需要写某个目录用semanage fcontext加restorecon打标签semanage fcontext -a -t httpd_sys_rw_content_t /opt/myapp/logs(/.*)? restorecon -Rv /opt/myapp/logs直接setenforce 0关掉 SELinux 是最省事的做法但生产环境不建议等于放弃了一层安全防护。针对性放行虽然麻烦点但一次配置长期有效。提示改完 SELinux 策略后用ausearch -m avc -ts recent可以查看最近的拒绝日志确认策略是否生效。5. JVM 参数与日志让应用跑得稳、查得清jar 包能启动只是及格线跑得稳、出问题能查清楚才是生产要求。这一节讲 JVM 内存参数怎么定、日志怎么配、以及几个常见的运行时问题。5.1 堆内存参数别拍脑袋写 -Xmx-Xms和-Xmx是最常调的两个参数分别是最小堆和最大堆。常见误区有两个一是设得过大把服务器内存吃光导致系统卡死二是-Xms和-Xmx设成一样大以为这样性能最好。先说大小怎么定。经验公式堆内存不超过物理内存的 50%~70%剩下的留给操作系统、堆外内存Netty 的 DirectBuffer、JVM 自身的 Metaspace、线程栈等。一台 4G 内存的机器-Xmx设 2G 比较稳妥8G 的机器设 4G 左右。-Xms和-Xmx设成一样大好处是避免堆动态扩缩带来的抖动坏处是启动时就占满内存。对于长期运行的服务我倾向于设成一样大比如-Xms2g -Xmx2g让内存行为可预测。但如果是内存紧张的机器-Xms可以设小一点比如-Xms512m -Xmx2g让 JVM 按需扩展。除了堆还有几个参数值得关注-XX:MetaspaceSize256m -XX:MaxMetaspaceSize512mMetaspace 存类元数据Spring 项目类多默认值可能不够设个上限避免无限增长。-XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/opt/myapp/dumps/OOM 时自动 dump 堆快照排查内存泄漏的救命稻草。dump 目录要提前建好并给写权限否则 dump 失败。-XX:UseG1GCJDK 9 以后 G1 是默认 GCJDK 8 需要显式开启。G1 对大堆4G 以上更友好停顿时间可控。小堆用默认的 Parallel GC 也行不必强上 G1。这些参数统一放到JAVA_OPTS环境变量里在 systemd 单元文件中通过Environment注入或者写个启动脚本。注意JAVA_OPTS不会自动被java命令读取需要在ExecStart里显式引用ExecStart/bin/bash -c /usr/local/java/jdk-17/bin/java $JAVA_OPTS -jar /opt/myapp/myapp.jar用bash -c包一层是为了让$JAVA_OPTS被展开。直接写java $JAVA_OPTS -jar在 systemd 里不会展开变量这是很多人配置不生效的原因。5.2 日志配置文件日志与 journald 的分工前面提到 systemd 用 journald 收集标准输出但业务日志建议还是写文件方便用grep、tail这些熟悉工具排查。Spring Boot 项目在application.yml里配logging: file: name: /opt/myapp/logs/app.log logback: rollingpolicy: max-file-size: 100MB max-history: 30这样日志按 100MB 轮转保留 30 天。日志目录/opt/myapp/logs要提前建好属主appuser。如果不想改代码配置也可以在启动参数里指定java -jar myapp.jar --logging.file.name/opt/myapp/logs/app.log命令行参数优先级最高适合临时调整。日志级别也要注意生产环境别开DEBUG日志量能把你磁盘写满。默认INFO级别排查特定问题时临时调某个包的级别logging: level: com.example.myapp: DEBUG只开自己业务包的 DEBUG框架包的日志保持 INFO避免日志洪水。5.3 几个高频运行时问题与处理问题一启动后端口没监听但进程还在用jstack看线程栈jstack pid /tmp/thread.txt搜BLOCKED或者看主线程卡在哪。常见原因是数据库连接超时、外部依赖初始化阻塞。jstack输出的线程状态能直接定位。问题二内存持续增长疑似泄漏先用jmap看堆概况jmap -heap pid再用jstat观察 GC 频率jstat -gcutil pid 1000如果 Full GC 后老年代占用还是居高不下基本可以确认有对象没释放。这时候用jmap -dump:formatb,file/tmp/heap.hprof pid导出堆快照拿 MAT 或者 VisualVM 分析。问题三CPU 飙高先top -Hp pid找到占 CPU 高的线程 ID转成十六进制printf %x\n tid然后在jstack输出里搜这个十六进制值定位到具体代码行。这套组合拳是排查 CPU 问题的标准流程。这些诊断工具都依赖 JDK 的bin目录所以前面强调JAVA_HOME要配对jps、jstack、jmap才能用。6. 版本更新与回滚部署不是一次性动作jar 包部署不是传上去跑起来就完事后续的版本迭代、回滚、灰度才是常态。这一节讲一套我用了多年的更新流程核心原则是任何更新都要能快速回滚。6.1 目录结构设计为回滚留后路推荐这样的目录结构/opt/myapp/ ├── myapp.jar - releases/myapp-1.2.0.jar # 软链接指向当前版本 ├── releases/ │ ├── myapp-1.1.0.jar │ └── myapp-1.2.0.jar ├── config/ │ └── application.yml └── logs/每次发版把新 jar 放到releases/下用版本号命名然后把myapp.jar软链接指向新版本。systemd 的ExecStart里写/opt/myapp/myapp.jar实际执行的是软链接指向的文件。更新流程# 1. 上传新版本 scp myapp-1.2.0.jar appuserserver:/opt/myapp/releases/ # 2. 切换软链接 ln -sfn /opt/myapp/releases/myapp-1.2.0.jar /opt/myapp/myapp.jar # 3. 重启服务 systemctl restart myapp # 4. 验证 curl http://127.0.0.1:8080/actuator/health回滚就是把软链接指回旧版本再重启ln -sfn /opt/myapp/releases/myapp-1.1.0.jar /opt/myapp/myapp.jar systemctl restart myapp整个过程不到一分钟。软链接用-sfn-n很重要否则当目标是目录时会出问题。这个细节我踩过坑ln -sf不带-n在某些情况下会把软链接建到目标目录里面去。6.2 重启时的优雅停机systemctl restart默认发 SIGTERMSpring Boot 收到后会执行优雅停机如果配置了server.shutdowngraceful等待正在处理的请求完成再退出。但 systemd 默认只等 90 秒就发 SIGKILL 强杀如果请求处理时间长可能被硬杀。可以在单元文件里调整TimeoutStopSec30设成 30 秒超过就强杀。配合 Spring Boot 的优雅停机配置server: shutdown: graceful spring: lifecycle: timeout-per-shutdown-phase: 20s这样应用最多等 20 秒处理完存量请求systemd 最多等 30 秒留了 10 秒缓冲。6.3 更新后的验证清单重启完别急着走人过一遍验证验证项方法通过标准进程状态systemctl status myappactive (running)端口监听ss -tlnp | grep 8080有监听记录健康检查curl 127.0.0.1:8080/actuator/healthstatus UP日志无异常journalctl -u myapp -n 50无 ERROR 堆栈业务接口调一个核心接口返回正常这五项都过了才算更新成功。我见过进程起来了但数据源没连上的情况status显示 running实际业务全挂所以健康检查和业务接口验证不能省。7. 一些零散但很值钱的实操经验前面按流程讲完了最后补充几个不成体系但很实用的点都是实际部署中攒下来的。关于时区。CentOS 7 默认可能是 UTC应用日志时间比北京时间差 8 小时排查问题时对不上。启动参数加-Duser.timezoneAsia/Shanghai或者在系统层面timedatectl set-timezone Asia/Shanghai。JVM 时区和系统时区是两回事只改系统不改 JVM 参数日志时间可能还是不对。关于文件句柄数。高并发应用可能遇到Too many open filesCentOS 7 默认单进程文件句柄上限 1024不够用。改/etc/security/limits.confappuser soft nofile 65535 appuser hard nofile 65535改完重新登录生效。systemd 托管的服务还要在单元文件里加LimitNOFILE65535因为 systemd 不读limits.conf的用户级配置。关于启动脚本的编码。如果启动脚本里有中文注释在 CentOS 7 的某些 locale 下可能报错。脚本统一用 UTF-8 无 BOM 保存或者干脆不写中文注释。file -i script.sh可以查看文件编码。关于 jar 包内的配置文件。Spring Boot 的配置加载顺序里外部config/目录优先级高于 jar 内部。但如果你把配置放在 jar 同级的application.yml不在config/子目录优先级也高于 jar 内部。这个细节在官方文档里有但很多人没注意导致改了外部配置不生效其实是放错了位置。关于多环境配置。用--spring.profiles.activeprod指定环境配合application-prod.yml。启动参数里加这个比在 jar 包里改配置灵活得多。systemd 单元文件的ExecStart里带上这个参数即可。关于磁盘监控。日志和 dump 文件是磁盘杀手建议加个简单的监控脚本磁盘使用率超过 80% 就告警。df -h配合 cron 定时检查或者用现成的监控系统。我遇到过日志没配轮转一周写满磁盘导致应用崩溃的情况从那以后日志轮转和磁盘监控成了部署标配。关于部署文档。每次部署的步骤、参数、遇到的问题都记下来形成团队内部的部署手册。新人接手时不用从头踩坑老手回看时也能快速回忆。文档不用多正式一个 Markdown 文件放在项目仓库里就行关键是持续更新。这套流程从最早的nohup裸跑到现在的 systemd 托管加软链接版本管理中间踩过的坑基本都在这了。核心就一句话让部署过程可重复、可回滚、可观测。做到这三点jar 包在 CentOS 7 上跑起来就不是什么难事。
阅读完成 · 觉得有帮助?