如果你刚接触Linux服务器,搜“Tomcat安装”的教程,大概率会得到一套标准动作:下载tar.gz、解压、改server.xml端口、执行startup.sh,然后把8080端口一放通,浏览器能打开那只小猫就算成功。我第一次装的时候也是这么干的,结果上线第二天就被运维同事拉去开会——日志把磁盘写满了、Manager后台裸奔在公网、进程被kill -9后留下半截PID文件,最尴尬的是连自己Tomcat用的是哪个JDK都说不清楚。
后来在好几个项目里反复部署、扩容、排障,才慢慢把“装Tomcat”这件事从“能跑”进化到“跑得明白”。今天这篇文章不打算再重复一份tar.gz解压指南,而是把Linux上安装部署Tomcat时那些容易被略过、但一旦出问题就很要命的细节摊开讲:从版本选型、目录规范、启动原理,到systemd托管、日志切割、多实例共存。适合两类人看:一是刚接触Linux服务部署的开发者,二是在生产环境里想把Tomcat装得更规范、少返工的运维同学。
1. 版本选型先过一遍:Tomcat配什么JDK,第一步就拦人
很多教程把第一步直接写成“下载Tomcat 9”,但没人解释为什么是9,而不是8.5或者10。这一步如果选错,后续所有配置都是在错误的地基上盖楼。
1.1 Tomcat版本和Java版本的对应关系
Tomcat本质上是一个用Java写的Servlet容器,它对Java版本有硬性要求,装之前务必先确认机子上Java的版本。
| Tomcat版本 | 最低Java要求 | 主流推荐Java版本 | 注意点 |
|---|---|---|---|
| Tomcat 8.5 | Java 7 | Java 8 | 老项目常用,但已停止维护 |
| Tomcat 9.0 | Java 8 | Java 8 / 11 | 目前最稳的长期使用版本 |
| Tomcat 10.0 | Java 8 | Java 11 | 命名空间从javax改为jakarta |
| Tomcat 10.1 | Java 11 | Java 11 / 17 | 需要较新JDK,老应用迁移成本高 |
| Tomcat 11 | Java 17 | Java 17 | 最新主线,适合纯新项目 |
这里最容易被坑的地方是Tomcat 10。Tomcat 10开始把Java EE规范迁移到了Jakarta EE,原来代码里写的javax.servlet.*全部变成了jakarta.servlet.*。如果你手里是一个老项目,代码里全是javax.servlet,部署到Tomcat 10上大概率直接抛ClassNotFoundException。别问我是怎么知道的——我有一个模拟项目X就是因为看上了新版本,结果一启动一连串包找不到,最后老老实实退回9.0.98。
1.2 JDK安装的取舍
Linux上装JDK的方式主要有三种:发行版软件源安装、手动解压官方tar.gz、用容器镜像。我个人的建议是,生产环境优先考虑发行版软件源自带的OpenJDK,例如在Debian系发行版上可以直接安装openjdk-11-jdk。好处是能跟着系统更新补丁,权限和路径也规整,不用手动维护PATH。
如果必须用特定小版本的JDK,那就手动解压到/usr/lib/jvm/下,然后用update-alternatives管理版本切换。有一点提醒:Tomcat本身不关系你用的是OpenJDK还是Oracle JDK,只要JAVA_HOME指对就行。别在上面纠结太多。
装完之后验证一下:
java -version echo $JAVA_HOME很多新手栽在第二步——java -version有输出,但JAVA_HOME是空的。这是因为系统默认java命令可能来自/usr/bin/java的软链接,环境变量根本没设置。Tomcat启动脚本偏偏只看JAVA_HOME,不看PATH,所以这一步没做好,后面startup.sh大概率跑不起来。
2. 下载解压之外,部署路径和目录结构才是长期项目的地基
Tomcat官网的下载页里有Binary Distributions、Source Distributions,Binary下面又分Core、Full distribution、Deployer等。日常部署只需要Core分类下的tar.gz包,后缀带src的、带windows-x64字样的都不要碰。
2.1 目录规范比解压动作更重要
很多教程随便让你解压到/root/apache-tomcat-9.0.98,然后直接用root跑,这在开发机没问题,上一线就全是事。我建议按这个布局来:
/opt/ ├── tomcat9 # 固定路径,不携带版本号 │ ├── bin/ │ ├── conf/ │ ├── lib/ │ ├── logs/ │ ├── temp/ │ ├── webapps/ │ └── work/ └── tomcat9-9.0.98 # 实际解压目录实际解压时先解到带版本号的目录,再做软链接ln -s tomcat9-9.0.98 /opt/tomcat9。这样以后升级只需要解压新版,改一下软链接,路径在脚本和配置文件里完全不用动。
创建普通用户运行Tomcat也是规范化的关键一步:
useradd -r -s /sbin/nologin tomcat chown -R tomcat:tomcat /opt/tomcat9Tomcat不在root下运行,主要是避免WEB应用有漏洞时直接拿到root权限。-s /sbin/nologin表示这个用户不能登录shell,只用来跑服务。
2.2 七个目录各管一摊,得拎清楚
| 目录 | 作用 | 实操建议 |
|---|---|---|
| bin | 启动/关闭脚本 | 主要用startup.sh、shutdown.sh、catalina.sh |
| conf | 配置文件 | server.xml、context.xml、web.xml、tomcat-users.xml |
| lib | Tomcat的jar包 | 不要乱放第三方jar,容易引发类冲突 |
| logs | 日志输出 | 重点是catalina.out和localhost日期日志 |
| temp | 临时文件目录 | 定期清理即可 |
| webapps | 应用发布目录 | 生产一般不用热部署,只丢war包 |
| work | JSP编译后的class | 无需手动操作,删了会自动重建 |
有个冷知识:work目录去缓存的效果立竿见影。有时候你更新了一个JSP页面,发现改动不生效,第一反应是重启,但重启慢还影响在线的请求。试一下把work目录下的编译产物清掉,只留着JSP让Tomcat重新编译,速度会快很多,这在老旧的JSP应用上很实用。
3. JAVA_HOME/catalina.sh/端口映射:启动背后那几个绕不开的变量
安装完Tomcat后,第一个./startup.sh如果直接成功,你可能会觉得这节没什么好看的。但如果报错,多半就集中在环境变量、启动方式、端口监听这三件事上。
3.1 setenv.sh:环境变量不要到处写
网上有教程让你把JAVA_HOME写进/etc/profile,这能用,但特别脏。更规范的做法是在Tomcat的bin目录下新建一个setenv.sh文件,因为catalina.sh脚本启动时会自动检测并加载它。这个文件对每台机器的个性化配置做了统一收纳,以后换机器只需要拷一个文件就够了。
export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64 export CATALINA_HOME=/opt/tomcat9 export CATALINA_BASE=/opt/tomcat9 # 内存参数建议也放这里,后面第6章会细讲 CATALINA_OPTS="-Xms512m -Xmx1024m -XX:MetaspaceSize=256m"CATALINA_HOME是Tomcat安装位置,CATALINA_BASE是实例位置。单实例时两者一致,多实例时BASE指向具体实例目录,这一点在第7章再展开。
3.2 startup.sh的本质是catalina.sh的壳
执行startup.sh时,它会调catalina.sh start。真正的Java进程是在后台拉起的,标准输出会重定向到logs/catalina.out。所以启动完不要干等,要养成看日志的习惯:
/opt/tomcat9/bin/startup.sh tail -f /opt/tomcat9/logs/catalina.out如果启动失败,catalina.out里会有明确的异常线索。想在前台直接观察日志和进程状态,可以用/opt/tomcat9/bin/catalina.sh run,这在调试阶段特别好用。
关停时一定要用:
/opt/tomcat9/bin/shutdown.sh不要养成kill -9的习惯,这会导致一些临时文件和PID文件残留,下次启动可能报端口被占用。
3.3 端口到底有几个,都听谁的
Tomcat的默认端口配置在conf/server.xml里,主要就这几个:
| 端口 | 用途 | 默认状态 |
|---|---|---|
| 8080 | HTTP请求监听 | 默认开放 |
| 8005 | SHUTDOWN关闭命令端口 | 本地监听,9之后屏蔽部分风险 |
| 8009 | AJP协议端口 | 9默认有,10.1后默认关闭 |
| 8443 | HTTPS监听 | 默认未配置 |
很多NAT场景下,你从外网访问8080打不开,第一反应是防火墙,第二反应是安全组,但其实还有一个地方经常被忽略——server.xml里Connector的address属性。如果你的机器有多块网卡,或者要对外提供服务,建议明确定义监听地址:
<Connector port="8080" protocol="HTTP/1.1" address="0.0.0.0" connectionTimeout="20000" redirectPort="8443" />address="0.0.0.0"表示监听所有网卡,如果是127.0.0.1就只能本机访问。这个字段写错的时候最迷惑,因为在本机curl怎么都通,出去就不通。
4. 管理端不是默认就开放:Tomcat 9以后的安全边界
Tomcat自带的webapps目录里默认有ROOT、docs、examples、manager、host-manager这五个应用。生产环境部署时,docs和examples是纯累赘,建议直接删除或改名,省得被扫描器当靶子。
4.1 Manager应用的默认限制
Tomcat 9开始,manager和host-manager这两个管理应用默认做了严格的地址限制,只有本机(127.0.0.1)能访问。如果你在浏览器里访问/manager/html,大概率看到一个403页面,上面写着“Your client is not allowed to access internal request”。
这个限制藏在conf/Catalina/localhost/manager.xml:
<Context privileged="true" antiResourceLocking="false" docBase="${catalina.home}/webapps/manager"> <Valve className="org.apache.catalina.valves.RemoteAddrValve" allow="127\.\d+\.\d+\.\d+|::1|0:0:0:0:0:0:0:1" /> </Context>如果想要远程管理,需要把allow改成你的IP段,例如allow="192.168\.1\.\d+"。但我要多说一句:在公网环境里把Manager开到公网IP,和把钥匙挂在门口没有区别。即使你设置了tomcat-users.xml里的用户密码,也不建议直接暴露。实在要用,至少做两件事:一是只放通特定来源IP,二是把默认的8080端口改掉。
4.2 管理用户千万别用默认账号
很多教程顺手教你在tomcat-users.xml里加manager-gui角色和用户,初衷是方便,但风险特别大。Tomcat的默认用户列表是空的,一旦你加了一个弱口令用户,就相当于给攻击者递了一把开Manager后台的钥匙。而且Manager后台不止能看JSP状态,还能上传部署WAR包,这等于直接给了WebShell。
我现在的习惯是,tomcat-users.xml里只保留需要的最小权限,密码至少20位随机。还要把conf/tomcat-users.xml的权限收一下:
chmod 600 /opt/tomcat9/conf/tomcat-users.xml这条命令看着平淡,但能挡掉同机其他低权限用户读你的密码文件。
5. 用systemd接管Tomcat进程:服务化配置和两个容易翻车的细节
手动startup.sh适合开发和临时测试,生产环境里还是建议用systemd托管。好处是开机自启、崩溃自动拉起、日志走journald,和别的服务一起用systemctl status tomcat统一管理。
5.1 服务文件这样写
在/etc/systemd/system/tomcat.service里创建文件:
[Unit] Description=Apache Tomcat 9 After=network.target [Service] Type=forking User=tomcat Group=tomcat Environment=JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64 Environment=CATALINA_HOME=/opt/tomcat9 Environment=CATALINA_BASE=/opt/tomcat9 ExecStart=/opt/tomcat9/bin/startup.sh ExecStop=/opt/tomcat9/bin/shutdown.sh TimeoutSec=60 Restart=on-failure [Install] WantedBy=multi-user.target配置好后执行:
systemctl daemon-reload systemctl enable --now tomcatType=forking表示启动命令会fork出子进程后返回,这和startup.sh的行为匹配。如果用Type=simple,systemd会认为主进程没有保持在前台,状态判断容易出错。
5.2 坑一:systemd启动成功但Tomcat没起来
这是典型的“服务状态看着没问题,实际进程死活找不到”的诡异场景。原因是startup.sh在后台启动了Java进程,但脚本本身很快就退出了,systemd可能以为启动失败。排查时先看journalctl -u tomcat -e,再单独手动跑一次startup.sh对比日志,基本能定位出是JAVA_HOME没读到,还是权限问题。
5.3 坑二:shutdown.sh执行了,Java进程没退干净
另一个常见问题是systemctl stop tomcat后,ps -ef | grep java还能看到残留进程。原因是systemd默认用KillMode=control-group,停止服务时会向整个控制组发SIGTERM,但shutdown.sh的优雅关停走的是8005端口的SHUTDOWN指令,两个机制配合不好的话,主进程没等子进程退干净就结束了。
我的处理办法是把服务文件里的KillMode改成mixed,并加上合理的TimeoutStopSec:
KillMode=mixed TimeoutStopSec=60这样既能保证shutdown.sh有足够时间做优雅关停,又不会让残留进程一直挂在那里。遇到过几次“端口明明释放了,但8080还是占用”的怪现象,基本都是这个原因。
6. 日志不切割会磁盘爆炸:catalina.out处理与JVM内存参数
Tomcat的日志体系里,catalina.out是Java进程的标准输出,所有System.out打出来的内容、未捕获异常、JVM崩溃信息都会跑到这里。问题是这个文件默认不会自己轮转,日志量一大,它能把磁盘撑满,在线服务直接不可用。
6.1 logrotate轮转配置
Linux自带的logrotate是好帮手,不需要装额外工具。在/etc/logrotate.d/tomcat里创建文件:
/opt/tomcat9/logs/catalina.out { daily rotate 7 compress missingok copytruncate create 0640 tomcat tomcat }几个参数的含义说一下:daily每天切一次;rotate 7保留7份旧日志;compress对旧日志做gzip压缩;copytruncate是先复制文件内容再清空原文件,这样Tomcat进程不需要重启。这一点很重要,因为直接move掉文件的话,Tomcat仍然往旧的inode写数据,磁盘空间根本不会释放。
6.2 JVM参数:内存不是越大越好
Tomcat的启动参数主要写在setenv.sh里的CATALINA_OPTS上。生产环境我一般这样起步:
CATALINA_OPTS="-Xms1024m -Xmx1024m -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m"-Xms和-Xmx写同一个值,避免了JVM频繁扩容缩容带来的性能抖动,这在服务端是很常见的一种做法。比如物理机内存16G,留4G给系统和其他进程,Tomcat堆给4G,其他进程堆给2G,剩下的留给文件缓存,就比较均衡。
GC的选型上,Java 8默认的ParallelGC适合吞吐量优先;服务延迟敏感的场景可以尝试G1。我的经验是,4G以内的堆,Parallel和G1差距没有想象的夸张,真正影响稳定性的是别把-Xmx设得太大——堆一旦进入swap,GC就会变成灾难。线上见过一台8G内存的虚拟机,硬是给Tomcat配了6G堆,结果高峰期整机swap不断,Full GC频繁到每秒一次,这种问题加机器比调参数更解决问题。
7. 单机跑多个Tomcat实例:用CATALINA_BASE做到隔离和共存
同一个机器上部署多个Tomcat的需求其实不少:跑不同版本的JDK应用、隔离不同团队的项目、或者做灰度部署。直接复制一份完整的Tomcat目录当然能跑,但升级维护时要改好几份,很累。正确的做法是把CATALINA_HOME和CATALINA_BASE拆开。
7.1 什么是CATALINA_BASE
CATALINA_HOME是Tomcat的安装目录,放bin、lib这些公用文件。CATALINA_BASE是实例目录,每个实例只需要自己的conf、logs、temp、webapps、work。这样一份Tomcat程序,可以被多个实例共享,升级程序只需要替换HOME,各实例的配置完全不动。
手动创建实例的步骤:
mkdir -p /opt/tomcat-instance/demo1 cp -r /opt/tomcat9/conf /opt/tomcat-instance/demo1/ cp -r /opt/tomcat9/logs /opt/tomcat-instance/demo1/ cp -r /opt/tomcat9/temp /opt/tomcat-instance/demo1/ cp -r /opt/tomcat9/webapps /opt/tomcat-instance/demo1/ cp -r /opt/tomcat9/work /opt/tomcat-instance/demo1/ chown -R tomcat:tomcat /opt/tomcat-instance/demo1启动时指定BASE:
CATALINA_BASE=/opt/tomcat-instance/demo1 /opt/tomcat9/bin/catalina.sh start7.2 多实例必须处理的三个点
同一个Tomcat程序开多个实例,最麻烦的不是启动脚本,而是资源配置冲突。
一是端口冲突。每个实例的conf/server.xml里,8080、8005、8009这三个端口都要改成不同的值。我有一个实例A用了8080、8005、8009,实例B用了8081、8006、8010,这个对应关系最好写进实例目录下一个简短的README里,不然半年后回来看,根本记不住哪个端口是谁的。
二是日志隔离。日志默认输出到当前BASE的logs目录,这点Tomcat做得很老实。唯一要留意的是日志切割和监控要分别指向每个实例的目录。
三是环境变量的作用域。多个实例启动时,如果直接在shell里export CATALINA_BASE,会串到下一个实例。用一行命令来启动比较拽而且不易错:
sudo -u tomcat CATALINA_BASE=/opt/tomcat-instance/demo1 /opt/tomcat9/bin/catalina.sh start多次踩坑之后,我自己最深的体会是:Tomcat部署这件事,八成的问题不是Tomcat本身,而是环境变量、权限、端口和日志这些周边细节。把这几个点管明白,安装部署就是一条直线。
最后分享一个小技巧。排查Tomcat问题的时候,我习惯先看setenv.sh里配了什么、再看server.xml改了什么、最后才看catalina.out。很多所谓“环境问题”,最后追进去都是配置文件被改得很乱。如果你也不想半年后被自己留下的配置坑到,建议从第一天起就把Tomcat的配置文件纳入版本管理,任何一个改动都在注释里写清楚原因。等哪天真的线上出问题,你会感谢当初那个愿意多写两行注释的自己。