Tomcat 部署运维
一个 WAR 包包含应用的类、依赖和 Web 资源。Tomcat 加载它,为应用建立 Context,再把 HTTP 请求交给匹配的 Servlet。部署时需要同时确定三个位置:哪个 Java 进程运行 Tomcat,哪个 Context 加载这个 WAR,以及客户端用什么路径访问它。
对于已有传统 Java Web 应用,独立 Tomcat 提供统一的容器版本与运维方式;采用可执行 JAR 的 Spring Boot 项目通常直接托管自己的进程,见 Spring Boot 部署运维。需要将 Boot 应用交给外部容器时,还要按框架要求打包 WAR、配置初始化入口和容器依赖,不能只把 JAR 改名为 WAR。
先确定容器版本与应用形态
Java、Servlet 与包名必须一起匹配
| Tomcat 分支 | 最低 Java 版本 | Servlet 规范 | 常用 API 包名 |
|---|---|---|---|
| 9.0 | Java 8 | Servlet 4.0 | javax.servlet.* |
| 10.1 | Java 11 | Servlet 6.0 | jakarta.servlet.* |
| 11.0 | Java 17 | Servlet 6.1 | jakarta.servlet.* |
最低 Java 版本是容器要求,应用和依赖还可能要求更高版本。Tomcat 9 到 10 及以上涉及 Java EE 到 Jakarta EE 的命名空间迁移,旧 WAR 不能仅靠更新容器自动兼容。选择时核对应用框架支持范围、编译目标、依赖与容器分支,并在该分支内采用受支持的安全补丁。Tomcat 官方版本表集中列出了规范和 Java 对应关系。
Tomcat 实现 Servlet、Pages 等 Web 规范,并非完整 Jakarta EE 应用服务器。应用依赖其他企业服务时,需要确认由什么库或服务提供,不能认为安装 Tomcat 就具备所有 Jakarta EE 能力。
示例使用 Tomcat 11.0.25 与 Temurin 25.0.4,Servlet 源码按 Java 17 目标编译。新部署可以选择别的受支持组合,但更换版本后应重新验证 WAR、JSP、启动参数和依赖。
从连接进入一个 Context
Tomcat JVM
└── Server
└── Service
├── Connector:监听地址、端口、HTTP 协议与连接限制
└── Engine:把请求交给虚拟主机
└── Host:主机名与应用部署设置
├── Context /app:一个 Web 应用
│ └── Filter → Servlet → 应用代码
└── Context /other:另一个 Web 应用Connector 接受连接,Engine 和 Host 选择应用范围,Context 代表应用的配置与生命周期。一个 Tomcat 中多个应用共享 JVM 堆、CPU 和进程故障风险;各自的 Web 应用类加载器不能提供进程级资源隔离。
常见交付有打包的 WAR、解压后的应用目录,以及指向外部 WAR 的 Context 描述文件。自动部署时,app.war 通常对应 /app,ROOT.war 对应根路径;显式描述文件 conf/Catalina/localhost/app.xml 则由文件名确定 /app。部署说明解释了文件命名和不同部署方式。
同一个应用不要同时放入自动扫描目录,又用描述文件从另一位置部署。重复来源会增加重部署和路径冲突的排查难度。
HOME 与 BASE 分开管理
CATALINA_HOME=/usr/local/tomcat
├── bin/ 启动脚本与容器组件
└── lib/ Tomcat 库
CATALINA_BASE=/srv/tomcat
├── conf/ 实例配置、Context 描述文件
├── webapps/ 此示例保持为空
├── logs/ 访问日志
├── work/ JSP 编译等工作文件
└── temp/ Java 临时文件
/srv/releases/app.war 只读应用制品HOME 管理容器分发文件,BASE 管理一个实例的配置与运行目录。多实例可以共用只读 HOME,但各自需要独立 BASE、监听端口和进程托管。实际目录查找顺序与启动变量见 Tomcat RUNNING。
运行用户只获得所需的写权限:日志、工作目录、临时目录,以及应用单独配置的数据目录。HOME、WAR 和配置由部署管理员维护。让运行用户能改写应用代码或启动配置,会使一次应用入侵更容易形成持久修改。
部署一个可请求的 WAR
容器路线:构建、启动与返回结果
下载并解压 Tomcat 示例工程。在 Linux Bash 中执行,需要 Docker Engine、Compose 插件和空闲回环端口 18093,当前用户需有 Docker 操作权限。
cd tomcat-lab
docker build --build-arg APP_RELEASE=r1 -t local/tomcat-lab:r1 .
docker build --build-arg APP_RELEASE=r2 -t local/tomcat-lab:r2 .
APP_RELEASE=r1 docker compose up -d --wait --pull never
curl -q --noproxy '*' --fail-with-body --max-time 5 \
http://127.0.0.1:18093/app/api/health成功响应为 {"status":"UP","release":"r1"}。访问 http://127.0.0.1:18093/app/ 能看到“JSP 已渲染”和应用路径 /app。这是应用真实实现的接口,不依赖默认欢迎页。
Dockerfile 的构建阶段使用容器自带的 Servlet API 编译:
javac --release 17 -cp /usr/local/tomcat/lib/servlet-api.jar \
-d web/WEB-INF/classes src/LabServlet.java
jar --create --file app.war -C web .这两行由 Dockerfile 在已经准备好的构建目录执行。编译和打包以 UID/GID 10001 运行;安装系统软件与准备只读文件由镜像构建阶段的 root 执行。运行镜像同样设置 USER 10001:10001,不把构建阶段的临时目录带入应用目录。
WAR 中 WEB-INF/classes 保存应用类,WEB-INF/lib 保存应用依赖,WEB-INF/web.xml 声明 Servlet 与 /api/* 映射。容器提供 Servlet API,示例没有把这份 API JAR 再塞进 WAR。Maven 项目通常将相应容器 API 声明为 provided,避免重复提供。
用描述文件固定部署来源
实例的 conf/Catalina/localhost/app.xml 为:
<Context docBase="/srv/releases/app.war" reloadable="false" unloadDelay="10000">
<Manager pathname=""/>
</Context>Host 设置 autoDeploy="false"、deployOnStartup="true"、unpackWARs="false"。启动时加载指定 WAR,运行中不自动扫描文件变化,也不向只读目录解压。JSP 生成文件写到 BASE 的 work。Context 的属性含义见 Context 配置,扫描和解压条件见 Host 配置。
reloadable 用于类或库变化后的重新加载,生产环境通常关闭。重新加载会重建应用上下文,不能用它替代带有发布验证和回退步骤的升级。
Manager pathname="" 明确关闭该实验的会话落盘。Compose 用 tmpfs 提供日志、work、temp,并将根文件系统设为只读。这样容易观察重建后的状态变化,但访问日志不会跨容器重建保存;正式部署应使用日志采集或专用日志存储,并安排轮转与容量上限。
docker compose exec -T app id
docker compose exec -T app sh -ec '
test ! -w /srv/releases/app.war
test ! -w /srv/tomcat/conf/server.xml
test -w /srv/tomcat/work
'
docker compose logs --tail=80 app预期 UID 为 10001,权限检查退出码为 0。应用目录或配置可写时应先修正镜像和挂载权限;启动日志中若有部署异常,继续健康检查也不会把失败应用变成可用状态。
Linux 安装包与 systemd 路线
不使用容器时,可从 Tomcat 官方下载页取得指定版本的二进制归档、SHA-512 校验文件和签名。比对摘要用于发现文件变化;确认发布者还需要验证签名及独立可信的签名密钥指纹。不要从同一个不可信下载位置取得文件和摘要后就认定来源可信。
以下是专用 Linux 测试主机的独立路线,不与上面的容器同时运行。管理员已安装受支持的 JDK 到 /opt/java,并将验证后的 Tomcat 11.0.25 解压到 /opt/apache-tomcat-11.0.25。使用示例配置和刚才构建的 WAR:
# 在 tomcat-lab 工程目录执行;先确认下面的专用用户名和目录未被其他服务占用。
getent passwd tomcat-lab
ls -ld /srv/tomcat /srv/releases 2>/dev/null如果已经存在,先核对用途,不继续覆盖。仅在全新专用目录下初始化:
sudo useradd --system --home-dir /srv/tomcat --shell /usr/sbin/nologin tomcat-lab
sudo install -d -o root -g root -m 0755 /srv/tomcat /srv/releases
sudo install -d -o root -g root -m 0755 /srv/tomcat/conf /srv/tomcat/webapps
sudo cp -a /opt/apache-tomcat-11.0.25/conf/. /srv/tomcat/conf/
sudo install -d -o root -g root -m 0755 /srv/tomcat/conf/Catalina/localhost
sudo install -d -o tomcat-lab -g tomcat-lab -m 0750 \
/srv/tomcat/logs /srv/tomcat/work /srv/tomcat/temp
docker create --name tomcat-war-export local/tomcat-lab:r1
docker cp tomcat-war-export:/srv/releases/app.war ./app-r1.war
docker rm tomcat-war-export
sudo install -o root -g root -m 0644 app-r1.war /srv/releases/app.war
sudo install -o root -g root -m 0644 server.xml logging.properties /srv/tomcat/conf/
sudo install -o root -g root -m 0644 app.xml /srv/tomcat/conf/Catalina/localhost/app.xmlWAR 也可以直接从可信制品库取得,无需在目标主机安装 Docker。导出容器没有启动,删除的只是这次命名的临时容器,导出的文件仍保留。
将宿主路线 server.xml 中 Connector 的 address 从 0.0.0.0 改为 127.0.0.1,由同机反向代理提供外部入口。确认 HOME 由管理员持有且普通用户不可写。安装附带的服务文件:
sudo install -o root -g root -m 0644 tomcat-lab.service /etc/systemd/system/tomcat-lab.service
sudo systemd-analyze verify /etc/systemd/system/tomcat-lab.service
sudo systemctl daemon-reload
sudo systemctl enable --now tomcat-lab
sudo systemctl status tomcat-lab --no-pager
curl -q --noproxy '*' --fail-with-body --max-time 5 \
http://127.0.0.1:8080/app/api/health服务文件使用 Type=simple 和 catalina.sh run 在前台运行,不使用先后台启动再追踪另一个 PID 的方式。它指定独立 HOME/BASE、普通账号、只读文件系统保护,以及三处可写目录。服务参数与状态判断可参阅 Linux 与服务器基础。systemd-analyze verify 只检查单元配置,实际启动、监听与请求仍要分别验证。
代理、会话与资源配置如何影响运行
代理必须保持应用知道的路径与协议
假定同机 Nginx 已按 Nginx 部署建立 HTTPS 虚拟主机,Linux 原生 Tomcat 监听 127.0.0.1:8080,可在该虚拟主机中加入:
location = /app { return 308 /app/; }
location /app/ {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 3s;
proxy_read_timeout 30s;
}这里保留 /app/ 前缀,与 Context 一致。若改为把外部 / 映射到内部 /app/,还必须检查重定向、Cookie Path、页面中的绝对地址和 WebSocket 路径。只改一次 proxy_pass 并不能修正应用生成的所有链接。
Tomcat 接收到的是代理连接。需要让应用识别原客户端地址和 HTTPS 时,可在 Engine 下、访问日志 Valve 之前配置:
<Valve className="org.apache.catalina.valves.RemoteIpValve"
internalProxies="127\.0\.0\.1"
remoteIpHeader="X-Forwarded-For"
protocolHeader="X-Forwarded-Proto"/>此例只信任来自同机回环 IPv4 的代理,且入口覆盖客户端传入的转发字段。容器跨网络代理应改成实际可信代理地址,并从网络上禁止客户端绕过入口直连后端。不要为方便匹配而信任所有来源。RemoteIpValve 与访问日志的请求属性选项见 Valve 文档。
HTTPS 入口下还要验证登录重定向不会退回 HTTP,Cookie 的 Secure、HttpOnly、SameSite 与 Path 符合业务。实验只使用 HTTP 和无认证计数器,不应原样承担登录安全。
观察会话在重建前后的变化
在容器实验目录中执行,Cookie 文件只保存本实验的会话:
curl -q --noproxy '*' --fail-with-body --max-time 5 \
--cookie-jar /tmp/tomcat-lab.cookies \
http://127.0.0.1:18093/app/api/session
curl -q --noproxy '*' --fail-with-body --max-time 5 \
--cookie /tmp/tomcat-lab.cookies \
http://127.0.0.1:18093/app/api/session同一会话预期先返回 visits:1,再返回 visits:2。如果第二次仍为 1,先检查 Cookie 是否被发送、请求路径是否落在 Cookie Path 内,以及是否访问了另一个实例。
示例会话只存内存且禁用落盘。重建容器后再次携带原 Cookie,计数重新从 1 开始。负载均衡场景可以使用粘性会话、容器会话复制或外部会话存储,但三者的故障行为不同:粘性路由没有为内存增加副本;复制或外置存储需要处理序列化、过期、网络故障和应用升级兼容。Session Manager 配置说明了容器内会话管理选项。
登录状态等重要会话迁移应单独设计。多个实例同时访问时,还应检查服务端会话更新是否存在并发覆盖,而非只验证一次登录。
连接数、线程数与下游等待
示例 Connector 使用 64 个最大工作线程、512 个最大连接和 64 的 acceptCount,用于小型实验,不作为生产容量推荐值。
| 参数 | 控制的对象 | 常见误判 |
|---|---|---|
maxThreads | 同步请求执行所用工作线程上限 | 增大线程数就一定提升吞吐 |
maxConnections | Connector 接受和处理的连接数量 | 连接数就是正在执行业务的请求数 |
acceptCount | 达到连接上限后,操作系统连接等待队列的请求值 | 它是应用内部任务队列长度 |
connectionTimeout | 建连后等待请求等连接阶段超时 | 它限制了一切业务执行时间 |
keepAliveTimeout | 等待同一连接下一次 HTTP 请求 | 它是数据库连接池超时 |
一个同步请求可能长时间等待数据库连接或远程响应。此时增加 HTTP 工作线程会使更多请求一起等待下游,内存和超时数量反而上升。先查看线程栈和连接池等待,再根据真实耗时、依赖容量与流量设置限制。HTTP Connector 文档给出了参数的准确作用阶段。
配置共享 Executor 后,Connector 的线程配置由 Executor 接管;要去对应的 Executor 上设置线程数和队列。虚拟线程可降低某些阻塞场景的线程成本,数据库连接、外部限额和内存仍需限制。Executor 文档列出了普通线程池与虚拟线程实现。
应用依赖与 JNDI 数据源
应用自己的库优先放入 WAR 的 WEB-INF/lib,不要为“所有应用都能找到”而把不同版本统一复制进容器 lib。类由不同加载器定义时,即使全限定名相同,也可能无法相互转换;旧版本 JAR 还可能引起 NoSuchMethodError。定位时结合 WAR 内容、依赖树和实际类来源,按 Tomcat 类加载说明判断容器库与应用库的可见范围。
JNDI 数据源由容器创建连接池,再供应用通过 java:comp/env/jdbc/... 查找。此时需要把 JDBC 驱动放在创建数据源的容器类加载器可见位置,并在 Context 的 Resource 中配置匹配的工厂、连接参数和池上限。应用自建数据源时,则由应用依赖和配置管理驱动及连接池,不要同时维护两个作用不明的池。
数据库密码应由受保护的配置供应,不能写入公开 WAR 或提交到仓库。具体 Resource 属性随连接池实现变化,直接参考 JNDI 数据源示例,接入后验证借还连接、超时、断连恢复和关闭行为。
日志、堆与进程状态
容器自身日志、HTTP 访问日志和应用业务日志用途不同。示例把容器日志写到标准错误,由 Docker 收集;访问日志写到 BASE 的 logs。Tomcat 自身 JULI 与应用日志框架可以并存,配置位置见日志说明。
示例访问日志格式为 %a %m %U %s %b %{ms}T,依次记录地址、方法、路径、状态、响应字节和毫秒耗时,不记录 Cookie、Authorization 或查询参数。Tomcat 11 的 %D 使用微秒,跨版本复用旧日志模板时尤其要核对单位。
docker compose exec -T app sh -ec 'tail -n 20 /srv/tomcat/logs/access*.log'
docker stats --no-stream tomcat-lab-app-1
docker compose exec -T app jcmd 1 VM.flags
docker compose exec -T app jcmd 1 Thread.print示例以 Java 为容器 PID 1,其他启动包装方式应先核对 PID。线程栈可能包含业务参数,应保存在受控位置。高 CPU 时结合多个间隔采样判断热点;大量线程等待同一个连接池时继续检查下游,不能仅凭线程数量推断需要加线程。
需要进一步判断堆、GC、类加载与本地内存时,可接着使用 JVM 诊断中的工具与采样方法。
-Xmx256m 只约束 Java 堆。512 MiB 容器还要容纳元空间、线程栈、直接内存、JIT 与运行时开销。若要保存 heap dump,应预先准备只有运行账号可写的受控磁盘目录,评估空间和敏感数据;tmpfs 容量不足或容器直接被内核杀死时,转储未必生成。
Manager、Host Manager、示例应用、远程 JMX 和调试端口都不应默认开放给公网。仅在确有管理需求时配置独立认证、网络限制及日志,并定期跟踪所在分支的安全更新。Tomcat 安全说明提供了对应配置入口。
升级、回退与故障恢复
用实际版本字段确认切换
前面已经构建 r1 与 r2 两个镜像。两者只改变 WAR 内的版本资源,适合验证切换行为;正式发布还需完成应用测试和制品校验。
APP_RELEASE=r2 docker compose up -d --wait --pull never --no-build
curl -q --noproxy '*' --fail-with-body --max-time 5 \
http://127.0.0.1:18093/app/api/health新响应应显示 r2。单机 Compose 会替换原容器,期间可能断开旧连接或短暂不可达。面向持续服务的部署,需要至少两个实例:先启动并验证新实例,再在入口切换流量,等待旧请求结束后停止旧实例。健康检查只在它实际覆盖的应用范围内提供判断,不能替代核心业务路径测试。
恢复 r1:
APP_RELEASE=r1 docker compose up -d --wait --pull never --no-build
curl -q --noproxy '*' --fail-with-body --max-time 5 \
http://127.0.0.1:18093/app/api/health响应恢复 r1 后,再检查 JSP 和会话行为。若旧镜像已被清理,--pull never 会使部署失败,应从可信仓库或离线备份恢复精确镜像。不要重新构建一个内容可能不同的同名标签冒充原发布版本。
原生 systemd 路线应先摘流、停止服务,再把验证好的新 WAR 安装到新的版本目录,修改描述文件指向它,启动并验证。保留旧 WAR、旧描述文件和匹配配置;恢复时在停机状态下重新指向旧版本。禁用自动部署后,文件替换不会自行让正在运行的类更新。
数据库迁移、消息格式和外部写入需要单独评估兼容性。应用切回旧 WAR 后,旧代码可能已经无法读取新结构;发布前应尽量采用向前、向后兼容的过渡变更,无法回退时明确数据恢复步骤与可接受的数据损失。
停止时留出请求与资源清理时间
示例 Server 使用 port="-1" 禁用独立 shutdown 端口,由 Docker 或 systemd 向前台 Java 进程发送停止信号。这样不使用依赖 shutdown 端口的 shutdown.sh。该配置与脚本限制见 Server 文档。
Context 的 unloadDelay 是等待 Servlet 卸载的时间,不能承诺任意业务都在此时长内安全完成。应用应主动关闭线程池、调度任务与连接池;WebSocket、异步任务和长事务也需要各自的停止策略。入口摘流、连接等待、应用清理和进程管理器的最终超时应相互匹配。
Docker 示例留 30 秒停止窗口,超时后可能强制结束进程。生产参数应根据实际最长请求与清理耗时验证,而不是直接复制实验值。停止前后分别检查正在处理的请求、应用关闭日志和残留进程。
从故障现象缩小范围
| 现象 | 检查位置 | 下一步 |
|---|---|---|
根路径 404,/app/ 正常 | Context 路径与代理映射 | 使用实际应用路径,或明确部署 ROOT 应用 |
/app/ 也 404 | Context 启动日志、docBase、WAR 读权限 | 修复首次部署异常后重启并请求应用接口 |
| Nginx 502 | Tomcat 是否监听、目标地址、网络与进程退出 | 在代理所在网络请求后端,再查失败进程日志 |
| JSP 500 | work 写权限、JSP 编译异常、相关依赖 | 修正权限或源码后验证同一 JSP |
UnsupportedClassVersionError | 实际 Java 与编译目标 | 对齐运行时和应用编译版本 |
| Servlet 类找不到或方法不匹配 | javax/jakarta、容器 API、重复 JAR | 修正依赖后重新构建 WAR,不盲目清空目录 |
| 请求越来越慢 | 线程栈、连接池等待、GC、上游超时 | 先处理主要等待资源,再调整并发限制 |
| 容器退出且无 Java OOM | 容器状态、内核内存事件、资源上限 | 分开判断 Java 堆耗尽与容器内存终止 |
404 负例可以直接验证,不把它当作健康成功:
STATUS=$(curl -q --noproxy '*' --silent --show-error --max-time 5 \
--output /tmp/tomcat-missing.body --write-out '%{http_code}' \
http://127.0.0.1:18093/app/api/missing)
test "$STATUS" = 404
docker inspect tomcat-lab-app-1 --format '{{json .State}}'原生部署对应检查 systemctl status tomcat-lab、journalctl -u tomcat-lab -n 100 --no-pager 和 ss -lntp。不要把 systemd 状态与另一个 Compose 实例的日志混在一起判断。
需要清理 JSP 工作文件时,先保存错误日志并停止对应应用,确认具体 work 子目录,再按可恢复方式移走该目录。清空整个 Tomcat 目录、删除数据库数据或开放所有端口,都不能替代对首个异常的定位。
结束容器实验执行 docker compose down。实验容器及其 tmpfs 内容会消失,两个镜像仍保留;需要保留访问日志时应先导出到受控位置。
权威资料与规范地址
按具体配置、协议或操作查阅完整定义:
版本与部署
| 查阅内容 | 官方地址 |
|---|---|
| Tomcat 官方版本表 | https://tomcat.apache.org/whichversion.html |
| 部署说明 | https://tomcat.apache.org/tomcat-11.0-doc/deployer-howto.html |
| Tomcat RUNNING | https://tomcat.apache.org/tomcat-11.0-doc/RUNNING.txt |
| Tomcat 官方下载页 | https://tomcat.apache.org/download-11.cgi |
