资讯动态

Ubuntu 18.04 单镜像构建 JDK+Tomcat+Redis 实战指南

发布时间:2026/10/26 4:29:48 来源:尧图企业网站定制
1. 为什么要在 Ubuntu 18.04 上自己构建 JDKTomcatRedis 镜像把 JDK、Tomcat、Redis 三件套塞进同一个镜像听起来像是“把厨房、客厅、卧室打通成一个开间”——能用但得想清楚为什么这么干。我在实际项目里遇到这个需求的场景很典型一套遗留的 Java Web 应用部署环境要求 Tomcat 跑业务代码、Redis 做会话共享和缓存而运维侧只允许交付一个自包含的镜像包避免在目标机器上分别装三个组件、配三份环境变量。这种情况下单镜像多进程方案虽然不符合“一个容器一个进程”的教条但在边缘部署、离线交付、教学演示这类场景里它确实省事。选择 Ubuntu 18.04 作为基础系统理由也很实在这个版本的 glibc 版本、apt 源生态对老版本 JDK 和 Tomcat 的兼容性经过大量验证很多企业内网的老应用就是在这个底座上跑起来的。你当然可以用更轻的 Alpine但 Alpine 的 musl libc 对某些 JNI 依赖、字体库、时区数据的支持会让你在排查“为什么 Tomcat 启动报 native 库找不到”上耗掉半天。Ubuntu 18.04 的镜像体积大一些基础镜像约 60MB 出头换来的是“少踩坑”这笔账在交付周期紧张时是划算的。这篇内容适合谁看如果你是会写 Dockerfile 但没试过在单镜像里编排多个服务的中级使用者或者你需要给团队做一套可复现的构建脚本再或者你正在学习镜像分层原理想找个综合案例那接下来的拆解会对你有用。我会把每一步的“为什么这么写”讲透包括那些文档里不会写的坑比如 Redis 前台运行与 Tomcat 启动顺序的冲突、时区导致日志时间错乱、apt 缓存清理对镜像体积的实际影响。需要先明确一个前提单镜像跑多进程必须有一个“进程管理者”负责拉起并守护这些服务。常见做法是用 supervisor或者写一个 entrypoint 脚本用后台拉起再wait。我倾向于 entrypoint 脚本方案因为它不引入额外依赖逻辑透明出问题容易排查。下面所有内容都围绕这个思路展开。2. 整体设计与分层思路拆解2.1 镜像分层的取舍逻辑Dockerfile 的每一条指令都会生成一层层数越多镜像越大但层数太少又会导致缓存失效频繁、构建变慢。我的分层原则是变化频率低的放前面变化频率高的放后面。基础系统、apt 依赖、JDK 安装这三块几乎不变放最前面Tomcat 版本相对稳定放中间Redis 配置和应用 war 包变化最频繁放最后。这样日常只改应用包时前面所有层都能命中缓存构建时间从几分钟降到十几秒。具体到指令层面RUN命令要尽量合并。比如 apt 安装依赖时apt-get update、apt-get install、清理缓存这三步必须写在同一个RUN里用连接否则清理动作发生在独立层里上一层下载的包缓存依然留在镜像中体积根本降不下来。这是新手最容易犯的错我见过一个镜像因为把 update 和 install 分开写白白多出 40MB 的 apt 列表缓存。2.2 单镜像多进程的进程管理方案单镜像里跑 Tomcat 和 Redis核心矛盾是“容器的主进程必须前台运行且不能退出”。如果你在 Dockerfile 里写CMD service redis start catalina.sh runRedis 是以守护进程方式启动的启动完就 fork 到后台主进程只剩 Tomcat一旦 Tomcat 挂掉容器就退出Redis 也跟着没了而且你收不到 Redis 自身的异常信号。我的方案是写一个entrypoint.sh里面用后台方式拉起 Redis再前台启动 Tomcat并用wait或tail -f保持主进程存活。更稳妥的做法是用trap捕获 SIGTERM在容器停止时先优雅关闭 Tomcat 再关 Redis避免数据丢失。这个脚本虽然只有几十行但它是整个镜像“能不能稳定跑”的关键后面第 4 节会给出完整代码和逐行解释。2.3 版本选型的兼容性考量JDK 选 8 还是 11如果应用是 Spring Boot 2.x 以下或者老 SSH 框架JDK 8 是安全牌Ubuntu 18.04 的 apt 源里默认能装 openjdk-8-jdk省去手动下载 tar 包的步骤。Tomcat 选 8.5 还是 9Tomcat 9 要求 Servlet 4.0对老应用兼容性略差8.5 是更稳的选择。Redis 用 apt 装还是源码编译apt 装的版本Ubuntu 18.04 源里是 4.0.x足够用省去编译依赖但如果你需要 Redis 5 以上的 Stream 特性就得源码编译那会多出 build-essential 依赖镜像体积增加约 200MB。我这次选 apt 方案理由是“够用且可维护”。组件版本选择选型理由体积影响基础镜像ubuntu:18.04glibc 兼容性好apt 生态成熟约 64MBJDKopenjdk-8-jdkapt 直装老应用兼容约 280MBTomcat8.5.xServlet 3.1 兼容广约 15MBRedisapt 源 4.0.x免编译满足缓存需求约 5MB3. 核心细节解析与实操要点3.1 基础镜像与 apt 源优化拿到ubuntu:18.04后第一件事不是急着装软件而是处理 apt 源。默认源在国外构建时下载速度慢且容易超时。我的做法是在RUN里先备份原 sources.list再替换为国内镜像源。这里要注意替换源之后必须紧跟apt-get update否则后续 install 用的还是旧索引。另外Ubuntu 18.04 已经进入维护期部分第三方源可能已下线替换前最好确认目标源仍提供 bionic 仓库否则会出现“404 Not Found”导致构建中断。一个实操细节apt-get install时加上-y --no-install-recommends两个参数。-y免交互--no-install-recommends只装必需依赖不装推荐包。实测下来光这一个参数就能省下几十 MB因为很多推荐包比如文档、示例配置在容器里根本用不上。装完之后立刻rm -rf /var/lib/apt/lists/*清空索引缓存这一步必须和 install 在同一个 RUN 层里。3.2 JDK 安装与环境变量配置用 apt 装 openjdk-8-jdk 后JAVA_HOME并不会自动设置需要手动指定。Ubuntu 下 JDK 8 的路径通常是/usr/lib/jvm/java-8-openjdk-amd64但为了稳妥我建议用update-alternatives --list java或者直接readlink -f $(which java)反推路径避免硬编码路径在别的镜像版本上失效。环境变量用ENV指令设置注意JAVA_HOME和PATH要一起配PATH里追加$JAVA_HOME/bin否则java -version能跑但javac找不到。注意ENV设置的环境变量在容器运行时依然生效如果你希望构建期临时用某个变量用ARG更合适。JAVA_HOME属于运行期也要用的所以用ENV没问题。验证环节不能省。装完 JDK 后在 Dockerfile 里加一句RUN java -version javac -version构建时如果版本不对会直接失败比等到运行容器才发现问题强得多。我踩过的坑是某次 apt 源里 openjdk-8-jdk 被解析成了 headless 版本没有 javac导致后续 Tomcat 编译 JSP 时报错加了这句验证就能提前暴露。3.3 Tomcat 的下载、解压与目录规划Tomcat 不走 aptapt 里的 tomcat8 包结构比较特殊配置分散直接下载官方 tar.gz 更可控。下载地址用archive.apache.org的固定版本链接不要用dlcdn的最新链接因为最新链接会随版本更新失效构建不可复现。下载后用tar -xzf解压到/opt/tomcat然后做三件事删掉 webapps 下的默认示例应用docs、examples、manager、host-manager只留 ROOT 或者干脆清空给 bin 目录下的*.sh加执行权限创建tomcat用户并把目录属主改过去。为什么要删默认应用manager 和 host-manager 带管理后台暴露在公网有安全风险而且占体积。为什么用非 root 用户跑 Tomcat容器里用 root 跑应用是安全大忌一旦应用被攻破攻击者直接拿到容器 root 权限。创建专用用户虽然多几行但这是基本的安全底线。注意useradd时加-r -s /bin/false创建系统用户且不给登录 shell。3.4 Redis 安装与关键配置调整apt 装 redis-server 后默认配置是daemonize yes后台运行且绑定127.0.0.1。这两个默认值在容器场景下都要改daemonize改成no让 Redis 前台运行这样 entrypoint 脚本里用拉起后能正确捕获进程绑定地址改成0.0.0.0或者注释掉 bind 行否则容器外访问不到。另外建议设置protected-mode no内网环境或者配密码二选一别两个都不做。配置文件修改用sed在 Dockerfile 里完成比 COPY 一个完整配置文件更轻量但可读性差一些。我的折中方案是只改关键几项用sed其余保持默认。改完用redis-server --version验证安装用redis-cli ping在构建期验证配置需要临时前台启动再关掉或者干脆放到运行期验证。这里有个细节sed替换daemonize yes时如果配置文件里是daemonize yes带注释的情况正则要写准建议用sed -i s/^daemonize yes/daemonize no/锚定行首避免误伤注释行。4. 实操过程与核心环节实现4.1 完整 Dockerfile 逐段拆解下面是我实际用的 Dockerfile 结构分段贴出来并解释每一段的设计意图。FROM ubuntu:18.04 LABEL maintainerdev-team \ descriptionJDK8 Tomcat8.5 Redis single image ENV DEBIAN_FRONTENDnoninteractive \ TZAsia/Shanghai \ JAVA_HOME/usr/lib/jvm/java-8-openjdk-amd64 \ CATALINA_HOME/opt/tomcat \ PATH$JAVA_HOME/bin:$CATALINA_HOME/bin:$PATHDEBIAN_FRONTENDnoninteractive是为了避免 apt 安装时弹出时区选择等交互界面卡住构建。TZ设置时区这个很关键——不设的话容器默认 UTCTomcat 日志和 Redis 日志时间会比北京时间差 8 小时排查问题时对时间线极其痛苦。JAVA_HOME和CATALINA_HOME一起配进PATH后面所有命令都能直接用。RUN sed -i s/archive.ubuntu.com/mirrors.aliyun.com/g /etc/apt/sources.list \ apt-get update \ apt-get install -y --no-install-recommends \ openjdk-8-jdk \ redis-server \ curl \ ca-certificates \ tzdata \ ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \ echo $TZ /etc/timezone \ rm -rf /var/lib/apt/lists/*这一段把源替换、更新、安装、时区链接、清缓存全部串在一个 RUN 里。tzdata必须显式装因为--no-install-recommends不会自动带它没有它ln -snf时区链接会指向不存在的文件。curl用于后面下载 Tomcatca-certificates保证 https 下载不报证书错误。RUN curl -fsSL https://archive.apache.org/dist/tomcat/tomcat-8/v8.5.85/bin/apache-tomcat-8.5.85.tar.gz \ -o /tmp/tomcat.tar.gz \ mkdir -p /opt/tomcat \ tar -xzf /tmp/tomcat.tar.gz -C /opt/tomcat --strip-components1 \ rm -rf /opt/tomcat/webapps/* \ chmod x /opt/tomcat/bin/*.sh \ rm -f /tmp/tomcat.tar.gz--strip-components1去掉 tar 包里的顶层目录让文件直接落在/opt/tomcat下省去重命名步骤。rm -rf webapps/*清空默认应用如果你有 war 包要部署后面 COPY 进去即可。chmod x给脚本加执行权限tar 解压后权限有时会丢。RUN sed -i s/^daemonize yes/daemonize no/ /etc/redis/redis.conf \ sed -i s/^bind 127.0.0.1/bind 0.0.0.0/ /etc/redis/redis.conf \ sed -i s/^protected-mode yes/protected-mode no/ /etc/redis/redis.conf RUN useradd -r -s /bin/false tomcat \ chown -R tomcat:tomcat /opt/tomcat COPY entrypoint.sh /entrypoint.sh RUN chmod x /entrypoint.sh EXPOSE 8080 6379 ENTRYPOINT [/entrypoint.sh]Redis 三处 sed 修改对应前面说的三个关键配置。创建 tomcat 用户并改属主。entrypoint 脚本单独 COPY 进来并加执行权限。EXPOSE声明端口只是文档性质实际映射靠-p。ENTRYPOINT用 exec 形式JSON 数组保证脚本能收到 SIGTERM 信号。4.2 entrypoint 脚本的进程编排实现#!/bin/bash set -e echo [entrypoint] starting redis... redis-server /etc/redis/redis.conf REDIS_PID$! for i in $(seq 1 30); do if redis-cli ping 2/dev/null | grep -q PONG; then echo [entrypoint] redis is ready break fi sleep 1 done echo [entrypoint] starting tomcat... su -s /bin/bash tomcat -c ${CATALINA_HOME}/bin/catalina.sh run TOMCAT_PID$! cleanup() { echo [entrypoint] shutting down... kill -TERM $TOMCAT_PID 2/dev/null || true wait $TOMCAT_PID 2/dev/null || true kill -TERM $REDIS_PID 2/dev/null || true wait $REDIS_PID 2/dev/null || true exit 0 } trap cleanup SIGTERM SIGINT wait $TOMCAT_PID逐段说set -e让脚本遇错即停。Redis 后台拉起后记录 PID然后用循环最多等 30 秒探测redis-cli ping确保 Redis 就绪再启 Tomcat——这个顺序很重要如果应用启动时连不上 Redis 可能直接抛异常退出。Tomcat 用su切到 tomcat 用户前台运行catalina.sh run是前台模式日志直接输出到容器 stdout方便docker logs查看。trap捕获停止信号先关 Tomcat 再关 Rediswait确保进程真正退出。最后wait $TOMCAT_PID让脚本主进程挂住Tomcat 退出时脚本跟着退出容器生命周期与 Tomcat 绑定。注意su -c里的命令用双引号包裹变量如果CATALINA_HOME路径含空格会出问题好在我们的路径是/opt/tomcat不含空格。另外su在某些精简镜像里可能不存在Ubuntu 18.04 自带没问题。4.3 构建、运行与验证的完整命令构建命令加-t打标签建议带版本号方便回滚docker build -t app-stack:jdk8-tomcat8.5-redis4-v1 .运行的时候注意端口映射和挂载。应用 war 包建议通过 volume 挂载到/opt/tomcat/webapps这样换应用不用重新构建镜像docker run -d --name app-stack \ -p 8080:8080 -p 6379:6379 \ -v /data/webapps:/opt/tomcat/webapps \ -v /data/redis:/var/lib/redis \ app-stack:jdk8-tomcat8.5-redis4-v1验证分三步docker logs app-stack看 entrypoint 输出是否依次出现 redis ready 和 tomcat startedcurl http://localhost:8080/看 Tomcat 是否响应redis-cli -h localhost -p 6379 ping看 Redis 是否返回 PONG。如果 Tomcat 页面 404检查 webapps 挂载目录里有没有 war 包或者 ROOT 目录。5. 常见问题与排查技巧实录5.1 构建期高频问题速查现象可能原因解决思路apt update 报 404源地址已下线或拼写错误换用仍提供 bionic 的镜像源确认路径下载 Tomcat 超时archive 站点慢或网络受限换用国内镜像站或提前下载好 COPY 进去镜像体积异常大apt 缓存未清理或分层不当检查 RUN 是否合并确认 rm 在同一层javac 找不到装成了 headless 版 JDK确认包名是 openjdk-8-jdk 而非 -headless时区仍是 UTCtzdata 未装或链接未生效显式装 tzdata 并重建 localtime 链接5.2 运行期典型故障排查Tomcat 启动后立即退出看docker logs最常见是JAVA_HOME没配好或者 catalina.sh 权限不足。还有一种隐蔽情况是 webapps 挂载目录属主是 roottomcat 用户没权限读 war 包日志里会报Permission denied。解决办法是挂载前chown -R 1000:1000或者运行时用--user指定。Redis 连不上先docker exec -it app-stack redis-cli ping在容器内测通了说明 Redis 本身没问题问题在端口映射或 bind 配置。如果容器内都不通检查daemonize是否真的改成了 no以及 entrypoint 里 Redis 是否启动成功——有时候 redis-server 因为配置文件语法错误直接退出但脚本没检查返回值Tomcat 照常启动造成“Tomcat 能访问但 Redis 挂了”的假象。我的改进是在 entrypoint 里对 Redis 启动后做 ping 探测探测失败直接exit 1让容器启动失败而不是带病运行。日志时间差 8 小时除了镜像里设 TZ还要注意 JVM 的时区。JDK 8 会读系统时区一般没问题但如果应用里硬编码了TimeZone.getTimeZone(UTC)那就得改代码。Redis 日志时间跟随系统时区设了 TZ 就正常。容器停止慢或数据丢失如果没配 trapdocker stop发 SIGTERM 后脚本直接退出Redis 来不及把内存数据落盘虽然 Redis 有 RDB 定期保存但最后一次可能丢。配了 trap 后先关 Tomcat 再关 RedisRedis 收到 SIGTERM 会执行保存再退出数据安全性好很多。实测docker stop耗时从瞬间变成 2-3 秒这个等待是值得的。5.3 几个踩过的坑和独家心得第一个坑--no-install-recommends装 Redis 时没带redis-tools导致容器里没有redis-clientrypoint 里的 ping 探测直接报 command not found。解决方法是显式把redis-tools加进安装列表或者探测改用nc -z localhost 6379。我选前者因为 redis-cli 在排查时也用得上。第二个坑Tomcat 的catalina.sh run在 su 切换用户后环境变量丢失。su -c默认不继承当前环境JAVA_HOME可能读不到。稳妥做法是在su命令里显式带上环境变量或者用su -s /bin/bash tomcat -c export JAVA_HOME...; catalina.sh run。我实测发现 Ubuntu 的 su 会保留部分环境但为了跨环境稳定显式 export 更保险。第三个心得镜像构建完用docker history看每层体积能快速定位哪一层臃肿。我最初版本 JDK 那层 400MB查下来是 apt 缓存没清干净合并 RUN 后降到 280MB。另外dive这类工具能逐层看文件变化排查大文件很直观不过它不是必需docker history够用。第四个心得单镜像多进程不适合横向扩展。如果你的应用需要多实例部署Tomcat 和 Redis 应该拆成两个镜像用编排工具管理。单镜像方案的价值在“单机自包含”一旦涉及集群、滚动更新它的劣势就暴露了——你没法单独扩 Tomcat 而不动 Redis。所以这个镜像我一般只用在演示环境、边缘节点、离线交付场景生产集群还是拆分部署。最后分享一个调试技巧构建时如果某一步失败用docker build --progressplain能看到完整输出比默认的折叠输出好排查。运行期如果 entrypoint 脚本有问题临时把ENTRYPOINT改成CMD [/bin/bash]进容器手动跑脚本逐段验证比反复重建镜像快得多。这套流程我前后迭代了五六版才稳定现在构建一次约 2 分钟有缓存时 20 秒运行起来 Tomcat 和 Redis 各司其职日志统一从docker logs出日常维护成本很低。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