iT邦幫忙

2021 iThome 鐵人賽

DAY 3
1

前言

今天的文章要來介紹如何產生 Docker 的映像檔了,有了上一篇文章的介紹相信讀者對於 Docker 已經有了初步的了解了,應該知道要有映像檔是可以用來虛擬化應用程式及其執行環境的檔案,但執行環境基本上不用自己寫因為 DockerHub 上面都有,所以今天這篇文章主要會介紹如何產生應用程式的映像檔。

不曉得大家有沒有寫過 C++,要產生一個可以執行的 .out 檔通常都會寫一個 Makefile 最後再下 make 的指令來產生 .out 檔,之所以要先講這個跟 Docker 毫無相關的原因是因為 Docker 要產生映像檔的方式跟這個幾乎是一樣的做法,而要產生映像檔時要寫的檔案就叫做 Dockerfile 也是本篇文章要講的重要內容。

由於筆者本身是前端出身因此這篇文章會來介紹如何產生前端的應用程式,接下來就正式進入本篇文章的重點吧!

Dockerfile 寫法

Dockerfile 的寫法簡單來說分為四個階段:

  • FROM:哪個映像檔(通常會抓取應用程式的執行環境,例如前端通常都是寫 JavaScript 所以就會利用 Node 作為執行環境)

  • WORKDIR:應用程式執行位置

  • ADD/COPY:新增或複製檔案到應用程式執行位置

  • RUN:執行應用程式

所以簡單的寫法就會像下面這樣:

接下來就簡單的用白話文來解釋一下上面每一行在做的事情:

  1. 第一行在做的事情就是我要抓取 node:10-alpine 映像檔,所以在這個 Dockerfile 產生的映像檔中就可以利用 node 的相關指令來做事情。
  2. 接下來就是我要讓這個應用程式執行在 /app 的資料夾中,但要如何可以讓這個 /app 的資料夾中有程式碼可以讓我執行呢?
  3. 所以我就要把本地端資料夾內的所有檔案都複製到這個 /app 中,這就是 COPY . /app 的作用。
  4. 最後則是執行 npm install 並且利用 node index.js 來執行這個 index.js 檔。

其實除了上面的寫法外,Dockerfile 還有以下幾個比較進階的寫法,分別是:

  • ARG:在 build 的過程中,可以從外部帶入一些參數進來作為後續的環境變數使用。

  • ENV:在 build 的過程中,可以定義一些環境變數,讓後面指令在執行時候可以使用,但前提是這個環境變數必須要從 ARG 中先被定義。

  • CMD:Docker 在啟動容器的時候會執行的指令,不過這個指令只能執行一次,也就是假如你的 Dockerfile 中寫了多個 CMD,最後只會執行最後寫的那個 CMD

  • ENTRYPOINT:Docker 在啟動容器的時候會優先執行的指令,所以比起 CMD 來說 ENTRYPOINT 會更優先執行。

上面的 CMDENTRYPOINT 看起來很像但其實兩者是不太一樣的,除了優先執行順序外還有一個很重要的觀念是,當今天 Dockerfile 只設定 CMD 而沒有設定 ENTRYPOINT 時,Docker 在啟動容器時會先使用預設的 ENTRYPOINT 指令,而預設的指令為 /bin/sh -c,所以假如今天要讓容器不用這種方式啟動的話,就要記得使用 ENTRYPOINT 這個寫法喔!

Dockerfile 重要觀念

其實 Dockerfile 還有一個很重要的觀念要跟讀者說,就是 Dockerfile 在每次要產生一個新的映像檔前都會先看 Dockerfile 中有沒有哪個是被修改過的,有點像是 Git 在檢查檔案有沒有被修改的道理,如果發現該行的結果其實是不需要修改的話,Docker 就會很聰明的直接利用上一次產生的映像檔當成是 cache 來當作是該行的結果。

舉例來說:在上面的例子每一次都一定會拉到正確的 node:10-alpine 所以每次在產生映像檔前我就不需要重新下載從之前已經下載好的 node:10-alpine 中拿過來用就好,也因為這個特性可以讓 Dockerfile 在寫法上可是有著效能上的差距,接下來我們就來一起看一下如何改善上面的 Dockerfile 寫法吧!

