当前位置: 首页 > news >正文

SpringBoot微服务编写Dockerfile流程及问题汇总

背景

跟 Docker 磕了两天,将一个包含 N 个微服务的应用部署包改造,使其能够生成 Docker 镜像,并在 Docker 容器中运行。几年前玩过 Docker,隐约记得几个命令「Dockerfile 命令:黑卡饮料、山楂果费、哦SUV,机器学习」,项目中用不到,早忘光了。

开着 metaso,一路追问了两天,终于搞定了这个应用的 Dockerfile 编写,卡住的点:

  1. 多层级命令结构下,启动脚本中的 . 的相对路径问题,它相对的是 WORKDIR 而不是当前执行脚本路径。
  2. 如何查看容器中运行程序的 logs 日志?非 Docker 命令输出到控制台的日志,而是 SpringBoot 应用通过 logback 组件写入到文件系统的日志。
  3. SpringBoot 应用配置文件中的变量,如何通过 Docker 运行命令传递并接收?基于 Nacos Config 的应用,容器运行时需要传递 Nacos 的信息。
  4. 外部宿主机怎么访问容器中的 Web 应用?
  5. 包含验证码的应用使用了 sun.awt.X11FontManager 绘图异常,怎么调整 JDK镜像?
  6. 一个容器中启动 N 个微服务 VS 每个容器启动一个微服务?
  7. 保证容器运行时不出现 Exited 状态的关键语句 tail -f /dev/null

需求描述

我有一个应用目录 myApp,下面有一个 appA,包含 bin/start.sh bin/stop.sh,Dockerfile。appA 需要引用到 myApp 目录的文件,现在需要对这个目录打镜像包含父级目录,而Dockerfile 位于子目录 appA中,怎么实现呢?

向秘搜AI输入上面的提问,它梳理出应用的目录结构如下:

	myApp/├── commonJar├── bin/│   ├── startAll.sh│   └── stopAll.sh		├── appA/│   ├── bin/│   │   ├── start.sh│   │   └── stop.sh│   └── Dockerfile│   └── bootstrap.yml		│   └── appA.jar	├── appB/│   ├── bin/│   │   ├── start.sh│   │   └── stop.sh│   └── Dockerfile│   └── appB.jar├── appC/│   ├── bin/│   │   ├── start.sh│   │   └── stop.sh│   └── Dockerfile│   └── appC.jar		└── Dockerfile

要实现将 myApp 目录及其子目录 appA 打包成 Docker 镜像,并且 appA 中的 Dockerfile 需要引用 myApp 目录下的文件,可以按照以下步骤进行操作:

  1. 构建上下文:Docker 构建时需要一个构建上下文(context),这个上下文是一个目录,Dockerfile 中的所有 COPY 和 ADD 指令都会相对于这个目录来查找文件。因此,我们需要将 myApp 目录作为构建上下文。
  2. 指定 Dockerfile 路径:由于 Dockerfile 位于 appA 子目录中,我们需要使用 -f 参数来指定 Dockerfile 的路径。
  3. 编写 Dockerfile:在 Dockerfile 中,使用相对路径来引用 myApp 目录下的文件。

编写Dockerfile

单独为每个模块编写 Dockerfile,以模块 appA 为例,编写如下内容:

#use jdk
FROM eclipse-temurin:8-jre-alpine#work dir in docker.
RUN mkdir /opt/myApp
WORKDIR /opt#copy all content in myApp/* to /apps.
COPY . myApp#grant start.sh and stop.sh
RUN chmod +x /opt/myApp/appA/bin/start.sh#expose all ports for appA
EXPOSE 8080ENTRYPOINT ["/opt/myApp/appA/bin/start.sh"]
CMD ["in"]

调整 appA/bin/start.sh 启动脚本,之前定位 appA.jar 是通过相对路径,直接在 Linux 运行正常,使用 Docker 容器运行时,由于工作目录设置的是父级 myApp ,相对路径也是相对工作目录的,所以直接启动会出现 appA.jar 文件不存在。

此外,启动时由于服务内部使用日志框架将日志输出到 appA/logs 目录了,所以忽略了控制台日志,要想保证容器不退出,必须让启动脚本处于挂起状态,通过一个启动参数控制。

调整启动脚本如下:

#!/bin/sh
basePath=$(cd `dirname $0`; pwd)
echo "basepath is $basePath"#change dir to appA ,which is .. of start.sh path
cd $basePath/..# start appA.jar use nohup & and ignore console log
loadPath=../commonJar
nohup java -Xmx512m -Dloader.path=$loadPath -jar -Dlogging.config=./logback-spring.xml  appA.jar >/dev/null 2>&1 &#hold on if has parameter 
if [ -n "$1" ]; thenecho '$1 is not empty, holding on for container'tail -f /dev/null
fi

此外,应用的 Nacos 参数需要容器通过设置环境变量的方式接收,因此修改应用的配置文件 bootstrap.yml ,使用环境变量接收:

spring:cloud:nacos:# nacos 服务器地址server-addr: ${address}# nacos 配置中心config:enabled: trueusername: ${username}password: ${password}# 引用的配置文件所属的命名空间,public时必须注掉,非public可以放开并修改为目标名称namespace: ${namespace}

这就编写好了模块 appA 的 Dockerfile 文件了,进入根目录 myApp 下依次创建镜像、运行容器、停止容器、删除容器、删除镜像。启动 DockerDesktop,

