###### tags: `教學` `Docker` `Dockerfile` `學習筆記` # Docker and Container 005 - Dockerfile [TOC] --- ## 1. 關於dockerfile - 在terminal打的一長串的指令 ←→ 可以整理進dockerfile裡 - 在這個檔案裡紀錄了要用哪些東西、做哪些事(安裝與設定的步驟),把它變成image(之後被拿來應用的東西) - 建立Dockerfile,須注意檔名大小寫,且不具副檔名,必須完全一模一樣(亦可使用「docker build -f OTHERNAME .」指定其他檔名的Dockerfile) - Dockerfile屬於文字檔案,可以使用任何編輯軟體撰寫,是以行為單位的指令和參數所組成,並以#標示為註解行 - 在 Dockerfile中,指令不區分大小寫。但為了更清楚地分辨指令和參數、指令一般是採用大寫形式。 ### dockerfile的內容 - Dockerfile 通常會有四部分:基底映像檔資訊、維護者資訊、映像檔操作指令和容器啟動時執行指令。 - 基本的有:FROM, RUN, ENTRYPOINT - 其他的還有什麼? - MAINTAINER(X), WORKDIR, ONBUILD, ADD, COPY, CMD, EXPOSE, ENV, LABEL, USER, ARG, STOPSIGNAL, SHELL - [參考資料](https://www.netadmin.com.tw/upload/news/NP171002000217100217565504.jpg) - dockerfile的運作過程: - dockerfile的第一個指令一定是FROM,去找指定的base image - 抓好image之後,會產生一個臨時的container(每一個指令都會產生一層容器) :::info **延伸閱讀** - [Docker 實戰系列(一):一步一步帶你 dockerize 你的應用](https://larrylu.blog/step-by-step-dockerize-your-app-ecd8940696f4) ::: --- ## 2. 編輯dockerfile ### 從**FROM**開始 1. 建立一個檔案 - `$ pwd`:查看現在所在位置(目錄) - `$ touch Dockerfile`,touch建立一個檔案,Dockerfile的d為大寫 2. 編寫相關內容 - **FROM alpine:latest**,alpine內含輕量的linux > 這行會載入程式需要的執行環境,會根據不同的需求下載不同的映像檔 - `$ cat Dockerfile`,列印出檔案內容 - `$ docker build -t kihifung/001 .`,在當下目錄建立一個image - ![](https://i.imgur.com/WjENS2A.jpg) - 因為dockerfile指令只有一行,所以step也只有1 - 6dbb9cc54074 為建立的image名稱 - `$ docker images`,查看已建立的image - ![](https://i.imgur.com/wTPNF8B.jpg) - `$ docker run kihifung/001 cat /etc/os-release`,啟一個container,並順便查看作業系統 --- ### 2.1. 利用dockerfile建立image `$ docker build -t kihifung/course-image-build .` - build 建立(映像檔) - -t:tag 取名 - kihifung/course-image-build:kihifung帳號下面的course-image-build專案 - `docker build -t kihifung/course-image-build --build-arg my_name_is="Tom Cruise" .` 建立映像檔的同時,加上一個參數的屬性(更改名字) ![](https://i.imgur.com/4Eoyg5k.jpg) --- ### 2.2. 進到container - `$ docker run -it kihifung/001 /bin/sh`,進到container的互動模式裡 - `$ docker container ls`,確認現在container的情形 - `$ docker run -d kihifung/001 tail -f /dev/null` - `tail -f`,跟蹤某個file的內容,並印到console上 - `/dev/null`,常用的範例file - `$ docker container ls`,確認現在container的情形 - `$ docker exec -it aa4e98479af1 /bin/sh`,進到container裡做事 - aa4e98479af1,container的名稱 --- ### 2.3. **ENTRYPOINT** > docker image執行時第一個執行的指令 > 寫在裡面,每一段都分別用"包著 ```dockerfile= FROM alpine:latest ENTRYPOINT [ "tail", "-f", "/dev/null" ] ``` - `$ docker build -t kihifung/002 .`,重新建立一個新的image - `$ docker images`,查看 - `$ docker run -d kihifung/002`,啟動container #### CMD vs ENTRYPOINT: 這兩個指令其實大多數情況互通,以下列舉幾個Dockerfile中功能相同的寫法: ```dockerfile= ENTRYPOINT ["java", "-jar", "target/accessing-data-mysql-0.0.1-SNAPSHOT.jar"] (建議寫法) ENTRYPOINT java -jar target/accessing-data-mysql-0.0.1-SNAPSHOT.jar CMD ["java", "-jar", "target/accessing-data-mysql-0.0.1-SNAPSHOT.jar"] CMD java -jar target/accessing-data-mysql-0.0.1-SNAPSHOT.jar ``` #### 查看container 1. 進到裡面看 - `$ docker exec -it ada485409e25 /bin/sh`,進到container裡做事 2. 從外面看 - `$ docker run -d -p 8080:80 kihifung/002`,將內外的port連結起來 - 8080:VM linux的port - 80:container的port --- ### 2.4. 安裝 Apache server - 編輯dockerfile檔: ```dockerfile= FROM alpine:latest RUN apk --update add apache2 # apk:alpine安裝套件的指令 RUN rm -rf /var/cache/apk/* # 清掉快取 ENTRYPOINT ["httpd", "-D", "FOREGROUND"] # ,啟動apache的httpd的服務 ``` - `$ docker build -t kihifung/004 .` - `$ docker run -d -p 8081:80 kihifung/004` - `$ ip a`,找出linux vm的ip位址,在瀏覽器查看,192.168.xxx.xxx:8081 --- ### 2.5. 建立共用變數 **ENV** > 在dockerfile裡,每個run都是使用臨時的container在執行 - 編輯dockerfile檔 ```dockerfile= FROM alpine:latest LABEL name="MyName" ENV myworkdir=/var/www/localhost/htdocs RUN apk --update add apache2 RUN rm -rf /var/cache/apk/* RUN cd ${myworkdir} \ && echo "<h3>I am Sean, this is a test page. line 1</h3>" >> index.html RUN cd ${myworkdir} \ && echo "<h3>I am Sean, this is a test page. line 2</h3>" >> index.html RUN cd ${myworkdir} \ && echo "<h3>I am Sean, this is a test page. line 3</h3>" >> index.html ENTRYPOINT ["httpd"] CMD ["-D", "FOREGROUND"] ``` - `$ docker build -t kihifung/005 .`建立映像檔 - `$ docker run -d -p 8080:80 kihifung/005` --- ### 2.6. **WORKDIR** >- 設定工作目錄 >- 修改ENV和WORKDIR >- 表示在這個 Docker 中的 Linux 即將會建立一個目錄 ```dockerfile= FROM alpine:latest LABEL name="MyName" ENV myworkdir=/var/www/localhost/htdocs WORKDIR ${myworkdir} RUN apk --update add apache2 RUN rm -rf /var/cache/apk/* RUN echo "<h3>I am Sean, this is a test page. line 1</h3>" >> index.html RUN echo "<h3>I am Sean, this is a test page. line 2</h3>" >> index.html RUN echo "<h3>I am Sean, this is a test page. line 3</h3>" >> index.html RUN echo "<h3>I am Sean, this is a test page. line 4</h3>" >> index.html ENTRYPOINT ["httpd"] CMD ["-D", "FOREGROUND"] ``` - `$ docker build -t kihifung/006 .`,建立image - `$ docker run -d -p 8084:80 kihifung/006`,起一個container跑 - 每次修改dockerfile內容,都要重新建立image,並重新使用container執行(之前port沒棄用,要重新給新port) --- ### 2.7. ARG >- ARG, argument。跟ENV不同的是,可以在build階段更改變數的值 >- 更改預設 ```dockerfile= FROM alpine:latest LABEL name="MyName" ENV myworkdir=/var/www/localhost/htdocs ARG whoami="Sean" WORKDIR ${myworkdir} RUN apk --update add apache2 RUN rm -rf /var/cache/apk/* RUN echo "<h3>I am ${whoami}, this is a test page. line 1st</h3>" >> index.html RUN echo "<h3>I am ${whoami}, this is a test page. line 2nd</h3>" >> index.html RUN echo "<h3>I am ${whoami}, this is a test page. line 3rd</h3>" >> index.html RUN echo "<h3>I am ${whoami}, this is a test page. line 9th</h3>" >> index.html ENTRYPOINT ["httpd"] CMD ["-D", "FOREGROUND"] ``` - `$ docker build -t kihifung/007 .`,建立image - `$ docker run -d -p 8086:80 kihifung/007`,起一個container跑 - `$ docker build --build-arg whoami=John -t kihifung/008 .`build-arg whoami=John 更改變數 --- ### 2.8. COPY >- 代表會將本機端 `[OO路徑]` 的所有檔案加到 Linux 的 `[XX路徑]` 目錄底下 >- 複製/引入(本地)文件內容,遠端請使用ADD - 格式為 `COPY <src> <dest>` - 複製本地端的 `<src>`(為 Dockerfile 所在目錄的相對路徑)到容器中的 `<dest>`。 ```dockerfile= FROM alpine:latest LABEL name="MyName" ENV myworkdir=/var/www/localhost/htdocs ARG whoami="Sean" WORKDIR ${myworkdir} RUN apk --update add apache2 RUN rm -rf /var/cache/apk/* RUN echo "<h3>I am ${whoami}, this is a test page. line 1st</h3>" >> index.html RUN echo "<h3>I am ${whoami},, this is a test page. line 2nd</h3>" >> index.html RUN echo "<h3>I am ${whoami},, this is a test page. line 3rd</h3>" >> index.html RUN echo "<h3>I am ${whoami},, this is a test page. line 9th</h3>" >> index.html COPY ./content.txt ./ RUN ls -l ./ RUN cat ./content.txt >> index.html ENTRYPOINT ["httpd"] CMD ["-D", "FOREGROUND"] ``` - `$ docker build -t kihifung/009 .`,建立image - `$ docker run -d -p 8089:80 kihifung/009`,起一個container跑 ### 2.9. **EXPOSE** - 是指 container 對外的埠號,在與外界溝通時使用 --- #### 小技巧 ```dockerfile= FROM <base_image> as <stage_name> 記得為每個 stage 命名,提高整體可讀性 未來若有更多 stage 加入時,可以不需跟著修改現存的 COPY 指令 COPY --from=<stage_name> 多利用 stage 名字取代數字以提高可讀性及維護性 ``` ### 2.10 其他範例 ```dockerfile= FROM node:9.2.0 COPY index.js package.json /app/ WORKDIR /app RUN npm install && npm cache clean --force CMD node index.js ``` 1. 找到適合的基底映像檔 base image > Docker 的 image 是一層一層疊加上去的,所以我們需要先在 [Docker Hub](https://hub.docker.com/explore/) 上選擇一個 base image,然後再慢慢把它加工成我們要的樣子 ![](https://miro.medium.com/max/491/1*bKRHfz7unRA35WHd9SBOCA.png) - 這個專案是用 Node.js 寫的,需要用 [node](https://hub.docker.com/_/node/) 作為 base image。決定 base image 後就在 Dockerfile 的第一行寫 [FROM](https://docs.docker.com/engine/reference/builder/#from) node:9.2.0 - `9.2.0` 指的是我們使用的 node image 版本,如果想要最新版的的話也可以指定 `node:latest` 2. copy 原始碼 - 建好 Node.js 環境之後要把 code 也包進去,這邊使用 `COPY` 指令,把 `index.js` 跟 `package.json` 複製到 `/app` 資料夾裡面 - 把本地(dockerfile所在地)的兩個檔案(index.js, package.json)複製到image的資料夾裡 3. 安裝 dependencies - 環境和code 也已經在裡面了,現在可切換到 `/app` 這個目錄安裝 dependencies - 使用 [WORKDIR](https://docs.docker.com/engine/reference/builder/#workdir) 切換到 `/app` 目錄,再用 [RUN](https://docs.docker.com/engine/reference/builder/#run) 跑 `npm install && npm cache clean --force` - 清 npm cache 是為了讓 build 出來的 image 不要包含這些 cache,這樣 image 會小一點 - 如果是其他語言就把 `npm install …` 換成其他指令就可以了,像 python 可能就是 `pip install …`,有 cache 的話記得要清一下 4. 設定 initial command - 環境、程式碼、dependencies 都準備好了,只剩把程式跑起來,這裡會用到 [CMD](https://docs.docker.com/engine/reference/builder/#cmd) 設定**這個 image 被跑起來時的預設指令**。把node跑起來: `node index.js`,這樣就完成了 Dockerfile。 5. build - 完成後就可以開始 build image,在專案目錄下跑 `$ docker build -t simple-express-server .` 就會根據 Dockerfile build 出你的 image - build 完之後查看現有映像檔, `$ docker image ls` ,可以看到多出一個 `simple-express-server`,這就是剛剛 build 出來的 image,裡面包含 node 的環境、程式碼還有 dependencies 跟預設啟動指令 6. 把 image 跑起來 * 有了 image 後可以 `$ docker run -p 3000:8080 simple-express-server` 把 image 跑起來 * 在 container 內跑的就是剛剛設定的預設指令 `node index.js`,`-p 3000:8080` 則是把 container 內的 8080 port 跟外部的 3000 port 接通,如此一來只要用瀏覽器到 `127.0.0.1:3000` 就可以看到 `Hello World!`,這樣就完成 dockerize 了,現在你的電腦即便什麼都沒有只裝 docker 也可以把這個程式跑起來 7. 跑在背景 * 如果照上面跑 `docker run -p 3000:8080 <image>` 的話會把終端機卡住,所以部屬的時候都會跑在背景,要跑在背景只要加一個 `-d` 就可以了,變成 `docker run -d <image>`,下指令後會得到一個 container ID,要看 log 的話可以跑 `docker logs <container ID>` 8. server 上沒有這個 image 怎麼辦 * 因為現在是在本機 build,遠端的 server 還沒有這個 image,如果要部屬必須先把 image 傳到遠端的機器,但這樣很麻煩,比較好的方式是**把 Dockerfile 跟所有原始碼放在 git repo 裡面,直接在 server 上 build image**,因為都是照 Dockerfile 裡面的內容跑,所以不管是在自己電腦還是 server 上 build 結果都是一樣的 - [參考資料](https://larrylu.blog/step-by-step-dockerize-your-app-ecd8940696f4) ### 2.11 撰寫規範 >- [Docker Shell vs. Exec Form](https://emmer.dev/blog/docker-shell-vs.-exec-form/) :::info - 矽谷牛的耕田筆記: 撰寫Dockerfile時有 Shell 與 Exec 兩種格式, RUN, CMD, ENTRYPOINT 等指令都同時支援。 - `Shell 格式就是 RUN command arg1 arg2 arg3` 這種直接描述的格式, - `Exec 則是用 [] 包起來`,每個參數單獨敘述,譬如 RUN ["command", "arg1", "arg2", "arg3"] 等。 - 本篇文章推薦 **`RUN 指令採取 Shell`** 格式,而 **`CMD/ENTRYPOINT 都應該採用 EXEC`** 格式。 如果不清楚差異,以及為什麼這麽寫可以參考原文: - 在 shell 形式中,`命令會從 shell 繼承環境變量` - 涉及到`由ENV指令設置的環境變量時,這兩種形式的行為是相同的` - 使用 exec 形式丟失的主要內容是所有有用的` shell 功能:子命令、管道輸出、鏈接命令、I/O 重定向等等,這些類型的命令僅適用於 shell 形式`。 - `大多數 shell 不會將進程信號轉發給子進程`,像是按下CTRL-C可能不會停止子進程 - 有一個特殊版本的 exec 形式CMD,CMD可作為ENTRYPOINT參數,這在shell無法實現。 ::: --- ## 3. 其他操作 ### 設定時區 ```dockerfile= FROM alpine:3.9 # 設定時區為上海 RUN apk add tzdata && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ && echo "Asia/Shanghai" > /etc/timezone \ && apk del tzdata CMD ["/bin/sh"] ``` :::info - [Docker 映象,基於 alpine 系統的時區配置](https://itw01.com/YJIXZED.html) ::: ### 從映像檔產生 Dockerfile 1. [抓取特定映像檔](https://registry.hub.docker.com/r/centurylink/dockerfile-from-image) 2. 以這個image來建立一個container,以解析目標image(結果會產生一個Dockerfile.txt) - [參考連結](https://philipzheng.gitbook.io/docker_practice/dockerfile/file_from_image) ## 4. 多階段構建 (multi-stage builds) > 目的:透過拆解步驟來撰寫dockerfile,以縮小image的規模 - 多階段構建 (multi-stage builds) - 透過將原先流水線中的多個映像檔整合進同個 Dockerfile 內,而後續的映像檔可透過指令取得中間映像檔 (intermediate image) 所產生的檔案 (artifacts),如此便能讓整個過程更為簡單,也**確保流水線簡潔易懂**及**提高維護性**。 :::info ### 參考資料 - [Use multi-stage builds(docker)](https://docs.docker.com/develop/develop-images/multistage-build/) - [什麼是 Docker multi stage?](https://cindyliu923.com/2021/05/23/What-is-docker-multi-stage/) - [透過 Multi-Stage Builds 改善持續交付流程](https://tachingchen.com/tw/blog/docker-multi-stage-builds/) - [透過 Multiple Stage Builds 編譯出最小的 Docker Image](https://jiepeng.me/2018/06/09/use-docker-multiple-stage-builds) - [Dockerfile - Multi-stage build 筆記](https://amikai.github.io/2021/03/01/docker-multi-stage-build/) - [Docker - MultiStage](https://ithelp.ithome.com.tw/articles/10224514) ::: --- ## 回顧 - [ ] Dockerfile是幹嘛的 - [ ] Dockerfile的運作方式 - [ ] 熟悉各種指令 ## 課後複習/測驗 ### Q7:什麼是Dockerfile? > 這對於初學者來說,是非常基本的問題,目的在於了解面試者是否已經具備建置任何Docker映像檔的能力,而適當的回答如下: Docker藉由讀取名稱為Dockerfile的文字檔來自動建置容器映像檔,它**包含建置特定映像檔所需之依順序執行的所有命令**,Dockerfile遵循特定格式和指令集,如下所示: ```dockerfile= FROM ubuntu CMD echo "This is a test." | wc ``` 接著,進行操作示範。先來嘗試建立一個Docker映像檔並運行成Docker容器,首先新增名為cmd.Dockerfile檔案,並加入下列內容:`FROM ubuntu CMD echo "This is a test." | wc`,然後建置Docker Image: - `$ docker build -t /mycmd -f ./cmd.Dockerfile` 現在,啟動容器及其結果: - `$ docker run /mycmd 1 4 16` --- 1. Dockerfile練習(nginx) 1. 使用nginx 2. 加上額外訊息(看自己想加什麼) 3. 將範例的英文名字改成自己的英文名字 1. 將content.txt內容做更動 - [參考連結](https://hahow.in/courses/5df27f1fa5ee510022a08500/assignments?item=5e76ff159c4b140023a6caf1) - content.txt的內容如下: ```htmlembedded= <div> <h3>My Book List</h3> <ul> <li><a href="#">Die Hard</a></li> <li><a href="#">Secret</a></li> <li><a href="#">Html 101</a></li> <li><a href="#">Kubernetes 202</a></li> <li><a href="#">AWS 303</a></li> </ul> </div> ``` 1. [Docker Debug 挑戰題 - 網頁跑板](https://ithelp.ithome.com.tw/articles/10257226) 1. 下載專案 `$ git clone https://github.com/uopsdod/docker-debug-initial.git` 2. 下載圖片 [https://github.com/uopsdod/docker-debug-initial-image/blob/main/docker\_debug\_cover.jpeg](https://github.com/uopsdod/docker-debug-initial-image/blob/main/docker_debug_cover.jpeg) 3. 建立 dokcer image `$ docker build -t mywebsite --no-cache .` 4. 啟動 docker container `$ docker run -d -p 81:80 --name mywebsite mywebsite` 5. 查看首頁畫面 - 找出 ip 位置 (ex. 192.168.64.8:81),[http://localhost:81/](http://localhost:81/) - [解答](https://youtu.be/TKIxxK9bJo4) --- ### 延伸閱讀 - [正確撰寫Dockerfile](https://www.netadmin.com.tw/netadmin/zh-tw/technology/7BD73E2A172C4847A3F72D238ACA5148) - [撰寫Dockerfile](https://ithelp.ithome.com.tw/articles/10207535) - [[Day 07]: Dockerfile 初探](https://zh-tw.coderbridge.com/series/9867865723164ad6b9de2a479ad9a37c/posts/6617bbab5e504cc28fd17475d035e849) --- - [回到目錄](https://hackmd.io/@Hualiteq/r1lye3M3d)