您的位置:首頁 >熱訊 > 財經評論 >

5 個技巧,讓你的 Dockerfile 像個大師! 焦點關注

今天給朋友們分享的是我們的嘉賓- 前端大佬神光的關于 Dockerfile 使用技巧的文章,希望能對大家有所幫助。

以下是大佬原文:


Docker 是一種容器技術,它可以在操作系統上創建多個相互隔離的容器。容器內獨立安裝軟件、運行服務。


(資料圖片)

但是,這個容器和宿主機還是有關聯的,比如可以把宿主機的端口映射到容器內的端口、宿主機某個目錄掛載到容器內的目錄。

比如映射了 3000 端口,那容器內 3000 端口的服務,就可以在宿主機的 3000 端口訪問了。

比如掛載了 /aaa 到容器的 /bbb/ccc,那容器內讀寫 /bbb/ccc 目錄的時候,改的就是宿主機的 /aaa 目錄,反過來,改宿主機 /aaa 目錄,容器內的 /bbb/ccc 也會改,這倆同一個。

這分別叫做端口映射、數據卷(volume)掛載。

這個容器是通過鏡像起來的,通過 docker run image-name。

比如:

dockerrun-p3000:3000-v/aaa:/bbb/ccc--namexxx-containerxxx-image

通過 xxx-image 鏡像跑起來一個叫做 xxx-container 的容器。

-p 指定端口映射,映射宿主機的 3000 到容器的 3000 端口。

-v 指定數據卷掛載,掛載宿主機的 /aaa 到容器的 /bbb/ccc 目錄。

這個鏡像是通過 Dockerfile 經過 build 產生的。

也就是這樣的流程:

一般在項目里維護 Dockerfile ,然后執行 docker build 構建出鏡像、push 到鏡像倉庫,部署的時候 pull 下來用 docker run 跑起來。

基本 CI/CD 也是這樣的流程:

CI 的時候 git clone 項目,根據 dockerfile 構建出鏡像,打上 tag,push 到倉庫。

CD 的時候把打 tag 的鏡像下下來,docker run 跑起來。

這個 Dockerfile 是在項目里維護的,雖然 CI/CD 流程不用自己搞,但是 Dockefile 還是要開發者自己寫的。

比如我創建一個 nest 項目:

npxnestnewdockerfile-test-pnpm

然后執行 npm run build,之后把它跑起來:

npmrunbuildnode./dist/main.js

這時候訪問 http://localhost:3000 可以看到 hello world,說明服務跑成功了:

那如何通過 Docker 部署這個服務呢?

首先,如果你沒安裝 docker,可以從 docker.com 下載 docker desktop,它自帶了 docker 命令:

跑起來可以看到本地的所有 docker 容器和鏡像:

命令行也是可用的:

然后我們來寫下 Dockerfile:

FROMnode:18WORKDIR/appCOPYpackage.json.COPY*.lock.RUNnpmconfigsetregistryhttps://registry.npmmirror.com/RUNnpminstallCOPY..RUNnpmrunbuildEXPOSE3000CMD[\"node\",\"./dist/main.js\"]

FROM node:18 是繼承 node:18 基礎鏡像。

WORKDIR /app 是指定當前目錄為 /app

COPY 復制宿主機的 package.json 和 lock 文件到容器的當前目錄,也就是 /app 下

RUN 是執行命令,這里執行了 npm install。

然后再復制其余的文件到容器內。

EXPOSE 指定容器需要暴露的端口是 3000。

CMD 指定容器跑起來時執行的命令是 node ./dist/main.js。

然后通過 docker build 把它構建成鏡像:

dockerbuild-tdockerfile-test:first.

-t 是指定名字和標簽,這里鏡像名為 dockerfile-test 標簽為 first。

然后在 docker desktop 的 images 里就可以看到這個鏡像了:

就是現在鏡像稍微大了點,有 1.45 G。

我們先跑起來看看:

docker run -d -p 2333:3000 --name first-container dockerfile-test:first

-d 是后臺運行。

-p 指定端口映射,映射宿主機的 2333 端口到容器的 3000 端口。

--name 指定容器名

然后就可以看到容器部分有了這個容器了:

瀏覽器訪問 http://localhost:2333 就可以訪問容器內跑的這個服務:

這就是 Dockerfile 構建成鏡像,然后通過容器跑起來的流程。

但是剛才也發現了,現在鏡像太大了,有 1.45G 呢,怎么優化一下呢?

這就涉及到了第一個技巧:

使用 alpine 鏡像,而不是默認的 linux 鏡像

docker 容器內跑的是 linux 系統,各種鏡像的 dockerfile 都會繼承 linux 鏡像作為基礎鏡像。

比如我們剛剛創建的那個鏡像,點開詳情可以看到它的鏡像繼承關系:

最終還是繼承了 debian 的 Linux 鏡像,這是一個 linux 發行版。

但其實這個 linux 鏡像可以換成更小的版本,也就是 alpine。

它裁剪了很多不必要的 linux 功能,使得鏡像體積大幅減小了。

alpine 是高山植物,就是很少的資源就能存活的意思。

