1. 从零开始走一遍:这套部署流程到底在解决什么问题
1.1 适合谁读,读完能干什么
我刚接触 Linux 部署 Tomcat 那会儿,以为解压 Tomcat、把 WAR 包扔进 webapps 目录就算完事,结果被 JDK 版本不兼容、端口被占用、内存溢出、日志刷屏这些事轮番教育。折腾过几轮之后才明白,Linux 下部署 Tomcat 项目这件事,看起来是一条命令能解决的,实际上牵扯到环境准备、版本匹配、目录权限、系统服务托管、日志管理等多个环节。任何一个环节没想明白,项目上线之后都会变成半夜爬起来的理由。
这篇教程的目标很朴素:从一台裸机开始,完成 Linux 系统安装,装好 JDK,下载并启动 Tomcat,再把项目的老式 WAR 包或者新式 Spring Boot 应用部署进去。我会把每一步背后的原因讲清楚,而不只是甩给你一串命令。适合刚接手 Linux 服务器、第一次部署 Java Web 项目的后端开发,也适合准备把本地开发环境迁移到云端服务器的大三学生。不涉及复杂的集群和容器编排,先把单机部署这条路走通。
1.2 技术选型:Linux 发行版和 Tomcat 版本怎么定
部署之前先解决两个为什么:
Linux 发行版选哪个?我个人的建议是,如果是自己练手,优先选 Ubuntu 22.04 LTS 或者 Debian 12;如果是公司已有的服务器,通常以 CentOS 7 或者 RHEL 系为主。CentOS 7 已经在 2024 年 6 月正式停止维护,新项目不太推荐再往上面搭环境,但存量服务器实在太多,所以下文涉及包管理器的命令我会两边都给出,你根据自己的环境挑着用。
Tomcat 版本选哪个?这里有个很多新人踩过的大坑:Tomcat 10 开始,Java Servlet 的包名从javax.*改成了jakarta.*。如果项目里用的是旧写法javax.servlet,直接扔进 Tomcat 10 跑是跑不起来的。所以老项目老老实实用 Tomcat 9,新项目再去考虑 Tomcat 10/11。JDK 方面,Tomcat 9 通常搭配 JDK 8 或者 JDK 11;Tomcat 10.1 往上,建议 JDK 11 以上。选型原则总结成一句话:跟着项目的依赖走,而不是跟着最新版本走。
2. 准备运行环境:虚拟机、系统镜像与网络初始化
2.1 系统镜像下载和虚拟机创建
在没有物理服务器的情况下,虚拟机是最快的搭建方式。我通常用 VMware Workstation,个人使用也可以考虑 VirtualBox。两种工具的差别不大,核心步骤一致。
系统镜像下载地址需要注意:去发行版的官方站点或者校内镜像站下载,不要去第三方下载站。以 Ubuntu 为例,官方站是 ubuntu.com/download/server,国内镜像站可以用清华 tuna 或者阿里云 mirrors。下载 ISO 的时候注意看版本号,Server 版没有图形界面,适合当服务器用;如果只是想体验桌面版,可以选 Ubuntu Desktop。
创建虚拟机的流程简单走一遍:新建虚拟机,选择稍后安装操作系统,分配至少 2 核 CPU、2GB 内存,磁盘给 20GB。装系统的时候语言选英语或者中文都行,分区直接用默认的“使用整个磁盘”,不需要手动分区。设置一个普通用户账号,记得别选 root 登录,生产环境用 root 是很大的隐患。
2.2 安装完系统后必须做的网络配置
系统装好以后,第一件事是确认网络通不通。虚拟机默认网络连接方式通常是 NAT,这种方式下虚拟机可以访问外网,但外部无法直接访问虚拟机里的服务。部署 Tomcat 后做测试时,这个问题会暴露出来。
Ubuntu 下查看当前 IP 用ip addr,如果看不到明显的 IP 或者想配置静态 IP,可以看/etc/netplan/目录下的 YAML 文件。修改后执行sudo netplan apply。CentOS 7 下则可能使用nmcli或者直接编辑/etc/sysconfig/network-scripts/ifcfg-ens33这类文件,CentOS 8 以上也转向了 NetworkManager。
# Ubuntu 22.04 示例,文件 /etc/netplan/01-netcfg.yaml network: version: 2 ethernets: ens33: dhcp4: no addresses: [192.168.1.100/24] routes: - to: default via: 192.168.1.1 nameservers: addresses: [223.5.5.5, 114.114.114.114]改完以后执行sudo netplan apply。有几点经验要记住:别在服务器上配一个和网关冲突的静态 IP,否则排查思路会变成一团乱麻;另外虚拟机克隆或者重新生成 MAC 地址后,旧配置里写的 IP 可能会失效。
2.3 为什么建议用 SSH 而不是直接在虚拟机页面上操作
可能有人觉得,都打开虚拟机窗口了,直接在终端敲命令不就行了?问题在于,服务器部署完成后你大部分时间是不愿意一直开着虚拟机的图形界面的。我更推荐的方式是:在宿主机上用 SSH 客户端连过去,比如 Windows 下用 Windows Terminal 或者 MobaXterm,macOS/Linux 下直接用系统终端。
前提是安装 openssh-server:
# Ubuntu / Debian sudo apt update && sudo apt install -y openssh-server # CentOS 7 sudo yum install -y openssh-server openssh-clients sudo systemctl start sshd sudo systemctl enable sshd连接时用ssh 用户名@IP,比如ssh tomcat@192.168.1.100。使用 SSH 的好处不只是方便,后续排错、查看日志、上传 war 包,都能在一个稳定的连接里完成,也方便你复用自己熟悉的本机工具。
3. JDK 是 Tomcat 绕不过去的前置条件
3.1 JDK 版本与 Tomcat 版本的匹配关系
Tomcat 是用 Java 写的,所以它的运行离不开 JDK。我之前见过一位同事,把 Tomcat 10.1 配上 JDK 8,Tomcat 启动倒是没报错,但部署的项目一访问就 500,查了半天才发现是版本匹配问题。
Tomcat 各版本与 JDK 的最低匹配关系大致如下:
| Tomcat 版本 | 最低 JDK 版本 | 说明 |
|---|---|---|
| Tomcat 9.0 | JDK 8 | 老项目首选,兼容javax.* |
| Tomcat 10.0 / 10.1 | JDK 8 / 11 | 包名改为jakarta.*,旧项目需改造 |
| Tomcat 11 | JDK 17 | 较新,不建议新手直接上 |
如果你只是在本机随便跑个 demo,用 OpenJDK 完全够。如果你从 Oracle 官方下载 JDK,需要登录账号才能拿到,比较麻烦。所以下面的步骤我用 OpenJDK 演示。
3.2 OpenJDK 安装与环境变量配置
用包管理器安装是最省心的方式:
# Ubuntu / Debian 安装 JDK 11 sudo apt update sudo apt install -y openjdk-11-jdk # CentOS 7 安装 JDK 8 sudo yum install -y java-1.8.0-openjdk-devel安装完成后,需要设置JAVA_HOME。很多人的误区是以为java -version能输出版本号就够了,实际上 Tomcat 的启动脚本会主动找JAVA_HOME或者JRE_HOME。如果不设置,就可能在启动时报Neither the JAVA_HOME nor the JRE_HOME environment variable is defined。
先找到 Java 的安装路径:
# 查看 java 命令所在路径 which java # 通常输出 /usr/bin/java # 继续看真实路径 readlink -f /usr/bin/java在 Ubuntu 下通常显示/usr/lib/jvm/java-11-openjdk-amd64/bin/java,所以JAVA_HOME就是/usr/lib/jvm/java-11-openjdk-amd64。接下来把环境变量写进/etc/profile或者单独的文件/etc/profile.d/java.sh,我习惯用后者,卸载重装时容易清理。
sudo tee /etc/profile.d/java.sh > /dev/null << 'EOF' export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64 export PATH=$JAVA_HOME/bin:$PATH EOF # 让配置在当前会话生效 source /etc/profile.d/java.sh3.3 验证 JDK 是否真的可用
配置完之后,重新登录或者source一下,然后输入:
echo $JAVA_HOME java -versionJAVA_HOME应该输出刚才设置的路劲;java -version能看到版本号。如果java -version显示了 OpenJDK 的版本,但echo $JAVA_HOME是空的,最常见的原因是你当前的 shell 没有重新加载 profile。执行source /etc/profile或者重新开一个 SSH 会话即可。
这里说一个实际的辅助技巧:可以用update-alternatives --config java查看系统里已经安装的所有 Java 版本。如果系统里同时存在 JDK 8 和 JDK 11,一定要确认默认的java指向哪个版本。Tomcat 启动时用的是默认版本,一旦指错,项目行为可能很诡异。
4. Tomcat 下载安装:解压就能用,但要理解目录结构
4.1 下载 Tomcat 的准备工作和官网地址上的“坑”
Tomcat 官网是 tomcat.apache.org。首页会列出多个大版本,比如 Tomcat 9、Tomcat 10.1、Tomcat 11。注意,官网提供的下载包里区分了tar.gz(Linux 用)和zip(Windows 用),别下错。
点进对应版本的页面后,你会看到几个下载入口,有Binary Distributions、Source Code Distributions等。我们要的是Binary Distributions下的tar.gz包,而不是src源码包。很多新手第一次点进去,看到一堆目录就发懵,其实就是照着apache-tomcat-9.0.10X.tar.gz这个文件名找即可。下载前先记一下这个文件的完整下载链接,一会儿用wget在服务器上下载。
cd /opt # 以 Tomcat 9.0.98 为例,实际版本号以官网为准 wget https://dlcdn.apache.org/tomcat/tomcat-9/v9.0.98/bin/apache-tomcat-9.0.98.tar.gz tar -zxvf apache-tomcat-9.0.98.tar.gz # 做一个软链,方便后续升级 ln -s /opt/apache-tomcat-9.0.98 /opt/tomcat使用 dlcdn.apache.org 这个地址时要注意:Apache 的 CDN 经常会把旧版本归档到 archive.apache.org,如果你遇到 404,可以改用 https://archive.apache.org/dist/tomcat/tomcat-9/ 下面找对应版本。
4.2 解压后的目录结构:每个目录是什么职责
解压完以后,先别急着启动,把目录结构看明白,后面很多问题都能少走弯路。
| 目录/文件 | 作用 | 备注 |
|---|---|---|
bin/ | 启动、停止脚本 | startup.sh、shutdown.sh、catalina.sh |
conf/ | 核心配置目录 | server.xml、tomcat-users.xml都在这里 |
lib/ | Tomcat 运行需要的 jar 包 | 一般不用动 |
logs/ | 日志输出目录 | catalina.out是最重要的日志 |
webapps/ | 部署 Web 应用的地方 | WAR 包扔进来即可 |
work/ | JSP 编译后的 class 存放目录 | 出问题时可以清空重启 |
bin/startup.sh和bin/shutdown.sh是日常用的命令。catalina.sh则是更底层的脚本,调用它也能启动和停止,还能在前台运行 Tomcat,方便直接看日志输出:
# 前台运行,适合调试 /opt/tomcat/bin/catalina.sh run4.3 启动 Tomcat 并完成第一个验证
启动 Tomcat 之前,检查一下是否已经装好了 JDK:
/opt/tomcat/bin/startup.sh正常输出会包含Tomcat started。然后验证端口的监听情况:
ss -lntp | grep 8080看到LISTEN状态说明 Tomcat 已经起来了。在虚拟机上执行:
curl http://localhost:8080如果能返回一大段 HTML,里面包含Apache Tomcat/9.0.x,默认首页就正常了。如果想从宿主机浏览器访问,需要确保虚拟机的网络模式是桥接或者做了端口转发,同时检查防火墙。
出现Tomcat started但端口没监听时,不要慌,先去logs/catalina.out看报错,这是最直接的排除路径。
5. 把项目部署到 Tomcat:三种方式的对比与实操
5.1 方式一:webapps 下放 WAR 包(最简单)
假设你已经有一个打包好的项目,比如demo.war。把这个文件放到webapps目录下:
cp demo.war /opt/tomcat/webapps/Tomcat 默认会在启动时扫描webapps目录,发现新的 WAR 包后,自动解压并以 WAR 包名作为上下文路径来发布。比如你放的demo.war,访问路径就是http://localhost:8080/demo/。如果你希望直接通过根路径访问,需要把 WAR 包改名为ROOT.war,覆盖原来的默认首页。
这种方式的优点是省事,缺点是每次更新项目时,旧版本解压出来的目录可能会残留无关文件。所以更新时建议先把旧的解压目录和 WAR 包一起删掉,再放新的 WAR 包进去。
5.2 方式二:server.xml 配置 docBase(常用但要注意恢复机制)
如果你希望项目目录放在webapps外面,或者用自定义路径访问,可以在conf/server.xml里的Host节点下增加<Context>配置。
下面是一个例子:项目 WAR 包放在/data/projects/demo.war,让它在根路径/下访问:
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true"> <Context docBase="/data/projects/demo" path="" reloadable="true" /> </Host>这种方式适合项目文件不想混在 Tomcat 自带的 webapps 目录里的场景。需要注意:修改server.xml后必须重启 Tomcat 才能生效,而且server.xml写错了会导致整个 Tomcat 启动失败,所以修改前先备份:
cp /opt/tomcat/conf/server.xml /opt/tomcat/conf/server.xml.bak5.3 方式三:Manager 管理界面(适合测试环境)
Tomcat 自带一个 Manager 管理界面,能在线部署和卸载 WAR 包,确实方便,但默认是关闭的。需要先编辑conf/tomcat-users.xml,添加一个具有manager-gui角色的用户:
<tomcat-users> <role rolename="manager-gui"/> <user username="admin" password="your-password" roles="manager-gui"/> </tomcat-users>然后在浏览器里访问http://localhost:8080/manager/html,登录后可以上传 WAR 包并部署。这个功能我不建议在生产环境开启,因为一旦密码泄露,等于把整个应用的管理权交给了别人。即使要开,也一定记得修改默认端口、限制来源 IP。
5.4 部署后的目录权限问题
部署完成后,一个被很多新手忽略的点是文件权限。Tomcat 进程用什么用户运行,就要保证那个用户对webapps和日志目录有写权限。如果你用普通用户tomcat运行 Tomcat,但项目文件是用 root 解压到/opt/tomcat/webapps/下的,可能出现“Tomcat 能启动,项目也能访问,但日志无法写入或者某些临时文件创建失败”的诡异问题。
我习惯的做法:
# 创建专门运行 Tomcat 的用户 sudo useradd -r -s /bin/false tomcat # 把整个 Tomcat 目录属主改成 tomcat sudo chown -R tomcat:tomcat /opt/tomcat后面用这个用户来启动 Tomcat,就避免了很多权限隐性问题。
6. Tomcat 的端口、内存与日志配置
6.1 修改端口时的注意事项
默认端口是 8080,如果和别的服务冲突,需要改。配置文件在conf/server.xml,找到这一段:
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />把port改成比如9090,然后重启。注意,redirectPort="8443"是 HTTPS 的端口,一般不用改。改端口后,访问地址也要同步变化,这一点在云端服务器上特别容易被忽略,因为安全组的入站规则也要一起调整。
还有一个容易踩的坑:Tomcat 修改端口后,如果日志里出现Address already in use,八成是端口号被操作系统里某个进程占用了,这时候先ss -lntp | grep 9090看一眼,别急着反复重启。
6.2 JVM 内存参数应该放在哪里
Tomcat 启动时需要一个 JVM,默认内存大小往往不够项目使用。我见过不少项目访问一多就报java.lang.OutOfMemoryError: Java heap space,就是因为没有手动调整内存参数。
内存参数分两种:JAVA_OPTS和CATALINA_OPTS。两者的区别在于,JAVA_OPTS对所有启动路径生效,CATALINA_OPTS只在 Tomcat 启动时生效。更推荐使用CATALINA_OPTS,避免停止或执行其他命令时也加载过大内存配置。
推荐的做法是在bin目录下新建一个setenv.sh文件(Tomcat 启动脚本会自动读取它):
sudo tee /opt/tomcat/bin/setenv.sh > /dev/null << 'EOF' export CATALINA_OPTS="-Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m -Djava.awt.headless=true" EOF sudo chmod +x /opt/tomcat/bin/setenv.sh说明一下几个参数:-Xms是堆初始大小,-Xmx是最大堆大小,建议设置成一样的值,这样 JVM 启动时就直接把堆申请到位,减少运行时扩容带来的性能抖动。-XX:MaxMetaspaceSize是 JVM 元空间上限,旧项目如果用到大量 CGLIB 代理或者反射,元空间不足也会报内存溢出。机器只有 2GB 内存的话,堆设置到 1GB 已经比较高,再高有可能把系统内存吃紧,反而触发其他问题。
6.3 日志输出和排查定位
日志是排错的主要线索来源。Tomcat 的主要日志文件:
| 文件 | 记录内容 |
|---|---|
catalina.out | Tomcat 控制台输出、启动日志、未捕获的异常 |
localhost.log | 应用启动初期访问日志和报错 |
manager.log | Manager 操作记录 |
localhost_access_log | HTTP 访问日志(默认关闭,可在 server.xml 里开启) |
查看日志时,先用tail -f观察实时输出:
tail -f /opt/tomcat/logs/catalina.out如果项目部署后立即 500,多半是应用自身的异常,直接看catalina.out里的堆栈即可。如果发现日志文件长得奇快无比,检查是不是代码里的日志框架和 Tomcat 的日志框架冲突,最常见的表现是log4j与java.util.logging的桥接问题。
7. Linux 上的常见部署故障处理
7.1 访问的时候一直 404
Tomcat 启动正常,curl http://localhost:8080也能看到首页,但访问你的项目路径时 404。这种情况我见得非常多,原因通常就两类。
第一类:WAR 包没有被正确解压。看看webapps目录下有没有生成和你 WAR 包同名的目录。如果没有,去查logs/catalina.out,看是不是 WAR 包本身损坏或者解压报错。第二类:上下文路径和你以为的不一样。比如 WAR 包叫demo-1.0.war,Tomcat 会默认把上下文路径设置为/demo-1.0,而不是/demo。如果项目里配置了 context-path,也要一起对照,否则访问/demo/自然 404。
另外还有一种很坑的情况:webapps/ROOT下的默认应用没有被清理,而你的项目又在根路径下,会出现访问根路径看到 Tomcat 默认首页,看不到你的项目。处理办法是删掉webapps/ROOT目录,然后把自己的 WAR 包改名成ROOT.war,重新部署。
7.2 端口被占用
启动时报Address already in use或者BindException,处理思路是:
# 找出占用 8080 端口的进程 ss -lntp | grep 8080 # 或者 lsof -i :8080 # 结束进程(生产环境先确认能不能杀) sudo kill -9 PID有些情况下你会发现端口已经被起了另一个 Tomcat 实例,比如之前启动过一次但没关干净。这时候不要盲目 kill,先确认是不是同一个项目的旧进程,避免误杀。
这里有一个容易忽略的细节:如果你的服务器本身就是一台机器上的多个网卡,Tomcat 默认绑定0.0.0.0,所有网卡的 8080 端口都会被占用。如果只允许特定 IP 访问,请去配置防火墙,而不是让 Tomcat 绑定单个 IP(虽然 Tomcat 也支持address属性)。
7.3 内存溢出,尤其是 PermGen 和 Metaspace
Java 8 以前的老项目经常出现java.lang.OutOfMemoryError: PermGen space,改用元空间后更多出现Metaspace溢出。如果你在日志里看到这类报错,优先检查setenv.sh里面的-XX:MaxMetaspaceSize是否设置得太小,或者项目代码是否存在频繁动态生成类的逻辑。
如果是堆内存溢出,日志一般是Java heap space。先确认-Xmx配置,再看是不是存在内存泄漏。简单排查可以使用jmap导出堆转储:
# 找到 Tomcat 进程 PID ps -ef | grep tomcat # 导出堆快照 jmap -dump:format=b,file=/tmp/tomcat_heap.hprof PID然后可以用 MAT 或者 VisualVM 分析。这一步对于新手可能有点难,但至少要能看懂日志里是哪种内存溢出,才能在搜索引擎里找到正确方向。
8. 生产环境的一些建议和踩坑后的结论
8.1 别用 root 跑 Tomcat
这里想单独强调一下。很多人图省事,装完系统一直用 root 操作,Tomcat 也用 root 启动。这在测试环境问题不大,生产环境风险很高,万一应用被上传了恶意 jsp 文件,root 权限意味着服务器完全失守。
搭环境的时候多花两分钟创建专用用户:
sudo useradd -r -s /usr/sbin/nologin tomcat sudo chown -R tomcat:tomcat /opt/tomcat sudo -u tomcat /opt/tomcat/bin/startup.sh用nologin作为登录 shell,用户不能直接登录系统,但可以运行 Tomcat 和相关命令。
8.2 清理默认应用与防火墙
Tomcat 下载后自带的默认应用有docs、examples、manager、ROOT等。如果不需要 Manager 在线管理功能,生产环境建议直接删掉或者改名:
rm -rf /opt/tomcat/webapps/docs /opt/tomcat/webapps/examples /opt/tomcat/webapps/manager /opt/tomcat/webapps/host-manager同时检查防火墙。Ubuntu 下用ufw,CentOS 7 下用firewalld:
# Ubuntu sudo ufw allow 8080/tcp # CentOS 7 sudo firewall-cmd --zone=public --add-port=8080/tcp --permanent sudo firewall-cmd --reload防火墙是部署中很容易忽略的环节。我见过太多人检查了 JDK、检查了 Tomcat、检查了项目,最后发现是云服务器安全组没放行端口。顺序排查时,把防火墙和安全组放在端口监听之后一起检查。
8.3 推荐用 systemd 托管 Tomcat
startup.sh手动启动的方式适合开发环境,生产环境建议用 systemd 把 Tomcat 注册成服务,好处是开机自启、崩溃自动拉起、日志统一管理。
在/etc/systemd/system/tomcat.service下创建服务文件,大致内容如下:
[Unit] Description=Apache Tomcat Web Application Container After=network.target [Service] Type=forking User=tomcat Group=tomcat Environment="JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64" Environment="CATALINA_PID=/opt/tomcat/temp/tomcat.pid" Environment="CATALINA_HOME=/opt/tomcat" Environment="CATALINA_BASE=/opt/tomcat" ExecStart=/opt/tomcat/bin/startup.sh ExecStop=/opt/tomcat/bin/shutdown.sh RestartSec=10 Restart=always [Install] WantedBy=multi-user.target然后执行:
sudo systemctl daemon-reload sudo systemctl enable --now tomcat sudo systemctl status tomcat这样以后重启机器,Tomcat 会自动启动,不用再手动执行脚本。
8.4 最后说点实际的
这套流程走下来,本质上就是把“在一个全新的 Linux 环境里跑起一个 Java Web 项目”这件事拆成了四个部分:操作系统、JDK、Tomcat、项目包。每一部分都有独立的验证方法,不许跳步:操作系统看网络和 SSH,JDK 看java -version和JAVA_HOME,Tomcat 看端口和默认首页,项目看访问路径和日志。只要每一层都验证通过,整体部署通常就不会出大问题。
如果是部署 Spring Boot 的可执行 jar,其实不需要 Tomcat 外层容器,java -jar一条命令即可;但如果项目是以 WAR 包形式提供,或者公司规范要求必须用外部 Tomcat,那上面这套流程就是完全适用的。我实际操作中的个人体会是,环境问题占了故障的小半,剩下的多是项目配置和版本兼容,像 Tomcat 10 的javax与jakarta问题,还有 JDK 版本和依赖冲突问题,布局排起来非常繁琐。所以,装环境的时候稍微多花一点时间把版本确认好,把配置写在setenv.sh这类工程化位置,后面能省下几倍的时间。