资讯动态

离线环境部署Java应用:Docker、Nginx与OpenJDK8镜像打包交付指南

发布时间:2026/10/14 9:10:32 来源:尧图企业网站定制
简介在无外网、yum源不可用的服务器环境下离线交付是保证Java应用与反向代理正常运行的关键手段。容器化技术通过将Docker引擎、Nginx服务及OpenJDK8基础镜像封装为可搬运的文件包让实施人员能在目标机器上快速完成环境还原。其核心原理是利用docker save/load实现镜像的完整迁移以rpm包离线安装Docker组件并通过启动脚本串联容器生命周期。这种方式不仅屏蔽了网络依赖还通过配置挂载和资源限制提升了运维可控性。常见于企业内部系统交付、政企项目部署及黑匣子服务器场景。本文从基础概念出发结合工程实践梳理离线部署中镜像导入、容器启动、时区与日志配置的完整路径帮助运维与交付工程师规避典型坑点最终自然落到以Docker、Nginx、OpenJDK8镜像为核心的离线交付解决方案上。1. 一套离线交付物Docker、Nginx与OpenJDK 8镜像为什么总被打包在一起服务器的交付场景往往比开发环境粗暴得多目标机器没有外网yum源指向内网却一片空白SSH 上去敲一句docker pull直接超时这时候你才知道什么叫做“黑匣子服务器”。标题里这一串东西——Linux 环境下的 Docker 安装包、Nginx 安装包、Docker 容器里的 Nginx 启动脚本、OpenJDK 8 镜像安装包——本质上就是一套完整的离线交付物解决的是“在一台干净的 Linux 机器上把 Java 8 应用和反向代理跑起来”这件事。适合谁部署实施工程师、运维以及要给客户交付单体 Java 应用的技术负责人。核心思路只有一条所有依赖的东西必须在到达现场之前全部变成文件然后带到目标机器上直接落盘。整套交付物的依赖关系很清晰Docker 是底座OpenJDK 8 镜像负责运行 Java 应用Nginx 负责反向代理外部流量而启动脚本则把这些组件串成一个可重复执行的操作。我从实际交付经验出发把每一块的安装包准备、落地方式和配置脚本拆开讲最后落到一组验证命令上。整个过程不复杂但坑不少尤其是离线环境下的镜像导入和容器启动顺序这两处最容易翻车。2. 把Docker安装包落地到Linux在线源与离线rpm包两种路径2.1 在线安装docker-ce最小命令与两个必调参数如果目标机器能访问外网Docker 的安装是最简单的一步。常见做法是直接配置 Docker 官方仓库然后安装docker-ce。先卸载掉系统自带的旧版本再装新包避免出现dockerd和containerd版本不匹配的问题。sudo yum remove -y docker docker-client docker-common docker-engine sudo yum install -y yum-utils sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo sudo yum install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin sudo systemctl enable --now docker这段命令的顺序是有讲究的。yum-utils提供yum-config-manager命令没有它你没法添加 Docker 官方源。安装docker-ce时必须把containerd.io一起装上因为新版本的dockerd依赖它来做容器生命周期管理。docker-compose-plugin是给后续用docker compose编排多容器用的不是必须但建议一同安装。装完后systemctl enable --now一次搞定开机自启和立即启动不用再单独start。装完第一件事不是拉镜像而是检查两个参数。第一docker info看Cgroup Driver是不是systemd如果不是后面的容器日志采集和资源限制会有一堆玄学问题第二编辑/etc/docker/daemon.json把镜像加速器和日志驱动设置好{ registry-mirrors: [https://docker.m.daocloud.io], log-driver: json-file, log-opts: { max-size: 20m, max-file: 3 }, exec-opts: [native.cgroupdriversystemd] }registry-mirrors只对 Docker Hub 的拉取生效内网私有仓库用insecure-registries字段单独配置。log-opts限制单个容器日志文件的上限是 20MB保留 3 个文件这一步能避免 Java 应用疯狂打日志把磁盘塞满。改完配置必须重启 Dockersudo systemctl restart docker。注意exec-opts里的native.cgroupdriversystemd要和宿主机的 systemd 保持一致否则 kubelet 之类的组件会报错。2.2 离线环境装Docker准备好rpm包再进场离线环境是这套交付物真正的用武之地。你需要在有外网的机器上把 Docker 相关的 rpm 包全部下载好带到目标机器上安装。不要天真地只下载一个docker-cerpm它有一堆依赖少了containerd.io会直接报错。用--downloadonly参数可以一次性把所有依赖拉下来# 在有外网的机器上执行 sudo yum install --downloadonly --downloaddir/opt/docker-rpms \ docker-ce docker-ce-cli containerd.io docker-compose-plugin # 把整个目录打包带走 tar -czf docker-offline.tar.gz -C /opt docker-rpms--downloadonly不会安装软件包只是把 rpm 文件下载到指定目录。执行完后检查/opt/docker-rpms目录里面除了 docker-ce 之外应该还有container-selinux、libseccomp、audit-libs之类的依赖包。目标机器上的安装命令很简单# 在离线目标机器上执行 tar -xzf docker-offline.tar.gz -C /opt cd /opt/docker-rpms sudo rpm -ivh *.rpm --nodeps sudo systemctl enable --now docker--nodeps在这里是个双刃剑。离线环境下依赖可能不完整加上它可以强制安装但如果真缺了关键的库dockerd启动时会闪退。所以加--nodeps之后必须紧接着验证sudo docker info能正常输出版本信息才说明安装成功。如果你不想用--nodeps就老老实实按rpm -ivh报错提示一个个补齐依赖这通常意味着你要回有外网的机器上再拉一轮包。从交付效率看先加--nodeps试装出问题再补依赖是更快的路径。离线安装有一点容易被忽略装完后/etc/docker/daemon.json是空的不会自动写入上面的镜像加速和日志配置。记得把在线环境那份配置手动复制过去不然容器日志会无限增长。这个文件我每次都会放进交付物里让启动脚本检查它的存在。3. OpenJDK 8镜像安装包从下载到离线导入的完整链路3.1 选openjdk:8-jdk-alpine还是eclipse-temurin:8-jdkOpenJDK 8 镜像是这套交付物里 Java 应用的地基。很多人直接docker pull openjdk:8-jdk-alpine但这只适合拿来跑个不依赖 glibc 的小工具。线上 Java 应用我一般推荐eclipse-temurin:8-jdk因为它基于 Ubuntu底层是 glibc和绝大多数 Java 生产环境兼容。两者的差别很实际镜像体积基础系统适用场景openjdk:8-jdk-alpine约 150MBAlpine Linux / musl libc轻量工具、简单Java程序eclipse-temurin:8-jdk约 400MBUbuntu / glibc生产Java应用、需依赖本地库openjdk:8-jre-alpine约 120MBAlpine Linux只跑运行时不编译体积差三倍但生产环境千万别为了省这 250MB 牺牲兼容性。Alpine 镜像里libstdc、fontconfig这类库经常缺失Java 应用如果调用了 JNI 或者生成验证码图片就会在运行时突然抛ClassNotFoundException或UnsatisfiedLinkError这类问题最折磨人。所以离线交付包我一般打好两个镜像一个eclipse-temurin:8-jdk给业务应用用一个openjdk:8-jdk-alpine给排查网络问题时的临时容器用。两三百兆的存储成本换来现场排查的底气值。3.2 本地导出、压缩与离线导入的完整命令在有外网的机器上拉取镜像后要用docker save把镜像导出成 tar 文件而不是直接拷贝镜像目录。docker save会把镜像的所有层完整保留到目标机器上用docker load能精确还原镜像的 ID 和 Tag这是官方支持的标准搬运方式。我一般会先打 tag再导出免得镜像迁移到目标机器后 tag 指向不明确# 1. 在有外网的机器上拉取镜像 docker pull eclipse-temurin:8-jdk docker pull openjdk:8-jdk-alpine # 2. 重新打tag带上应用标识 docker tag eclipse-temurin:8-jdk registry.internal/java8:base docker tag openjdk:8-jdk-alpine registry.internal/java8:alpine # 3. 导出为tar包并压缩 docker save registry.internal/java8:base -o java8-base.tar docker save registry.internal/java8:alpine -o java8-alpine.tar gzip java8-base.tar java8-alpine.tar第二步打 tag 不是多此一举。离线交付的镜像来源可能五花八门统一 tag 到内网命名空间下启动脚本里引用镜像名就不会因为来源不同而出错。docker save默认不压缩所以导出后一定要gzip400MB 的镜像能压到 180MB 左右传文件时能省不少时间。到目标机器后的导入操作# 在离线目标机器上执行 docker load -i java8-base.tar.gz docker load -i java8-alpine.tar.gz # 验证镜像可用 docker run --rm registry.internal/java8:base java -versiondocker run --rm最关键的是--rm表示验证完立即删除容器不留垃圾。java -version会输出到标准错误终端确实能看到openjdk version 1.8.0_xxx之类的结果看到这个才代表镜像里的 JDK 真的能跑。有个细节docker load一个 400MB 的 tar 包可能需要一两分钟期间没有任何进度条输出终端看起来像卡住了别急着 CtrlC耐心等。导入完成后docker images确认两个镜像的 tag 和大小都正常再继续下一步。如果目标机器的磁盘空间紧张可以先删掉java8-alpine.tar.gz再导入下一个包避免同时占用两份空间。4. Nginx容器化启动脚本从镜像拉取到一条命令跑起来4.1 宿主机的Nginx首选方案用容器跑不用yum装标题里把“Nginx安装包”和“docker容器的nginx启动脚本”并列很多人会纠结到底走哪条路。我的建议生产环境优先把 Nginx 跑在 Docker 容器里宿主机的 Nginx 安装包只作为一个后备方案。原因很实际Nginx 的配置随应用版本迭代容器化之后回滚就是docker rm加docker run两条命令而宿主机安装的 Nginx 配置文件散落在/etc/nginx下改错了还要手工备份恢复这是最原始的“后悔药”成本。离线交付物里 Nginx 镜像的选择我推荐nginx:1.24-alpine。Alpine 版 Nginx 体积小约 20MB且 Nginx 不像 JDK 那样依赖 glibc 生态Alpine 的 musl libc 不会造成兼容性问题。镜像拉取和导出方式与 OpenJDK 8 完全一致# 在有外网的机器上 docker pull nginx:1.24-alpine docker tag nginx:1.24-alpine registry.internal/nginx:base docker save registry.internal/nginx:base -o nginx-base.tar gzip nginx-base.tar宿主机上照常用 yum 装一个nginx包的做法我也保留在交付物里作为启动脚本的前置依赖检测项。因为启动脚本需要用到curl做健康检查而curl在最小化安装的系统里可能不存在这时候一个顺手装上的nginx包会连带把curl依赖装上。不过重要的事情说三遍Nginx 配置和服务跑在容器里宿主机的 nginx 包只是备胎装好后不要systemctl start nginx免得 80 端口被它抢占容器启动时端口冲突。4.2 启动脚本逐行拆解docker run参数和端口映射逻辑下面这份start-nginx.sh是我离线交付物里最核心的文件。它的设计目标很简单幂等、可重复执行、出错可见。脚本目录我固定放在/opt/deploy/nginx/配置文件挂载目录和日志目录提前创建好。#!/usr/bin/env bash # Nginx容器启动脚本 - 幂等执行 set -e NGINX_IMAGEregistry.internal/nginx:base CONTAINER_NAMEapp-nginx CONF_DIR/opt/deploy/nginx/conf.d LOG_DIR/opt/deploy/nginx/logs PORT_HTTP80 PORT_HTTPS443 # 1. 检查镜像是否存在 if ! docker image inspect $NGINX_IMAGE /dev/null 21; then echo ERROR: nginx镜像不存在请先执行 docker load 导入 exit 1 fi # 2. 检查端口占用 for port in $PORT_HTTP $PORT_HTTPS; do if ss -lntp | grep -q :$port ; then echo ERROR: 端口 $port 已被占用先释放再执行脚本 exit 1 fi done # 3. 清理已存在的旧容器保留配置删除容器本体 docker rm -f $CONTAINER_NAME /dev/null 21 || true # 4. 创建配置目录并写入default.conf mkdir -p $CONF_DIR $LOG_DIR cat $CONF_DIR/default.conf EOF server { listen 80; server_name _; client_max_body_size 100m; location /api/ { proxy_pass http://172.17.0.1:8080; 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 / { root /usr/share/nginx/html; index index.html; } } EOF # 5. 启动Nginx容器 docker run -d \ --name $CONTAINER_NAME \ --restartalways \ -p $PORT_HTTP:80 \ -p $PORT_HTTPS:443 \ -v $CONF_DIR:/etc/nginx/conf.d:ro \ -v $LOG_DIR:/var/log/nginx \ -e TZAsia/Shanghai \ $NGINX_IMAGE # 6. 健康检查等待容器启动并检测80端口 sleep 3 if docker exec $CONTAINER_NAME nginx -t /dev/null 21; then echo Nginx配置合法容器已启动 else echo ERROR: Nginx配置有误查看容器日志: docker logs $CONTAINER_NAME exit 1 fi # 7. 输出验证结果 curl -sI http://127.0.0.1:80 | head -n 3脚本的第一个关键点是set -e任何一行命令出错立即退出不会出现“容器没起来但脚本继续跑完”的假成功。第二步检查端口用的是ss -lntp比netstat更通用且带-p能看到占用端口的进程 PID方便排查是谁抢占了 80/443 端口。第三步docker rm -f加|| true是幂等的核心第一次执行时没有旧容器rm会报错但被true吞掉脚本继续第二次执行时旧容器被强制删除再重建配置目录里的default.conf因为挂载在宿主机所以不受影响这是 Nginx 配置能独立迭代的关键。docker run -d之后的参数逐个解释--name app-nginx固定容器名保证关闭和删除命令不需要查 ID。--restartalways容器崩溃后 Docker 自动重启宿主机开机时也会自动拉起这条对无人值守的服务器极重要。-p 80:80和-p 443:443真实场景下宿主机上多个站点需要不同端口改成-p 8080:80 -p 8443:443即可脚本里用变量PORT_HTTP和PORT_HTTPS可以在顶部改。-v $CONF_DIR:/etc/nginx/conf.d:ro只读挂载宿主机配置目录到容器内容器内无法篡改配置本机改文件后执行docker exec app-nginx nginx -s reload即可热加载新配置。-e TZAsia/ShanghaiNginx 容器默认时区是 UTC加了这条之后访问日志里的时间戳才是北京时间排查问题时不用再手工加 8 小时。第 7 步的curl -sI是最终验证输出HTTP/1.1 200 OK就说明整个链路通了。这里我用-I只取响应头不下载页面内容省流量也快。如果返回的是000状态码基本可以断定容器没起来直接跳去docker logs看报错。5. 启动脚本与镜像部署避坑常见问题排查5.1 容器启动三秒就退出docker logs只看到exit code 1遇到这个现象先别慌这是 Nginx 容器化部署最常见的翻车现场。docker run后容器状态立刻变成Exited (1)即使配置看起来没什么问题。原因是配置文件里有语法错误或者容器内 Nginx 进程没有以前台模式运行。官方 Nginx 镜像默认用nginx -g daemon off;保持前台运行只要配置能通过nginx -t进程就不会自己退出。排查时先执行docker logs --tail 20 app-nginx看到nginx: [emerg] listen() to 0.0.0.0:80 failed (98: Address already in use)基本就是端口被抢占了看到nginx: [emerg] unexpected } in /etc/nginx/conf.d/default.conf则是配置语法错误需要手动修正default.conf后重新执行启动脚本。这个问题的解决路径很固定先在宿主机执行nginx -t -c /etc/nginx/nginx.conf验证全部配置确认无误后再跑启动脚本从根上避免反复试错。5.2 配置目录挂载不生效容器内看到的是默认nginx页面挂载了conf.d目录容器内也确实是这个路径但访问时返回的是 Nginx 默认欢迎页。这个坑的原因往往是配置文件的后缀名不对。Nginx 镜像的主配置/etc/nginx/nginx.conf里有include /etc/nginx/conf.d/*.conf;这一行它只加载.conf结尾的文件。如果你放进去的是default.txt或者改了一行留下的default.conf~临时文件Nginx 出于安全考虑不会加载它们。解决确认配置文件名严格以.conf结尾然后执行docker exec app-nginx nginx -t和docker exec app-nginx nginx -s reload。顺便检查宿主机挂载目录的权限chmod -R 644配置文件chmod 755目录。权限不足时容器内 Nginx 进程可能读不了配置现象同样是“配置改了但没生效”只有docker logs能看到permission denied报错。5.3 镜像导入成功docker run时却报invalid reference format离线导入registry.internal/java8:base后执行docker run registry.internal/java8:base结果报docker: invalid reference format。问题出在宿主机 Docker 对镜像仓库地址的解析上如果镜像 tag 没有带版本号或者直接用了/开头的路径部分 Docker 版本会解析失败。另一个常见原因是执行docker load后镜像 tag 变成了registry.internal/java8:base之外的名字比如none这种情况是导出时用了docker save imageID而不是docker save repo:tag。解决执行docker images查看实际镜像名和 tag如果为空先用docker tag IMAGE_ID registry.internal/java8:base手动打 tag再执行启动脚本。这类问题本质是镜像的 repo 标识链断了docker tag就是给它重新接上。5.4 容器里的时间比宿主机快8小时日志里全是UTC时间eclipse-temurin系列镜像基于 Ubuntu默认时区是 UTC。Java 应用打印的日志、Nginx 的访问日志都会比北京时间慢 8 个小时排查问题时对不上时间线容易误判故障发生顺序。这个问题在 OpenJDK 8 镜像里尤其容易踩因为 JVM 的默认时区读取的是/etc/localtime而这个文件在容器里指向 UTC 时区。解决启动 Java 容器时加环境变量-e TZAsia/Shanghai或者在启动脚本里挂载宿主机的时区文件docker run -d \ --name java-app \ -e TZAsia/Shanghai \ -v /etc/localtime:/etc/localtime:ro \ registry.internal/java8:base \ java -jar your-app.jar这两条是一个保险组合TZ环境变量对大多数 JVM 实现生效挂载/etc/localtime则直接改掉镜像里的默认时区配置。线上我一般两个都加避免某些 JVM 版本只认TZ不认/etc/localtime的差异这类兼容性问题属于典型的“看着是同一个镜像跑起来却不一致”的黑匣子现象。5.5 离线镜像包传完了docker load时报tar包损坏服务器上docker load -i java8-base.tar.gz执行到一半报unexpected EOFcrc 对不上。离线交付最痛的坑scp传大文件时网络闪断文件看似传完实际校验失败。用scp传输 200MB 以上的文件我遇到过好几次这个问题血泪经验总结下来就是三步传到目标机后先对比 md5 再执行导入、用分段压缩减小单包体积、校验通过前不删本地源包。# 在有外网的机器上计算哈希 md5sum java8-base.tar.gz java8-base.tar.gz.md5 # 在目标机器上验证 md5sum -c java8-base.tar.gz.md5把 md5 文件一起放到交付物里导入镜像前先跑md5sum -c输出OK才说明文件完整。这一步也是目录清单里必须有的内容别省。集齐镜像、脚本、校验文件之后整套交付物才算真正满足“到了现场就能装装了就能跑”的要求。6. 验证脚本与进阶操作把启动命令升级成自检工具交付完最后一步不是留一句“重启一下试试”而是给现场的人一套验证脚本。我把启动和验证拆开check.sh专门做健康检查逻辑简单到新手也能照着输出判断问题#!/usr/bin/env bash # 交付自检脚本 - 验证Docker、Nginx容器、Java镜像三部分 set -e echo 1. Docker进程状态 systemctl is-active docker echo 2. Nginx容器运行状态 docker ps --filter nameapp-nginx --format {{.Status}} echo 3. Nginx配置合法性 docker exec app-nginx nginx -t echo 4. HTTP响应码 curl -s -o /dev/null -w HTTP_STATUS: %{http_code}\n http://127.0.0.1:80 echo 5. Java镜像可用性 docker run --rm registry.internal/java8:base java -version 21 | head -n 1 echo 6. 磁盘剩余空间 df -h / | tail -n 1这段脚本里第 4 步用curl -s -o /dev/null -w只输出 HTTP 状态码比curl -sI更干净适合放进自动化工具里按状态码判断。第 6 步的磁盘检查是我自己加的因为 Java 应用跑一段时间后日志可能挤爆磁盘查一下能提前预防故障。这套脚本的价值在于把“看起来正常”变成“验证过正常”。进阶操作里值得做的是给 Nginx 容器加资源限制。Java 8 应用和 Nginx 共用 Docker 底座如果不限制容器内存某个应用出问题会把整个宿主机的内存吃光连 Docker 守护进程都连不上。在启动脚本的docker run参数里加两行--memory512m --cpus1 \ --log-opt max-size10m --log-opt max-file3 \--memory512m限制 Nginx 容器最大使用 512MB 内存--cpus1限制它最多占满一个核心。这个限额对静态资源转发和反向代理场景足够又能防止异常进程失控。日志参数再次限制单容器日志大小和daemon.json的双层保险防止/var/lib/docker目录被日志撑满。最后提醒一句习惯性做法交付后把整套启动脚本的default.conf里server_name改成实际域名并且做完一次“冷启动测试”——关掉服务器电源重新开机确认 Nginx 容器和 Java 容器都能自动恢复。这一点比任何文档都管用。整套方案下来的体会是离线部署的问题从来都不是命令有多难而是每一个依赖项有没有被转化成文件、被校验、被串联。希望帮到你少踩一次现场交付的坑。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