我們改下 dockerfile,使用 alpine 的鏡像:

node:18-alpine3.14 是使用 18 版本的 node 鏡像,它底層使用 alpine 3.14 的基礎鏡像。

然后 docker build

dockerbuild-tdockerfile-test:second.

這次的 tag 為 second。

然后在 docker desktop 里看下:

好家伙,足足小了 900M。

我們點開看看:

可以看到它的底層 linux 鏡像是 alpine3.14。

體積小了這么多,功能還正常么?

我們跑跑看:

dockerrun-d-p2334:3000--namesecond-containerdockerfile-test:second

docker desktop 可以看到這個跑起來的容器:

瀏覽器訪問下,依然是正常的:

alpine 只是去掉了很多 linux 里用不到的功能,使得鏡像體積更小。

這就是第一個技巧。

然后再來看第二個:

使用多階段構建

看下這個 dockerfile,大家發現有啥問題沒:

有的同學可能會說:為什么先復制 package.json 進去,安裝依賴之后再復制其他文件,直接全部復制進去不就行了?

不是的,這兩種寫法的效果不同。

docker 是分層存儲的,dockerfile 里的每一行指令是一層,會做緩存。

每次 docker build 的時候,只會從變化的層開始重新構建,沒變的層會直接復用。

也就說現在這種寫法,如果 package.json 沒變,那么就不會執行 npm install,直接復用之前的。

那如果一開始就把所有文件復制進去呢?

那不管 package.json 變沒變,任何一個文件變了,都會重新 npm install,這樣沒法充分利用緩存,性能不好。

我們試試看就知道了:

現在重新跑 docker build,不管跑多少次,速度都很快,因為文件沒變,直接用了鏡像緩存:

dockerbuild-tdockerfile-test:second.

現在我們改下 README.md:

然后重新跑 build:

現在花了 25s,其實是沒有重新 npm install 的。

然后改下 package.json:

再跑 docker build

時間明顯多了很多,過程中你可以看到在 npm install 那層停留了很長時間。

這就是為什么要這樣寫:

這里沒問題,大家還能發現有沒有什么別的問題么?

問題就是源碼和很多構建的依賴是不需要的,但是現在都保存在了鏡像里。

實際上我們只需要構建出來的 ./dist 目錄下的文件還有運行時的依賴。

那怎么辦呢?

這時可以用多階段構建:

FROMnode:18-alpine3.14asbuild-stageWORKDIR/appCOPYpackage.json.RUNnpminstallCOPY..RUNnpmrunbuild#productionstageFROMnode:18-alpine3.14asproduction-stageCOPY--from=build-stage/app/dist/appCOPY--from=build-stage/app/package.json/app/package.jsonWORKDIR/appRUNnpminstall--productionEXPOSE3000CMD[\"node\",\"/app/main.js\"]

FROM 后面添加一個 as 來指定當前構建階段的名字。

通過 COPY --from=xxx 可以從上個階段復制文件過來。

然后 npm install 的時候添加 --production,這樣只會安裝 dependencies 的依賴。

docker build 之后,只會留下最后一個階段的鏡像。

也就是說,最終構建出來的鏡像里是沒有源碼的,有的只是 dist 的文件和運行時依賴。

這樣鏡像就會小很多。

我們來試試看:

dockerbuild-tdockerfile-test:third-f222.Dockerfile.

標簽為 third。

-f 是指定 Dockerfile 的名字。

然后 desktop 里看下構建出來的鏡像:

鏡像體積比沒有用多階段構建的時候小了 250 M。

然后跑起來試試看:

這次映射 2335 端口到容器內的 3000 端口。

依然能正常訪問:

這就是第二個技巧,多階段構建。

使用 ARG 增加構建靈活性

我們寫一個 test.js

console.log(process.env.aaa);console.log(process.env.bbb);

打印了環境變量 aaa、bbb

跑一下:

exportaaa=1bbb=2node./test.js

可以看到打印了這倆環境變量:

然后我們寫個 dockerfile,文件名是 333.Dockerfile:

