很多人在Linux上部署Tomcat时,最烦的一件事就是:手动执行/opt/tomcat/bin/startup.sh一切正常,8080端口秒开,可机器只要一重启,Tomcat就当场"消失"。登上去一看,ps -ef | grep java什么都查不到。这时候有人让你写rc.local,有人建议加init.d,还有人折腾一晚上发现根本没用。说实在的,这类问题我接手过不下二十次,九成的坑都出在同一个地方:把Tomcat的启动链路和Linux的服务管理机制之间的衔接点搞错了。
先给结论:在现在的Linux发行版上,正确做法是用systemd托管,老SysV系统才考虑init.d脚本。但真正完整做下来你会发现,难点往往不在服务文件本身,而在JAVA_HOME、运行用户、目录权限这些被一笔带过的细节上。下面这篇文章,我按实际操作的节奏,把Linux上设置Tomcat开机启动这件事彻底讲清楚。
1. 开机起不来的根源:Tomcat"不合群"的启动模型
先提一个很少被注意的事实:Tomcat官方提供的startup.sh和shutdown.sh,设计目标就是"前台跑一下就走"。你执行startup.sh之后,脚本内部会调用catalina.sh start,拉起一个JVM进程,然后把控制权交还给当前终端。这个行为模式,和Linux服务管理器期望的"由我全权托管进程生命周期"完全不同。
1.1 systemd的进程模型与Tomcat实际行为的差异
systemd管理服务时,核心任务是找到"主进程"。它通过追踪主进程来控制服务状态,大体分两种:Type=simple是systemd直接拉起进程并常驻;Type=forking则是systemd启动一个父进程,父进程在派生守护子进程后退出,systemd把子进程认作服务本体。
Tomcat恰好落在第二种:catalina.sh start会启动一个JVM,但这个JVM的诞生方式并不是标准的daemon流程,它更像是"从当前shell剥离出去"的独立进程。于是问题就来了——如果服务文件里Type类型写错,或者执行路径、环境变量没让systemd找到正确的脚本,systemd会在超时后判定启动失败,表现就是"开机了、Tomcat没起来"。
1.2 手动能跑、开机就挂的三个高频根因
我把这类问题拆开看过,几乎每次都跑不出下面三个原因:
- 环境变量断层。终端里执行
startup.sh时,shell已经帮你加载了/etc/profile、~/.bashrc里的内容,JAVA_HOME、PATH都不缺。但systemd启动服务时环境是干净的,只有最基础的变量。Tomcat脚本一旦找不到JAVA_HOME,就会报"找不到java"然后退出。 - 权限不匹配。很多人图省事把Tomcat放在
/root目录下直接用root跑。但如果服务配置成以tomcat用户运行,而/opt/tomcat的属主还是root,那么JVM连logs/目录都写不进去,启动自然失败。 - 就绪顺序问题。8080端口本身不复杂,可一旦Tomcat里的应用依赖数据库或Redis,而这些外部服务还没起来,连接超时就会让启动卡在半路,最后被systemd标记为failed。
2. 实战:用systemd写一个可复用的Tomcat服务单元
现在主流的CentOS 7/8/9、Ubuntu 18.04以上都用systemd,这是最干净、最好维护的方案。我们一步步来。
2.1 环境准备:确认Java路径和Tomcat目录
开始前先拿两个信息。第一是JAVA_HOME的绝对路径:
readlink -f $(which java) # 输出类似 /usr/lib/jvm/java-11-openjdk-amd64/bin/java # 取上一级就是 /usr/lib/jvm/java-11-openjdk-amd64第二是Tomcat安装目录,下面统一按/opt/tomcat举例。这里强调一句:别把Tomcat放在用户家目录。家目录路径随用户环境变化,而systemd服务文件里写死绝对路径才最稳妥。如果还没装Tomcat,常规做法是下载tar包解压到/opt/tomcat,然后建一个专用用户:
useradd -r -s /sbin/nologin tomcat chown -R tomcat:tomcat /opt/tomcat-r表示系统用户,-s /sbin/nologin禁止登录,安全性更高。
2.2 编写tomcat.service,逐行拆解为什么这么写
创建服务文件:
vim /etc/systemd/system/tomcat.service我常用的配置如下:
[Unit] Description=Apache Tomcat 9 Web Application Container After=network.target remote-fs.target Wants=network.target [Service] Type=forking Environment="JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64" Environment="CATALINA_HOME=/opt/tomcat" Environment="CATALINA_BASE=/opt/tomcat" Environment="CATALINA_PID=/opt/tomcat/temp/tomcat.pid" Environment="CATALINA_OPTS=-Xms512m -Xmx1024m" Environment="JAVA_OPTS=-Djava.security.egd=file:/dev/urandom" PIDFile=/opt/tomcat/temp/tomcat.pid ExecStart=/opt/tomcat/bin/startup.sh ExecStop=/opt/tomcat/bin/shutdown.sh ExecReload=/opt/tomcat/bin/catalina.sh restart User=tomcat Group=tomcat TimeoutStartSec=90 SuccessExitStatus=143 KillMode=mixed [Install] WantedBy=multi-user.target每个关键项背后的逻辑:
After=network.target remote-fs.target:保证网络和文件系统就绪后再启动Tomcat。尤其当Tomcat日志写在独立挂载盘上时,文件系统没挂好,catalina.out都写不了。Type=forking:对应Tomcat启动脚本的行为——启动后台JVM后立即返回。这个要是写成simple,systemd会认为进程还没起来,最终超时失败。PIDFile:Tomcat启动时会把PID写入temp/tomcat.pid,systemd靠它追踪服务实例。动手前先确认temp目录存在且属主为tomcat。ExecStop:优先用官方shutdown.sh做优雅停机,而不是KillSignal强杀,避免请求处理到一半被中断。User=tomcat:以专用用户运行,阻止JVM获得过高权限。SuccessExitStatus=143:shutdown.sh执行后Tomcat主进程会被正常终止,返回码143(128+15),这不应该是错误。不加这行,systemd虽然不影响下次启动,但日志里会多出一条误导性的failed记录。
2.3 启用服务与生效验证
配置文件写好后:
systemctl daemon-reload systemctl enable tomcat systemctl start tomcatenable和start分开做有个好处:明确区分"注册开机启动"和"立即运行"。enable会在/etc/systemd/system/multi-user.target.wants/下创建符号链接;start则立即拉起服务。
查看状态:
systemctl status tomcat正常时Loaded行应显示enabled,Active行显示active (running)。如果Loaded显示的是disabled或static,说明[Install]段可能被注释了,或者enable动作没真正生效。
3. 老系统怎么办:init.d脚本与rc.local的合理定位
虽然新服务器基本都是systemd,但总有些存量机器跑着老版本CentOS 6,或者被运维策略限制没法迁移。这时候只能走SysV那套。
3.1 手写init.d服务脚本的要点
/etc/init.d/tomcat本质是一个接收start|stop|restart|status参数的shell脚本。网上模板很多,但很多都漏了关键的JAVA_HOME导出。我用的最简稳定版是这样:
#!/bin/bash # chkconfig: 2345 85 15 # description: Apache Tomcat # processname: tomcat JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64 CATALINA_HOME=/opt/tomcat export JAVA_HOME CATALINA_HOME case "$1" in start) if [ -f $CATALINA_HOME/temp/tomcat.pid ]; then echo "Tomcat already running" exit 0 fi su -s /bin/bash tomcat -c "$CATALINA_HOME/bin/startup.sh" ;; stop) su -s /bin/bash tomcat -c "$CATALINA_HOME/bin/shutdown.sh" ;; restart) $0 stop sleep 3 $0 start ;; status) ps -ef | grep org.apache.catalina.startup.Bootstrap | grep -v grep ;; *) echo "Usage: $0 {start|stop|restart|status}" exit 1 esac exit 0顶部写死JAVA_HOME和CATALINA_HOME是关键。随后用su -s /bin/bash tomcat -c切换运行身份,也避免root直接启动Tomcat。
注册到开机启动:
chmod +x /etc/init.d/tomcat chkconfig --add tomcat chkconfig --level 2345 tomcat onchkconfig --level 2345 on相当于在运行级别2/3/4/5下创建启动链接,和老版本的update-rc.d是同一层级的机制。
3.2 rc.local作为临时兜底
如果连chkconfig都懒得配置,/etc/rc.local还能再兜个底。在文件末尾追一行:
su -s /bin/bash tomcat -c "/opt/tomcat/bin/startup.sh"然后确保rc.local可执行:
chmod +x /etc/rc.localrc.local的执行时机非常靠后,对依赖数据库的服务反而算个优势。但缺陷同样明显:没有停止入口,要重启Tomcat只能手工找PID杀。我的态度很明确:能用systemd就绝不碰init.d和rc.local,后者只适合临时救场或一次性脚本。
4. JAVA_HOME、运行用户、SELinux:三个隐蔽的翻车点
服务文件写完,很多人以为收工了,结果一测试各种起不来。我最常接到的问题,都集中在这几个细节上。
4.1 JAVA_HOME要显式固化,不要依赖软链接
Tomcat的catalina.sh启动时会先判断JAVA_HOME或JRE_HOME是否已配置。如果两者都为空,它才去系统PATH里找java。而systemd服务默认的PATH非常精简,往往不包含你手动添加的路径。所以最稳妥的做法是在服务文件里显式声明JAVA_HOME。
这里要特别提醒:不要偷懒把JAVA_HOME写成/usr/bin/java这种软链接路径。我遇到过一位同事,JAVA_HOME指向/usr/bin/java,后来系统升级JDK版本,软链接指到新版本,Tomcat用的JDK就悄悄变了,编译好的应用直接报版本错误。用readlink -f找到真实路径写进配置,一劳永逸。
4.2 用tomcat用户跑,权限落点在哪里
可能有人觉得用root跑Tomcat省心,不用管属主。但Tomcat里跑的是Web应用,一旦应用有漏洞,攻击者拿下的是root权限的JVM进程,后面什么都拦不住了。所以生产环境务必用独立用户。
用户建好后,还要确认两处权限:
chown -R tomcat:tomcat /opt/tomcat chmod -R 755 /opt/tomcatlogs、temp、work这三个目录必须有写权限:
chown -R tomcat:tomcat /opt/tomcat/logs /opt/tomcat/temp /opt/tomcat/work如果是文件上传为主的系统,应用里配置的上传目录也要一并授权。不少机器开机起不来,就是日志目录没权限,JVM启动到一半写日志报错退出——这个报错在systemctl status里还不一定能一眼看出来。
4.3 别忽略SELinux和防火墙
CentOS上默认SELinux是enforcing,你可能遇到Tomcat进程起来了但端口监听失败的情况。日志上不一定有清晰提示,但服务状态里会冒出Permission denied。
临时验证方法:
setenforce 0 systemctl restart tomcat如果能起,那就是SELinux策略限制。生产上别直接禁用SELinux,而是用semanage调整端口或文件的上下文策略。
防火墙问题就更常见了。先看监听:
ss -antlp | grep 8080如果监听正常但外部访问不了,优先查firewalld或UFW规则。很多时候人一着急就去重启Tomcat,其实Tomcat好端端跑着。
5. 开机后Tomcat没起来的完整排查链路
这是整套流程里最值得反复看的部分。生产环境遇到"重启完就是起不来",别急着敲startup.sh,按这个顺序查,大概率十分钟定位。
5.1 第一眼:服务状态与系统日志
机器重启回来后,先从最外层看:
systemctl status tomcat --no-pager如果显示active (running)但业务访问不了,问题在应用内部,不在自启配置上。如果显示failed或inactive (dead),接着看journal:
journalctl -u tomcat --since "2025-01-01 00:00:00" --no-pagerjournal里会记录systemd视角的报错,比如ExecStart返回非0、超时、权限拒绝等。我习惯只看最近20条左右,太多反而干扰。
5.2 在systemd环境下手动复现
systemctl运行服务时的环境和终端完全不同,很多复现不了的坑都出在这里。正确的复现方式是:
systemctl stop tomcat systemctl start tomcat如果这次能起,说明服务配置本身没问题,问题出在开机阶段的时间线或依赖冲突。如果还是起不来,直接翻Tomcat自己的日志:
tail -n 100 /opt/tomcat/logs/catalina.outcatalina.out是JVM输出主通道,Java异常栈、ClassNotFoundException、端口冲突全在这里。
5.3 检查PID文件与端口的对应关系
还有一种情况:systemctl status显示active,但8080死活不通。这时候检查PID文件里的PID是否真对应活进程:
cat /opt/tomcat/temp/tomcat.pid ps -ef | grep java | grep -v grep如果PID文件存在但进程列表里找不到,说明JVM在ExecStart启动后、systemd确认活性之前就崩了。这类"启动瞬间失败"通常是JVM参数写错、CATALINA_OPTS里Xms大于物理内存、或者JDK版本与Tomcat不兼容造成的。
5.4 一个常被忽视的上游依赖
有时候配置看起来全对,就是开机时系统启动太快,Tomcat在网卡还没完全就绪时就尝试绑定地址。解法是在Unit段增强依赖:
After=network.target network-online.target Wants=network-online.target单机Tomcat不依赖外部服务时,这个不是必须的。但如果应用启动就要连数据库或Redis,那么至少要保证After=network.target写对,应用侧最好再加重试机制——因为systemd永远解决不了"数据库还没就绪"这件事。
6. 让开机自启从"能跑"进化到"好跑"
配置已经能开机自启了。但离"好跑"还差几个成熟的细节。
6.1 用setenv.sh统一管理JVM参数
bin/startup.sh会自动读取bin/setenv.sh中定义的JVM参数,这个文件默认不存在,需要手动创建。我强烈建议把Xms、Xmx、GC参数放这里,而不是堆在systemd服务文件的Environment里。原因很实际:一份服务文件可能被多个Tomcat实例复用,而每个实例的内存需求往往不同。
例如/opt/tomcat/bin/setenv.sh:
export JAVA_OPTS="-Djava.security.egd=file:/dev/urandom $JAVA_OPTS" export CATALINA_OPTS="-Xms512m -Xmx1024m -XX:+UseG1GC"创建后记得授权:
chown tomcat:tomcat /opt/tomcat/bin/setenv.sh chmod 755 /opt/tomcat/bin/setenv.sh注意,systemd服务文件里就不要再重复设置相同的CATALINA_OPTS了,否则后加载的设置会覆盖setenv.sh里的值,到时候查内存参数明明改了好几处却分不清哪边生效。
6.2 多实例部署的服务文件命名策略
一台机器跑多个Tomcat实例时,建议每个实例一个独立目录、一个独立用户、一个独立服务文件,名称直接按端口或项目代号区分:
/etc/systemd/system/tomcat-8080.service /etc/systemd/system/tomcat-8081.service复用同一个安装目录、靠环境变量切实例的做法,在自启场景下容易踩PID文件冲突的坑。每个实例如果CATALINA_BASE指向同一份,temp/tomcat.pid会被互相覆盖;即便单独设了CATALINA_PID,日志目录和work目录最好也彻底分开。
6.3 验证"真的开机自启成功"的最终手段
配置完成后,不要只依赖systemctl status确认。我自己的习惯是重启一次:
reboot机器回来之后,等一两分钟再执行:
systemctl is-enabled tomcat systemctl status tomcat --no-pager curl -I http://127.0.0.1:8080systemctl is-enabled显示enabled说明开机项注册成功;curl能拿到HTTP响应,基本就能确定自动启动链路是通的。如果curl不通但状态是active,再回头查catalina.out。
我还会顺手在服务文件里加一行:
Environment="CATALINA_OPTS=-Djava.awt.headless=true"纯后台Web服务可能用不上,但应用里有生成图表、操作图片的逻辑时,headless参数能避免不少AWT相关的诡异异常。多一个参数不会拖慢启动,能省后面排查的精力。
以我这几年的经验来说,Linux设置Tomcat开机启动配置完不是终点,真正要持续维护的是环境变化——JDK升级、服务器改IP、端口被占。好在服务文件里所有关键路径都是显式写好的,改起来有据可循。这次把流程跑通之后,以后部署别的Java服务,基本就是复制这套systemd模板,改改路径和参数就能直接上线。