背景
跟 Docker 磕了兩天,將一個包含 N 個微服務的應用部署包改造,使其能夠生成 Docker 鏡像,并在 Docker 容器中運行。幾年前玩過 Docker,隱約記得幾個命令「Dockerfile 命令:黑卡飲料、山楂果費、哦SUV,機器學習」,項目中用不到,早忘光了。
開著 metaso,一路追問了兩天,終于搞定了這個應用的 Dockerfile 編寫,卡住的點:
- 多層級命令結構下,啟動腳本中的
.
的相對路徑問題,它相對的是WORKDIR
而不是當前執行腳本路徑。 - 如何查看容器中運行程序的 logs 日志?非 Docker 命令輸出到控制臺的日志,而是 SpringBoot 應用通過 logback 組件寫入到文件系統的日志。
- SpringBoot 應用配置文件中的變量,如何通過 Docker 運行命令傳遞并接收?基于 Nacos Config 的應用,容器運行時需要傳遞 Nacos 的信息。
- 外部宿主機怎么訪問容器中的 Web 應用?
- 包含驗證碼的應用使用了 sun.awt.X11FontManager 繪圖異常,怎么調整 JDK鏡像?
- 一個容器中啟動 N 個微服務 VS 每個容器啟動一個微服務?
- 保證容器運行時不出現 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 目錄下的文件,可以按照以下步驟進行操作:
- 構建上下文:Docker 構建時需要一個構建上下文(context),這個上下文是一個目錄,Dockerfile 中的所有 COPY 和 ADD 指令都會相對于這個目錄來查找文件。因此,我們需要將 myApp 目錄作為構建上下文。
- 指定 Dockerfile 路徑:由于 Dockerfile 位于 appA 子目錄中,我們需要使用 -f 參數來指定 Dockerfile 的路徑。
- 編寫 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 腳本如下
- 根目錄下創建鏡像:
docker build -t myapp .
,針對當前文件目錄下的 Dockerfile進行編譯,且鏡像名稱必須小寫。 - 在父級別目錄中對子模塊構建:
docker build -t appa -f appA/Dockerfile .
- 后臺進程方式運行容器:
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
- 刪除容器:
docker rm containId
- 刪除鏡像:
docker rmi imageId
- 查看鏡像:
docker images
- 查看日志:
docker logs appa
【容器名稱】僅shell 執行時的控制臺日志。 - 針對運行狀態的容器,可以打印容器中日志目錄下的文件:
docker exec -it containId /opt/myApp/appA/logs/a.log
- 存儲鏡像:
docker save -o appA.tar appA:latest
啟示錄
第一點,糾結一個問題 「一個容器中啟動 N 個微服務 VS 每個容器啟動一個微服務?」根據 Docker容器的設計思想:
- 一次構建,到處運行"的可移植性
- 輕量高效的資源利用
- 微服務友好的架構哲學,單一職責原則:每個Container僅運行一個主進程(如Nginx/MySQL),通過組合多個Container完成復雜應用,天然契合微服務拆分。
- 開發即生產的生命周期管理
- 開放生態的擴展性
比較好的實踐方式是一個微服務一個鏡像,獨立容器啟動。那么之前的部署方式一臺服務器上三個服務運行的方式就不可行了。但是如果硬是這么實踐,也是沒問題的吧,畢竟通過瘦身包方式部署的,微服務共用了通用 jar 包。
一個容器里面運行 N 個微服務的 Dockerfile 文件就編寫在根目錄 myApp 中,然后逐次調用子模塊的 start.sh 腳本進行啟動,只用對根目錄的應用打一個鏡像就可以了。小公司的單體多模塊的應用,就沒必要打 N 個鏡像了。
但是需要注意在一個容器中同時啟動的微服務的個數,如果過多的話,可能 Docker 容器運行有資源約束,可能有些服務啟動會失敗。
第二點,JDK 選擇上面,本來所有模塊統一用輕量級 eclipse-temurin:8-jre-alpine
可以的,單有一個包含驗證碼的模塊,這個精簡鏡像存在字體缺失問題,所以對該模塊使用 openjdk:17-jdk
鏡像。
最后一點,docker run
命令的參數 -v
必須在 --name
之前。用了 Docker 容器管理后,程序就不需要提供 stop 腳本了,直接通過容器的 stop 命令就可以停止應用。