FROMnode:18-alpine3.14ARGaaaARGbbbWORKDIR/appCOPY./test.js.ENVaaa=${aaa}\bbb=${bbb}CMD[\"node\",\"/app/test.js\"]

使用 ARG 聲明構建參數,使用 ${xxx} 來取

然后用 ENV 聲明環境變量。

dockerfile 內換行使用 \

之后構建的時候傳入構建參數:

dockerbuild--build-argaaa=3--build-argbbb=4-targ-test-f333.Dockerfile.

通過 --build-arg xxx=yyy 傳入 ARG 參數的值。

點擊查看鏡像詳情,可以看到 ARG 已經被替換為具體的值了:

然后跑起來:

dockerrun--namefourth-containerarg-test

這次就不用 -d 后臺運行了,直接看下日志:

可以看到容器內拿到的環境變量就是 ENV 設置的。

也就是說 ARG 是構建時的參數,ENV 時運行時的變量。

靈活使用 ARG,可以增加 dockerfile 的靈活性。

這就是第三個技巧。

CMD 結合 ENTRYPOINT

前面我們指定容器跑起來之后運行什么命令,用的是 CMD:

其實還可以寫成 ENTRYPOINT:

這兩種寫法有什么區別么?

我們來試試:

寫個 444.Dockerfile

FROMnode:18-alpine3.14CMD[\"echo\",\"光光\",\"到此一游\"]

然后 build:

dockerbuild-tcmd-test-f444.Dockerfile.

然后 run 一下:

dockerruncmd-test

沒有指定 --name 時,會生成一個隨機容器名。

就是這種:

這不是重點。

重點是用 CMD 的時候,啟動命令是可以重寫的:

dockerruncmd-testecho\"東東\"

可以替換成任何命令。

而用 ENTRYPOINT 就不會:

FROMnode:18-alpine3.14ENTRYPOINT[\"echo\",\"光光\",\"到此一游\"]

docker build:

dockerbuild-tcmd-test-f444.Dockerfile.

docker run:

dockerruncmd-testecho\"東東\"

可以看到,現在 dockerfile 里 ENTRYPOINT 的命令依然執行了。

docker run 傳入的參數作為了 echo 的額外參數。

這就是 ENTRYPOINT 和 CMD 的區別。

一般還是 CMD 用的多點,可以靈活修改啟動命令。

其實 ENTRYPOINT 和 CMD 是可以結合使用的。

比如這樣:

FROMnode:18-alpine3.14ENTRYPOINT[\"echo\",\"光光\"]CMD[\"到此一游\"]

docker build:

dockerbuild-tcmd-test-f444.Dockerfile.

docker run:

dockerruncmd-testdockerruncmd-test66666

當沒傳參數的時候,執行的是 ENTRYPOINT + CMD 組合的命令,而傳入參數的時候,只有 CMD 部分會被覆蓋。

這就起到了默認值的作用。

所以,用 ENTRYPOINT + CMD 的方式更加靈活。

這是第四個技巧。

COPY vs ADD

其實不只是 ENTRYPOINT 和 CMD 相似,dockerfile 里還有一對指令也比較相似,就是 ADD 和 COPY。

這倆都可以把宿主機的文件復制到容器內。

但有一點區別,就是對于 tar.gz 這種壓縮文件的處理上:

我們創建一個 aaa 目錄,下面添加兩個文件:

使用 tar 命令打包:

tar-zcvfaaa.tar.gz./aaa

然后寫個 555.Dockerfile

FROMnode:18-alpine3.14ADD./aaa.tar.gz/aaaCOPY./aaa.tar.gz/bbb

docker build 生成鏡像:

dockerbuild-tadd-test-f555.Dockerfile.

docker run 跑起來:

dockerrun-d--namesixth-containeradd-test

可以看到,ADD 把 tar.gz 給解壓然后復制到容器內了。

而 COPY 沒有解壓,它把文件整個復制過去了:

image.png
image.png

也就是說,ADD、COPY 都可以用于把目錄下的文件復制到容器內的目錄下。

但是 ADD 還可以解壓 tar.gz 文件。

一般情況下,還是用 COPY 居多。

案例代碼上傳了 github:

https://github.com/QuarkGluonPlasma/nestjs-course-code/tree/main/dockerfile-test

總結

Docker 是流行的容器技術,它可以在操作系統上創建多個隔離的容器,在容器內跑各種服務。

它的流程是 Dockerfile 經過 docker build 生成 docker 鏡像,然后 docker run 來跑容器。

docker run 的時候可以通過 -p 指定宿主機和容器的端口映射,通過 -v 掛載數據卷到容器內的某個目錄。

CI/CD 基本也是這套流程,但是 Dockerfile 是要開發者自己維護的。

Dockerfile 有挺多技巧:

使用 alpine 的鏡像,而不是默認的 linux 鏡像,可以極大減小鏡像體積,比如 node:18-alpine3.14 這種 使用多階段構建,比如一個階段來執行 build,一個階段把文件復制過去,跑起服務來,最后只保留最后一個階段的鏡像。這樣使鏡像內只保留運行需要的文件以及 dependencies。 使用 ARG 增加構建靈活性,ARG 可以在 docker build 時通過 --build-arg xxx=yyy 傳入,在 dockerfile 中生效,可以使構建過程更靈活。如果是想定義運行時可以訪問的變量,可以通過 ENV 定義環境變量,值使用 ARG 傳入。 CMD 和 ENTRYPOINT 都可以指定容器跑起來之后運行的命令,CMD 可以被覆蓋,而 ENTRYPOINT 不可以,兩者結合使用可以實現參數默認值的功能。 ADD 和 COPY 都可以復制文件到容器內,但是 ADD 處理 tar.gz 的時候,還會做一下解壓。

靈活使用這些技巧,可以讓你的 Dockerfile 更加靈活、性能更好。


最后,歡迎學編程的朋友們加入魚皮的,和上萬名學編程的同學共享知識、交流進步,學習原創項目并享有答疑指導服務。

往期推薦

關鍵詞:

在线看成人片,性感美女在线,91视频在线看,青柠电影在线看