Docker容器化Nginx反向代理Tomcat部署实战 最近在帮朋友的一个Java Web项目做容器化改造他那边的情况很典型后端是标准的Tomcat应用前端有些静态资源平时切换环境、发版总是手忙脚乱。聊到最后方案落到了一条非常基础但又极其关键的链路上——用Docker创建Nginx容器再把请求转给Tomcat容器。这个标题看起来简单实际做起来牵扯到镜像选择、容器网络、端口映射、反向代理配置、排错定位等一系列问题。把这套东西彻底吃透基本就能应对日常开发环境里绝大多数的容器部署需求。这篇文章不打算罗列“docker run一条命令搞定”之类的表面功夫而是把整个项目的来龙去脉、每一步背后的为什么、以及我在实操中踩过的坑都拆开讲。无论你是刚接触Docker、正在搭建本地开发环境还是准备把老应用逐步迁移到容器这篇内容应该都能帮你省下不少折腾时间。1. 先把链路想明白Nginx和Tomcat是怎么配合的1.1 一次请求的完整旅程正常情况下当一个Java Web应用部署在Tomcat上时用户直接访问Tomcat的8080端口就能拿到页面。那为什么还要在中间插一个Nginx先看一次完整请求经过的节点浏览器 → Nginx容器80端口 → Tomcat容器8080端口 → 应用处理 → 沿原路返回Nginx在这里扮演的是反向代理的角色。所谓“反向”是指代理服务器位于服务端对外暴露的是统一入口内部的具体服务节点对用户完全透明。用户请求进来后只跟Nginx打交道Nginx根据规则决定把请求转发给哪个后端服务。打个比方Nginx是小区大门外的物业前台Tomcat是某栋楼里的住户。访客不需要知道住户具体住哪一栋哪一户只要把需求告诉前台前台再转达给住户结果再交回访客手里。对外你只知道那个前台剩下的细节全是内部的事。1.2 这层代理解决了什么问题为什么要多这一个环节我在实际项目里最直接的感受有三点端口统一与安全。一个服务器上可能同时跑着Tomcat、Node服务、Python应用如果全部都直接对外暴露端口管理和防火墙策略会变得很痛苦。Nginx统一收口在80或443端口內部的8080、3000等端口只在容器网络内开放不直接暴露到公网安全面小了很多。静态资源与动态请求分离。Tomcat本身是一个应用服务器处理Java动态请求是它的强项但让它直接返回图片、CSS、JS这些静态文件资源消耗不小。Nginx处理静态文件的效率远高于Tomcat让Nginx直接返回静态资源、把动态请求转给Tomcat既减轻Tomcat压力也加快页面加载。负载均衡的入口。当Tomcat需要水平扩展成多实例时Nginx可以用upstream做负载均衡对外仍然是一个地址。这个能力在容器化、微服务化的场景里几乎是必备的。1.3 Docker在这条链路里的角色传统部署方式下Nginx和Tomcat装在同一台机器上用本机回环地址通信。改用Docker之后各自跑在独立的容器里表面上隔离了但这带来了两个核心变化第一个变化是环境一致性。Nginx镜像就是Nginx环境Tomcat镜像自带JDK和Tomcat不会出现“在我机器上好好的到服务器就404”这种尴尬。第二个变化是通信方式。容器之间不再是本地进程间通信而是通过网络协议通信所以要专门考虑Docker网络的问题。在做这个项目之前我也见过一些同事图省事直接把Nginx部署在宿主机上用proxy_pass http://localhost:8080把请求转发给Tomcat容器映射出来的端口。这样确实能通但违背了容器化的初衷——Nginx那个宿主机环境仍然是个黑盒没法随项目一起分发。更合理的做法是Nginx也容器化让它跟Tomcat在同一个自定义网络里通过容器名通信这样整套依赖关系可以一键复现。这也是下面所有操作的核心出发点。1.4 为什么不用docker run --link如果你是老Docker用户可能听过--link参数它也能让两个容器互相用名字访问。但--link已经被官方标记为遗留功能它建立的是有方向的单向依赖而且容器重启后连接关系可能失效。相比之下自定义bridge网络是更现代也更好用的方案网络内所有容器自动支持DNS解析双向通信配合docker-compose还能实现声明式管理。所以本文根本不提--link直接用自定义网络。2. 动手之前的准备镜像、网络、配置三件套这部分容易被跳过但恰恰是大多数翻车事故的根源。我在排障现场见过太多次“命令没问题、环境有问题”的情况。2.1 镜像选择与版本坑镜像版本不是随便拉一个latest就完事不同版本差异直接决定了你后面会不会踩坑。Nginx镜像我推荐nginx:stable-alpine。stable系列比mainline系列更稳适合生产alpine版本体积小基础系统精简方便打命令进容器排查。相比之下nginx:latest属于mainline版本功能新但也意味着相对激进。如果你不想在Nginx配置语法上猜来猜去stable-alpine是更保险的选择。Tomcat镜像推荐tomcat:9.0-jdk8这类带JDK版本的标签。这里有一个容易忽略的细节Java Web应用对Servlet版本、JDK版本都有硬性要求直接用latest很可能变成Tomcat 10那里面包名已经从javax换成了jakarta老项目直接跑不了。Tomcat 9兼容javax.*命名空间这是当前绝大多数存量Java Web项目还在用的标准。选镜像时看清楚JDK版本要跟项目实际编译版本匹配。下面是两个镜像的简单对比供参考镜像标签特点适合场景nginx:stable-alpine体积小、稳定、有apk包管理器生产/开发通用推荐nginx:latest功能新、属于mainline分支想尝鲜、需要新特性的场景tomcat:9.0-jdk8经典组合兼容javax大多数存量Java Web项目tomcat:10.1-jdk17新版本使用jakarta命名空间新项目或已适配Spring Boot 32.2 网络先行先建自定义网络既然确认要让Nginx容器和Tomcat容器通过容器名互相访问网络就应该在容器启动之前建好。这一步成本极低但收益很大。docker network create app-net执行后用docker network ls能看到一个名叫app-net的bridge网络。为什么要在启动容器之前建因为docker run创建容器时可以直接通过--network指定网络如果网络不存在会直接报错反过来还要先创建一个再重新建容器浪费时间。自定义网络和Docker默认的bridge网络相比核心优势在于内置DNS解析。同一个自定义网络里的容器可以直接用容器名作为主机名访问对方。比如Tomcat容器叫tomcat-app那Nginx容器配置里写proxy_pass http://tomcat-app:8080Docker会自动解析成Tomcat容器的IP。容器重启导致IP变了也没关系Docker的DNS会跟着更新。如果不建自定义网络你就得去查容器IP来写配置一重启全废。2.3 配置文件在宿主机先规划好容器是短暂的随时可以删掉重建但配置文件如果写死在容器里一删就全没了。比较好的做法是在宿主机固定一个目录作为所有配置文件的存放点启动容器时挂载进去。我习惯在/opt/docker/nginx下放Nginx配置在/opt/docker/tomcat下放Tomcat相关的挂载内容。你完全可以用自己的目录但建立清晰的目录规划很重要。尤其是Nginx的conf.d目录它里面每个.conf文件都会被Nginx主配置include进来最适合放我们自己的server块。启动容器时直接把宿主机的配置文件单独挂载成对应的default.conf文件比挂载整个配置目录更清晰也避免误覆盖Nginx主nginx.conf。3. 第一步把Tomcat容器跑起来3.1 启动Tomcat容器命令全解Tomcat容器是整个链路的后端先把它创建出来。命令如下docker run -d \ --name tomcat-app \ --network app-net \ -p 8080:8080 \ tomcat:9.0-jdk8每个参数逐个说明-d后台运行容器终端不会被日志刷屏。--name tomcat-app给容器固定名字这个名字就是后面Nginx转发时用的主机名尽量短而有辨识度。--network app-net指定容器加入之前建好的app-net网络。-p 8080:8080把容器的8080端口映射到宿主机8080端口。格式是“宿主端口:容器端口”左边是可以随便改的右边是Tomcat默认监听端口。注意这个-p端口映射只是为了开发期方便直接访问Tomcat验证。如果整套链路都由Nginx对外收口Tomcat的8080端口理论上可以不映射到宿主机只在容器网络内部使用。但开发阶段还是建议映射出来排查问题方便很多。3.2 启动后必须确认应用可用容器启动后不能急着配Nginx先确认Tomcat自己是否正常工作。docker ps docker logs tomcat-appdocker ps看容器状态如果STATUS列不是Up而是Exited说明启动失败用docker logs tomcat-app看报错日志多半是端口占用、镜像拉取不完整或者JDK版本不兼容。看到Server startup in类似信息说明Tomcat已经起来了。这时可以先验证一下访问效果curl http://localhost:8080这里我遇到过一个非常典型的问题Tomcat 9的新镜像默认webapps目录是空的启动虽然成功但访问8080会直接404。因为新版镜像把默认的示例应用挪到了一个叫webapps.dist的目录里。如果你也遇到这个情况解决办法很简单docker exec tomcat-app bash -c cp -r webapps.dist/* webapps/这样就把Tomcat自带的ROOT示例应用复制到webapps目录。当然如果你有实际的应用war包这一步可以跳过直接把war包丢进webapps目录即可。这里想多说一句很多人习惯用docker exec -it进入容器改东西但容器一删除改的东西全部丢失。开发阶段临时调试可以正式环境应该反过来——把war包或应用目录通过-v挂载进容器让容器的数据状态可预期。3.3 怎样确认请求真的打到了Tomcat光靠浏览器开页面还不够排查问题时更重要的是确认请求确实到达了Tomcat。Tomcat的访问日志默认在logs目录下可以这样查看# 查看Tomcat实时日志 docker logs -f tomcat-app如果你访问了8080页面docker logs里应该能看到对应的HTTP访问记录。这一招在后面验证Nginx转发是否生效时非常关键假如Nginx配置错了请求根本没转发到TomcatTomcat日志里就不会有记录问题就能立刻定位到Nginx那一层而不是Tomcat应用本身。4. 第二步用Nginx容器做反向代理4.1 一份能直接上线的Nginx配置文件Nginx的核心在于配置文件。这里给出一份针对这个项目场景的完整配置放在宿主机/opt/docker/nginx/default.confupstream backend_tomcat { server tomcat-app:8080; } server { listen 80; server_name localhost; location / { proxy_pass http://backend_tomcat; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }逐块拆解upstream backend_tomcat定义了一个后端服务器组里面用server tomcat-app:8080指明后端Tomcat地址。这里的tomcat-app就是容器名会被Docker DNS自动解析。用upstream而不是直接在location里写proxy_pass http://tomcat-app:8080的好处是以后Tomcat扩容时只需要在这个块里加一行server tomcat-app2:8080 weight3;就能实现简单的轮询负载均衡。location /表示对所有请求进行转发。proxy_pass http://backend_tomcat就是“把请求转给后端Tomcat组”。注意proxy_pass结尾不要带路径避免URL拼接出现意外。下面四个proxy_set_header非常重要它们把客户端原始信息传给后端TomcatHost $host保留客户端请求时的Host头否则Tomcat拿到的Host是Nginx容器自身的应用里如果做域名判断就会出问题。X-Real-IP $remote_addr把真实客户端IP传给后端因为从Tomcat视角看请求来源是Nginx容器不是真正的用户。X-Forwarded-For追加客户端IP链路支持多层代理场景。X-Forwarded-Proto $scheme告诉后端请求是HTTP还是HTTPS防止应用里生成链接时协议判断错误。这些请求头配置是反向代理场景里的“标配”甚至可以原样抄到其他Nginx后端的组合里比如Nginx转发到Node服务、Python服务都一样适用。4.2 启动Nginx容器与挂载细节配置文件准备好后创建Nginx容器docker run -d \ --name nginx-gateway \ --network app-net \ -p 80:80 \ -v /opt/docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro \ nginx:stable-alpine命令里几个关键点-p 80:80把Nginx容器的80端口映射到宿主机80这是对外统一入口。如果宿主机80被占用了可以临时改成别的端口比如-p 8081:80但生产环境还是优先保证80端口归Nginx。-v挂载配置时我用的是单文件挂载并且加了:ro只读后缀。这样宿主机上改配置容器内立即生效同时容器内无法修改该文件避免误操作。用单文件挂载而不是挂载整个目录是因为如果整个目录挂进去会覆盖镜像里/etc/nginx/conf.d原有的默认文件结构有时候反而会引入干扰。启动后用docker ps确认容器状态。这里有个隐蔽的错误值得单独提如果挂载的宿主机配置文件本身有语法错误Nginx容器可能反复重启或者启动失败。可以用docker logs nginx-gateway看日志一般会直接提示哪一行出错。4.3 动态请求和静态资源分开处理前面的配置把所有请求都转给Tomcat对一个纯粹的Java Web应用来说够用了。但如果你的项目里前后端分离或者想利用Nginx处理静态资源的优势就需要再加一段静态资源处理规则实现动静分离。修改配置如下upstream backend_tomcat { server tomcat-app:8080; } server { listen 80; server_name localhost; location / { proxy_pass http://backend_tomcat; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location ~* \.(css|js|png|jpg|jpeg|gif|svg|ico|woff2?|ttf|eot)$ { root /usr/share/nginx/html; expires 7d; add_header Cache-Control public; } }第二段location用正则匹配常见的静态文件后缀命中后直接从Nginx容器的/usr/share/nginx/html目录找文件返回不再转给Tomcat并设置expires 7d让浏览器缓存一周。如果这些静态文件是发布时同步到容器目录里的这里会是最高效的访问路径。要注意的是location /和location ~*的匹配优先级Nginx先尝试普通前缀匹配再按正则顺序匹配正则命中后优先使用正则结果。所以这段配置里静态文件的请求会走第二个规则其他请求走第一个规则转给Tomcat。这个顺序在生产环境里也是同样逻辑你可以放心使用。5. 第三步完整链路验证5.1 从宿主机验证外部访问两个容器都起来以后验证整条链路是否按预期工作curl -I http://localhost期望看到的状态码是200返回头里应该包含Nginx的标识。接着再看一眼Tomcat日志docker logs --tail 50 tomcat-app如果Nginx成功把请求转给了TomcatTomcat日志会立刻出现对应访问记录。这时候整条链路已经打通外部请求走宿主80端口进入Nginx容器再被反代到Tomcat容器处理。5.2 从容器内部验证网络连通性外部访问正常不代表容器间网络就没有问题。要确认Nginx容器确实能访问到Tomcat容器可以这样测docker exec nginx-gateway curl http://tomcat-app:8080这条命令的妙处在于curl是在Nginx容器里执行的它访问的tomcat-app:8080走的是Docker内部网络完全绕开了宿主机端口映射。如果能打印出Tomcat首页的HTML说明容器间网络、DNS解析都没问题。这一招在排查“为什么外面访问502”的时候尤其好用——如果从Nginx容器内能访问Tomcat问题多半出在Nginx配置如果容器内就访问不通问题出在网络或Tomcat容器本身。5.3 改了Nginx配置以后怎么做开发过程中改配置是家常便饭。改完宿主机上的default.conf后Nginx并不会自动加载新配置需要重载。命令是docker exec nginx-gateway nginx -s reloadreload是平滑重载不会中断现有连接业务影响几乎为零。这里有个经验值得记住每次改配置前先验证语法再重载避免错误配置直接生效导致全线502docker exec nginx-gateway nginx -t看到test is successful再执行reload。不要省这一步我见过改配置少了个分号reload瞬间把线上所有请求打到错误后端的真实案例。6. 实战避坑目录常见问题与排查技巧这一部分我把这几次实操下来遇到过的典型问题整理成了一份速查表都是真实发生过的场景按优先级排序遇到问题对照着排查效率会高很多。问题现象可能原因排查命令/方法访问80端口返回502 Bad GatewayNginx容器连不上Tomcat容器docker exec nginx-gateway curl http://tomcat-app:8080确认两个容器都在app-net网络502且容器内能访问TomcatNginx配置里upstream名字写错、proxy_pass路径不对docker exec nginx-gateway nginx -t检查配置语法页面404Tomcat的webapps下没有ROOT应用docker exec tomcat-app ls webapps确认目录内容访问8080但页面无法打开端口映射缺失、防火墙拦截docker ps查看端口映射0.0.0.0:8080-8080/tcp容器启动后反复重启挂载的Nginx配置语法错误docker logs nginx-gateway直接看报错行号Nginx日志里出现大量connect refusedTomcat容器没启动或网络隔离docker ps -a看Tomcat状态docker logs tomcat-app宿主机端口被占其他进程占用80或8080端口ss -lntp或lsof -i:80查看占用进程6.1 502 Bad Gateway的两种常见原因502是配置Nginx反代后最容易碰到的报错。它代表Nginx把请求转发出去后没能从后端得到有效响应。我遇到过的情况主要分两种第一种容器间网络不通。通常是把Tomcat和Nginx建在了不同网络里或者Nginx容器启动早于网络创建。排查方法确认两个容器的网络是否一致docker inspect tomcat-app -f {{.NetworkSettings.Networks}} docker inspect nginx-gateway -f {{.NetworkSettings.Networks}}两边都要有app-net。如果Nginx容器用了默认bridge而Tomcat在app-net肯定不通。第二种同网络但Nginx配置里容器名写错。注意upstream里的名字必须和--name完全一致多一个字符、少一个字符都解析不到。遇到这种情况先用6.2里提到的容器内curl验证就能快速锁定问题层。6.2 容器间通但Nginx转发还是502这种情况更隐蔽。从Nginx容器内执行curl http://tomcat-app:8080明明能返回Tomcat首页外部访问却502。问题基本出在proxy_pass路径上。比如upstream定义的是backend_tomcat但proxy_pass写成http://backend_tomcat/末尾多了一个斜杠。这个斜杠的存在会改变URL的拼接规则如果后端路径不匹配就会出现404或者500。另一种情况是proxy_pass http://tomcat-app:8080直接写了容器名而不是upstream组名两者都能通但语义不同——如果你后续想加第二个Tomcat做负载均衡就得统一用upstream。6.3 端口冲突的经典场面很多时候Tomcat容器起不来不是因为镜像问题而是宿主8080端口被占。解决办法有两种要么杀掉占用进程要么改映射端口。开发环境我通常直接用后者docker run -d \ --name tomcat-app \ --network app-net \ -p 18080:8080 \ tomcat:9.0-jdk8把宿主端口改成18080容器内8080不变。只要Nginx容器里用的是tomcat-app:8080映射端口改成多少都不影响容器间通信外部访问时用对应端口就行。6.4 前后端分离场景下转发的特殊处理如果你的项目是前后端分离前端页面由Nginx直接托管后端API才转给Tomcat配置会再调整一层。比如后端接口统一有/api前缀location /api/ { proxy_pass http://backend_tomcat; proxy_set_header Host $host; }此时前端请求/api/user/listNginx会把它转发给Tomcat的/api/user/list再由应用层路由处理。在Spring MVC这类框架里Controller上如果加过RequestMapping(/api)就能直接对上。如果后端没有统一前缀而是直接定义在Controller上就更简单所有动态请求都往Tomcat转就行。6.5 日志怎么看、怎么用好排查问题时日志比什么都重要。Nginx容器日志、Tomcat容器日志分别在两个容器的标准输出里docker logs -f nginx-gateway docker logs -f tomcat-appdocker logs -f是实时跟踪模式请求进来后几秒内就能看到对应访问日志。如果开了Nginx转发的调试需求还可以临时在Nginx配置的server块里加一行error_log /var/log/nginx/error.log debug;不过生产环境不要长期开debug日志量会暴涨磁盘很快被填满。临时排查后建议改回默认的error级别。7. 实操中的一点体会和后续扩展整套流程做完以后我最大的感受是Docker部署Nginx转Tomcat这件事难点从来不在单条命令而在把“网络”、“配置”、“日志”这三条线索串起来。网络解决容器之间能不能找到对方配置解决找到之后怎么正确转发日志则在出问题时告诉你请求到底停在哪一段。三样都理清了无论后面把Tomcat扩容成几个实例还是换成其他后端服务思路都是一样的。如果想在这个基础架构上进一步演进我通常会建议走两步第一步把Nginx的upstream从单节点扩展成多节点实现Tomcat集群负载均衡第二步把整套容器编排逻辑用compose文件固化下来让网络、容器、挂载都在同一个声明文件里换环境时一条命令全部拉起。这两步都属于水到渠成的事但前提都是你现在把底层链路真正跑通了。