第一步,进入应用根目录 cd /xxx/myApp

第二步,运行构建命令:docker build -t appa -f appA/Dockerfile . 。构建完成后,执行 docker images 查看镜像:
在这里插入图片描述
第三步,执行容器启动:docker run -d -e address=IP:port -e username=xxx -e password=xxx -e namespace=nonPublic -p 8080:8080 -v /Applications/dockerlogs:/opt/myApp/appA/logs --name appa appa
等待容器启动后,使用 docker ps -a 查看容器状态,正常是 Up :
在这里插入图片描述
第四步,停止容器:docker stop d8ec9fbf8f49
在这里插入图片描述

第五步,删除容器:docker rm d8ec9fbf8f49,只能针对 Exited 状态的容器进行删除。

第六步,删除镜像:docker rmi 616b785f371e,只能针对没有容器运行的镜像进行删除。

Docker 操作汇总

针对每个应用提供一个 docker 操作脚本,方便操作,汇总 docker 脚本如下

  1. 根目录下创建镜像:docker build -t myapp .,针对当前文件目录下的 Dockerfile进行编译,且镜像名称必须小写
  2. 在父级别目录中对子模块构建:docker build -t appa -f appA/Dockerfile .
  3. 后台进程方式运行容器:docker run -d -e address=IP:port -e username=xxx -e password=xxx -e namespace=nonPublic -p 8080:8080 -v /Applications/dockerlogs:/opt/myApp/appA/logs --name appa appa ,容器环境变量、挂载日志文件,开放宿主机端口和容器端口一致。
  4. 查看容器:docker ps -a
  5. 删除容器:docker rm containId
  6. 删除镜像:docker rmi imageId
  7. 查看镜像:docker images
  8. 查看日志:docker logs appa【容器名称】仅shell 执行时的控制台日志
  9. 针对运行状态的容器,可以打印容器中日志目录下的文件:docker exec -it containId /opt/myApp/appA/logs/a.log
    9存储镜像: docker save -o appA.tar appA:latest

启示录

第一点,纠结一个问题 「一个容器中启动 N 个微服务 VS 每个容器启动一个微服务?」根据 Docker容器的设计思想:

  1. 一次构建,到处运行"的可移植性
  2. 轻量高效的资源利用
  3. 微服务友好的架构哲学,单一职责原则:每个Container仅运行一个主进程(如Nginx/MySQL),通过组合多个Container完成复杂应用,天然契合微服务拆分。
  4. 开发即生产的生命周期管理
  5. 开放生态的扩展性

比较好的实践方式是一个微服务一个镜像,独立容器启动。那么之前的部署方式一台服务器上三个服务运行的方式就不可行了。但是如果硬是这么实践,也是没问题的吧,毕竟通过瘦身包方式部署的,微服务共用了通用 jar 包。

一个容器里面运行 N 个微服务的 Dockerfile 文件就编写在根目录 myApp 中,然后逐次调用子模块的 start.sh 脚本进行启动,只用对根目录的应用打一个镜像就可以了。小公司的单体多模块的应用,就没必要打 N 个镜像了。

但是需要注意在一个容器中同时启动的微服务的个数,如果过多的话,可能 Docker 容器运行有资源约束,可能有些服务启动会失败。

第二点,JDK 选择上面,本来所有模块统一用轻量级 eclipse-temurin:8-jre-alpine 可以的,单有一个包含验证码的模块,这个精简镜像存在字体缺失问题,所以对该模块使用 openjdk:17-jdk 镜像。

最后一点,docker run 命令的参数 -v 必须在 --name 之前。用了 Docker 容器管理后,程序就不需要提供 stop 脚本了,直接通过容器的 stop 命令就可以停止应用。

相关文章:

  • day32 python解释性库PDPbox
  • 差分数组 - 对区间内元素的统一操作
  • Coze工作流-选择器的用法
  • LangChain入门和应用#1
  • COMPUTEX 2025 | 广和通5G AI MiFi解决方案助力移动宽带终端迈向AI新未来
  • 【java第19集】java面向对象编程详解
  • k8s-NetworkPolicy
  • Datawhale 5月llm-universe 第4次笔记
  • 【题解-洛谷】P6180 [USACO15DEC] Breed Counting S
  • docker面试题(4)
  • Win11上安装docker
  • 深度学习+Flask 打包一个AI模型接口并部署上线
  • 系统架构设计师案例分析题——数据库缓存篇
  • 学习笔记:黑马程序员JavaWeb开发教程(2025.4.9)
  • 第14天-Matplotlib实现数据可视化
  • 计算机视觉与深度学习 | Python实现CEEMDAN-ABC-VMD-DBO-CNN-LSTM时间序列预测(完整源码和数据)
  • 考取AZ-305 认证的心得
  • docker中使用openresty
  • 企业级数据加密权威方案:从 AES-CBC 到 AES-GCM 的升级实践
  • HJ23 删除字符串中出现次数最少的字符【牛客网】
  • 超3500种动物物种正遭气候变化威胁
  • 凤阳鼓楼修缮疑云:施工方曾允许自然人违规挂靠和转包
  • 瑞银:国际市场对中国资产关注度持续升温,外资回流空间仍很大
  • 欧洲要求参与俄乌谈判,美称俄乌不需要第三方
  • 百度一季度营收增长3%:净利下滑8%,云业务带动非在线营销业务营收增四成
  • 人形机器人灵犀X2将走出实验室,首轮预售推出3个版本