改善 Dockerfile 寫法

首先我們先來看原本的寫法:

上面這份 Dockerfile 最大的問題就在於 COPY . /app 以及 RUN npm i && node index.js 這兩行,每次只要我資料夾內有檔案變動,我就要重新複製所有的檔案到 /app 這個資料夾,乍看之下好像沒什麼問題,但熟悉前端的人就會知道這些檔案也包括了 package.json 以及 package-lock.json 這兩個用來管理 mode_modules 的檔案,也就會造成每次我複製檔案時都會重新跑底下 npm install 這就會讓整個 build 的過程變得相當久。

所以我們可以簡單調整一下,先複製 package.json 以及 package-lock.json 這兩個檔案,然後跑 npm install,這樣只要我沒有變動到安裝套件就不用花很長的時間再重新安裝套件了,最終改善後的寫法就像下面這樣:

雖然行數變多了,但我們來看一下執行結果:

可以發現只要我沒有動到 package.json 也就是沒有安裝新的套件,我就不會執行 npm i 這個指令,也因此讓 Docker 可以更有效率的產生這個映像檔,不用每次都重新安裝套件。

multi-stage build

multi-stage build 可以想像成是一個檔案有多個階段的 build,也就是我要把某一階段 build 好的檔案傳給另一階段讓他可以利用,舉個例子來說:前端通常都會利用 webpack 進行專案內容打包的工作,但打包完會變成一個又一個的靜態檔,這時候就必須要利用 multi-stage build 的觀念,把第一個 build 也就是經由 webpack 打包好的靜態檔傳給下一個 build 可能是利用 nginx 來 serve 這些靜態檔。

multi-stage build 的寫法也很簡單,只要用到 as 這個關鍵字就好,透過 as 就可以把這個映像檔的內容快速地提供給該 Dockerfile 下的其他映像檔使用,寫法如下:

可以發現這個 as 的關鍵字最主要的做法就是要把這個 FROM 的階段當作是一個標籤可以讓其他階段在進行操作時可以用 --from=標籤 的方式進行使用,所以 multi-stage build 最主要的作用就是幫助一個複雜的 Dockerfile 可以有效的管理各個階段的 build 並且可以讓下一個階段的 build 能使用上一個階段的產物。

Docker 指令集

  • docker build Dockerfile -t imageName:編譯 Dockerfile 並產生相對應 imageName 的映像檔, -t 代表的是 tag 的意思。

  • docker images:列出該機器下的所有映像檔。

  • docker run -d image:產生容器,並執行於背景。

  • docker container ps -a:列出該機器下的所有容器。

  • docker stop CONTAINER_ID:暫停 container,也就是關閉的意思。

  • docker image rm IMAGE_ID:移除該 IMAGE_ID 的映像檔。

    但有時候可能會出現這個畫面:

    這是因為這個映像檔正在被某個容器使用中,因此想要移除該映像檔前必須要先移除該容器。

  • docker container rm CONTAINER_ID:移除該 CONTAINER_ID 的容器

    但有時候可能會出現這個畫面:

    這是因為這個容器正在使用中,因此想要移除該容器前必須要先把該容器暫停。

小結

今天介紹了如何撰寫 Dockerfile 來產生 Docker 映像檔,但假如我今天有多個檔案每個檔案都必須要執行,那我不就要連續下三次 docker build 以及 docker run 的指令,這樣也太麻煩,即便我用 shellscript 的方式來統整這些指令還是很不方便,因此在明天的文章筆者要來介紹一個進階的 Docker 用法叫 Docker Compose,就敬請期待明天的文章吧XD

如果對於文章有什麼問題都歡迎在下面留言給筆者,讓我們就明天的文章見吧!


上一篇
Day02-容器化管理工具(Docker)
下一篇
Day04-管理 Docker 的各種組合(Docker Compose)
系列文
前端工程師學習 DevOps 之路30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言